연준에서 긴축 사이클에 들어가면서 금리를 계속 올리려고 하면 한국은 덜 딸려 올라가고 일본은 거의 올라가지 못한다. 즉 엔화가 원화 대비보다도 약해진다고 볼 수 있다.
만약 내년부터 금리 인하가 시작된다고 본다면, 미국과 한국의 금리는 빨리 내려올수록 엔화는 상대적으로 강세를 보일 수 있다.
"금리 인하" 한다는 이야기가 나온다면, 현재 10년물 금리는 미국은 6~7차례 기준 금리 인하를 반영한 금리라고 볼 수 있는데, 그렇다면 엔화가 강세가 되려면 3.9%보다 더 많이 인하된다는 기대감이 반영되어야 엔화의 가격이 오르게 될 것이다. 그런데 만약에 연준이 그정도로 인하를 하지 않는다고 한다면 엔화의 강세는 나타나지 못하게 된다. 즉, 엔화는 강세가 되었다가 약세가 되었다가하는 반복되는 장세를 보일 가능성이 높다. 결국 엔화는 중장기적으로 볼 때 엔화의 강세가 나올 순 있으나, 그 길이 순탄치는 않을 것 같다.
그렇다면 일본의 정책 변화가 나타난다면 엔화가 강세를 보여줄 수도 있겠으나 現 BOJ 총제 우에다의 11월 발언를 보면, "일본 물가가 옛날에 비해 올라오고 있기 때문에, 연말부터 내년초까지는 이런 현상이 완화적인 정책을 쓰고 있는 일본 은행 입장에서는 큰 도전적인 시기가 될 것 같다." 발언 이후, 12월 Boj 회의에서 정책 변경이 일어나는 것 아니냐고 시장에서 기대했지만, 정작 12월 회의에서는 현 정책을 고수하겠다고 발표하면서 큰 실망감을 얻었다.
FOMC, 피벗은 언제부터?
12월 FOMC에서 중요한 내용을 요약하자면 아래와 같다.
첫 번째, 시중 금리에 관하여
11월에 financial Condition의 시중금리가 너무 높게 올라오다보니 연준에서 추가적인 기준 금리 인상은 안해도 되는 것 아닌가라는 이야기가 있었고, 이에 반응한 시장은 12월 달부터 시작해서 국채 금리가 빠르게 내려와 5.0%에서 4.15%까지 내려오게 되었다. 그래서 12월 FOMC에서는 금리 환경이 완화되었으니, 금리 인상은 아니더라도 지켜본다는 발표가 있을 것이라 예측했는데, 파월 의장은 금융 시장하고 중앙 은행하고 하는 기대가 다를 수 있다고 밝혔다. 즉, 금융 시장에서 금리가 내려갈 땐 신경쓰지 않겠다는 뜻이었고 이는 금리가 올라가는 것이 두려운 것인지 내려가는 것은 별 신경 쓰지 않겠다는 스텐스의 발언이었다.
두 번째, 금리 인하에 대한 시그널
긴축 사이클이 시작한 2022년 초부터 항상 나타나는 패턴이 있었는데, 연준은 점도표에서 금리를 높게 찍었다. 이를 보고 시장이 아니라고 했다가 따라가고, 했다가 따라가는 양상이 반복되었고, 최종적으로 연준에서 점도표를 5.5%~5.75%까지 찍었다. 이에 시장은 5.25%~5.5%를 찍게 되었다. 이후, 12월 FOMC에서 financial Condition에 대해서 이야기하면서 시장에서는 연준에서 5.5%~5.75%까지는 못 올릴 것이다라고 확신을 가지게 되었다.
연준은2024년 점도표에서 3번 인하하고 2025년에 추가 4번 인하한다고 이야기하고 있다. 즉 2년간 7번 인하할 것이다라고 말하고 있다. 하지만 시장에서는 내년에 6,7번을 할 것으로 예측하고 있다. 즉 시장에서는 연준이 기대하는 것보다는 더 빠르고 더 많이 인하하는 것을 기대하고 있다.
세 번째, QT(양적 긴축)에 대한 이야기
역레포의 금액이 2조 달러에서 빠르게 줄어들고 있는데, 이것이 양적 긴축에 영향을 줄 수 있는 것 아닌가라는 질문에 파월 의장은 큰 영향을 주지 않을 것 같고 양적 긴축은 스케쥴대로 밀고 갈 것이다라고 발언하였다.
시장이 이렇게 반응하는 이유가 무엇인가.
과거 미국 실업률 데이터를 보자면 실업률이 최저치까지 낮아졌다가 경제 침체와 함께 실업률이 최고치까지 올라가는 경향을 보였다. 과거를 보았을 때 연준은 성장에 대해서 신경을 쓸 수 밖에 없을 것이다.
18년~20년 금리 데이터를 보자면 금리를 계속 올리면서 18년 10월부터 점차 회사채, 신흥국 시장이 무너지기 시작했다. 이후 연준에서 금리 인상을 멈추고 시장에게 오히려 끌려가기 시작했다.
이런 경험을 통해서 연준은 한 번 끌려오기 시작하면 끌려올 수 밖에 없다는 생각을 가지게 된 것이다.
데이터를 여러 형태로 바꾸다 보면 문자열을 조작해야 하는 경우가 많으므로 빅쿼리는 내장 문자열 함수 라이브러리를 제공한다.
WITH
example AS(
SELECT
*
FROM
UNNEST ( ['Seatle','New York', 'Singapore'] ) AS city )
SELECT
city,
LENGTH(city) AS len,
LOWER(city) AS lower,
STRPOS(city, 'or') AS orpos
FROM
example
이 쿼리는 문자열의 길이를 계산해 문자열을 소문자로 만든 후 'city' 컬럼에서 부분 문자열의 위치를 찾는다.
부분 문자열 'or'은 'new york' 및 'Singapore'에서 찾을 수 있지만 'Seattle'에서는 찾을 수 없다.
문자열 조작에 특히 유용한 두 가지 기능은 SUBSTR 및 CONCAT 이다. SUBSTR은 부분 문자열을 추출하고 CONCAT는 입력값을 연결한다.
WITH example AS (
SELECT 'armin@abc.com' AS email, 'Annapolis, MD' AS city
UNION ALL SELECT 'boyan@bca.com', 'Boulder, CD'
UNION ALL SELECT 'carrie@cab.com', 'Chicago, IL'
)
SELECT
CONCAT(
SUBSTR(email, 1, STRPOS(email,'@') -1), --username
' from ', city) AS callers
FROM example
■ 국제화
빅쿼리의 문자열은 유니코드이므로 영어와 관계된 가정은 피해야 한다. 예를 들어 일본어에는 '대문자'라는 것이 없으며, 문자열 타입을 바이트 배열 타입으로 변환할 때 기본적으로 적용되는 UTF-8 인코딩은 타밀어 같은 언어를 제대로 표현하지 못한다.
WITH
example AS(
SELECT
*
FROM
UNNEST( [ 'Seattle', 'New York', '東京' ] ) AS city )
SELECT
city,
UPPER(city) AS allcaps,
CAST(city AS BYTES ) AS bytes
FROM
example
내 컴퓨터에 타밀어는 없으므로 패스.
빅쿼리는 유니코드 문자 배열, 바이트 배열 및 유니코드 코드 포인트 배열(int 64)로 문자열을 나타내는 3가지 방법을 지원한다.
동일한 문자열에서 CHAR_LENGTH 및 BYTE_LENGTH 결과와 코드 포인트 수가 문자 수와 어떻게 다른지 확인하자.
WITH
example AS(
SELECT *
FROM
UNNEST( [ 'Seattle','New York', '東京' ] ) AS city )
SELECT
city,
CHAR_LENGTH(city) AS char_len,
to_code_points(city)[ORDINAL(1)] AS first_codept ,
ARRAY_LENGTH(to_code_points(city)) AS num_codept,
CAST (city AS bytes) AS bytes,
BYTE_LENGTH(city) AS byte_len
FROM
example
따라서 어떤 열에 다른 언어로 된 텍스트가 포함되어 있는지 파악한 후 언어의 차이를 고려해서 문자열 조작 함수를 사용해야 한다.
■ 출력 및 파싱
문자열을 파싱할 때는 간단히 INT64나 FLOAT64 타입으로 변환하면 되지만, 어떤 값을 원하는 형태의 문자열로 표현하라면 FORMAT 함수를 사용해야 한다.
SELECT
CAST(42 AS STRING),
CAST ('42' AS INT64),
FORMAT ('%03d',42),
FORMAT('%6.3f', 32.457842),
FORMAT('%5.3f', 32.4),
FORMAT('**%s**','H'),
FORMAT('%s-%03d','Agent',7)
FORMAT은 C의 printf 함수와 유사하게 작동하며 같은 형식 지정자를 사용한다. 좀 더 유용한 지정자 중 몇몇은 앞의 예에서 설명했다.
FORMAT 함수는 날짜나 타임스탬프 값도 지원하지만, 사용자의 로케일을 감안해서 날짜를 형식화할 수 있는 FORMAT_DATE와 FORMAT_TIMESTAMP 함수를 사용하는 것이 더 좋다.
■ 문자열 조작 함수
문자열 조작은 ETL 파이프라인에서 일반적으로 필요하므로, 다음 편의 함수를 잘 기억해 두면 유용한다.
불리언 변수는 True 또는 False 중 하나의 값만 갖는 변수다. SQL은 대소문자를 구분하지 않으므로 TRUE나 true나 모두 같은 의미를 갖는다.
■ 논리 연산
필터링에 대해 설명할 때 WHERE 절에 AND나 OR 또는 NOT 등을 포함한 불리언 표현식은 물론 실행 순서를 제어하기 위해 괄호를 사용할 수 있다는 점을 설명했다. 그떄 예제로 사용했던 쿼리는 다음과 같다.
SELECT
gender,
tripduration
FROM
`bigquery-public-data`.new_york_citibike.citibike_trips
WHERE
(tripduration < 600
AND gender = 'femaie')
OR gender = 'male'
다음과 같이 불리언 변수와 함께 비교 연산자를 사용해도 된다.
WITH
example AS (
SELECT
NULL AS is_vowel, NULL AS letter, -1 AS position
UNION ALL SELECT TRUE,'a',1
UNION ALL SELECT FALSE, 'b',2
UNION ALL SELECT FALSE,'c',3
)
SELECT * FROM example WHERE is_vowel != FALSE
그러나 다음 예제처럼 내장 상수와 비교할 때는 IS 연산자를 사용하는 것이 더 간단하다.
WITH
example AS (
SELECT
NULL AS is_vowel, NULL AS letter, -1 AS position
UNION ALL SELECT TRUE,'a',1
UNION ALL SELECT FALSE, 'b',2
UNION ALL SELECT FALSE,'c',3
)
SELECT * FROM example WHERE is_vowel IS NOT false
이때 두 쿼리의 실행 결과가 다른 것을 알 수 있다. 비교 연산자(=, !=, < 등)는 NULL과 비교하면 NULL을 리턴하지만 IS 연산자는 그렇지 않기 때문이다.
■ 조건식
불리언이 WHERE 절에서만 유용한 것은 아니다. SELECT에서 조건식을 사용하면 쿼리를 단순화할 수 있다. 예를 들어 카탈로그의 품목별로 희망하는 가격 인상율 및 세율을 적용해 최종 판매 가격을 계산해야 한다고 가정해 보자. 이때 카탈로그에서 인상율이나 세율이 누락되어 있으면 기본 인상율이나 세율을 적용하고자 한다. 이런 경우 IF 함수를 사용하면 된다.
WITH catalog AS(
SELECT 30.0 AS costPrice, 0.15 AS markup, 0.1 AS taxRate
UNION ALL SELECT NULL, 0.21, 0.15
UNION ALL SELECT 30.0, NULL, 0.09
UNION ALL SELECT NULL, NULL, 0.09
UNION ALL SELECT 30.0, NULL, NULL
)
SELECT *, ROUND(
costPrice *
IF(markup is NULL, 1.05, 1+markup) *
IF(taxRate IS NULL, 1.10, 1+taxRate), 2) AS salesPrice
FROM catalog
IF 함수의 첫 번째 파라미터는 평가할 조건이다. 조건이 참이면 두 번째 파라미터를 반환하고, 그렇지 않으면 세 번째 파라미터를 반환한다. 이 함수를 SELECT에서 호출했으므로 해당 작업은 각 행마다 실행된다.
■ Coalesce로 Null 값을 깨끗하게 처리하기
단일 값이 누락된 경우에는 보완이 가능하지만, 둘 이상의 값이 누락되어 값을 보완할 수 없다면 어떻게 할까? 다시 말해, 세율만 누락된 경우에는 10%의 세율을 적용하고, 품목의 가격 인상률까지 누락된 경우에는 그렇게 하지 않으려고 한다.
이런 경우 Coalesce 함수를 사용하면 Null이 아닌 값을 얻을 때까지 표현식을 계속 평가할 수 있어 편리하다.
WITH catalog AS(
SELECT 30.0 AS costPrice, 0.15 AS markup, 0.1 AS taxRate
UNION ALL SELECT NULL, 0.21, 0.15
UNION ALL SELECT 30.0, NULL, 0.09
UNION ALL SELECT NULL, NULL, 0.09
UNION ALL SELECT 30.0, NULL, NULL
)
SELECT *, ROUND(COALESCE(
costPrice * (1+markup) * (1+taxRate),
costPrice * 1.05 * (1+taxRate),
costPrice * (1+markup) * 1.10,
NULL
), 2) AS salesPrice
FROM catalog
Coalesce는 가능할 때마다 계산을 단락short-circuit 평가한다. 즉, Null이 아닌 결과를 얻은 후에는 식을 평가하지 않는다. 따라서 Coalesce 함수의 마지막 파라미터인 NULL은 필요하지 않지만 그 의도를 더 명확히 표현하기 위한 것이다.
빅쿼리는 입력값이 2개이면 Coalesce보다 간단하게 사용할 수 있는 IFNULL 함수도 지원한다. IFNULL(a,b)는 Coalesce(a,b)와 동일하며 a가 NULL이면 b를 반환한다. 즉 IFNULL(a,b)는 IF(a IS NULL, b, a)와 동일하다.
조건부 표현식에 대한 이 절의 첫 번째 쿼리는 다음처럼 단순하게 표현할 수 있다.
WITH catalog AS(
SELECT 30.0 AS costPrice, 0.15 AS markup, 0.1 AS taxRate
UNION ALL SELECT NULL, 0.21, 0.15
UNION ALL SELECT 30.0, NULL, 0.09
UNION ALL SELECT NULL, NULL, 0.09
UNION ALL SELECT 30.0, NULL, NULL
)
SELECT *, ROUND(
costPrice *
(1 + IFNULL(markup, 0.05)) *
(1 + IFNULL(taxRate, 0.10))
,2) AS salesPrice
FROM catalog
■ 타입 변환과 타입 강제
직원의 휴가 사유를 근무 시간 대신 기록하기 위해 근무 시간을 문자열로 저장하는 예제 데이터셋을 살펴보자.
WITH example AS(
SELECT 'John' AS employee, 'Paternity Leave' AS hours_worked
UNION ALL SELECT 'Janaki', '35'
UNION ALL SELECT 'Jian', 'Vacation'
UNION ALL SELECT 'Jose', '40'
)
SELECT SUM(hours_worked) FROM example
hours_worked 컬럼이 숫자 타입이 아닌 문자열이므로 쿼리는 작동하지 않는다.
올바른 값을 얻으려면 집계를 수행하기 전에 hours_worked 컬럼을 INT64로 명시적으로 변환해야 한다. 명시적 타입 변환을 캐스팅casting이라고 하며 CAST( )함수를 명시적으로 사용해야 한다. 캐스팅이 실패하면 빅쿼리는 오류를 반환한다. 오류 대신 NULL을 반환하려면 SAFE_CAST를 사용한다. 예를 들어 다음 쿼리는 오류를 발생한다.
SELECT CAST("true" AS bool), CAST("invalid" AS bool)
이제 SAFE_CAST를 사용해 보자.
SELECT CAST("true" AS bool), SAFE_CAST("invalid" AS bool)
임시적 변환을 타입 강제coercion라고 하며, 사용하는 데이터 타입과 필요한 데이터 타입이 다르면 자동으로 타입 강제가 이뤄진다. 예를 들어 FLOAT64가 필요한데 INT64를 사용하면 정수는 부동소수점 숫자로 강제로 변환된다. 빅쿼리가 수행하는 유일한 타입 강제는 INT64를 FLOAT64 및 NUMERIC으로, NUMERIC을 FLOAT64로 변환하는 것이다. 다른 모든 변환은 명시적으로 CAST를 사용해야 한다.
총 근무 시간을 구하는 문제에서 hours_worked 문자열 컬럼의 값 중에는 'Vacation'처럼 정수로 변환할 수 없는 값도 있으므로 SAFE_CAST를 사용해야 한다.
WITH example AS(
SELECT 'John' AS employee, 'Paternity Leave' AS hours_worked
UNION ALL SELECT 'Janaki', '35'
UNION ALL SELECT 'Jian', 'Vacation'
UNION ALL SELECT 'Jose', '40'
)
SELECT SUM(SAFE_CAST(hours_worked AS INT64)) FROM example
만약 단순히 스키마 문제로 모든 행의 데이터가 숫자를 표현하는 문자열로 저장된 경우라면, 즉 숫자가 아닌 문자열이나 NULL 값 등이 없다면 간단하게 CAST를 사용할 수 있다.
WITH example AS(
SELECT 'John' AS employee, '0' AS hours_worked
UNION ALL SELECT 'Janaki', '35'
UNION ALL SELECT 'Jian', '0'
UNION ALL SELECT 'Jose', '40'
)
SELECT SUM(CAST(hours_worked AS INT64)) FROM example
■ 불리언 변환을 피하기 위해 COUNTIF 사용하기
WITH example AS(
SELECT true AS is_vowel, 'a' AS letter, 1 AS position
UNION ALL SELECT false, 'b',2
UNION ALL SELECT false, 'c',3
)
SELECT * FROM example
SUM, AVG 등은 불리언 값과는 작동하지 않는다. 따라서 다음과 같이 집계를 수행하기 전에 불리언 값을 INT64로 변환해야 한다.
WITH example AS(
SELECT true AS is_vowel, 'a' AS letter, 1 AS position
UNION ALL SELECT false, 'b',2
UNION ALL SELECT false, 'c',3
)
SELECT SUM(CAST(is_vowel AS INT64)) AS num_vowels FROM example
하지만 가능하면 타입 변환은 피하는 것이 좋다. 이 예제에서는 불리언 값에 IF 문을 사용하는 것이 더 깔끔하다.
WITH example AS(
SELECT true AS is_vowel, 'a' AS letter, 1 AS position
UNION ALL SELECT false, 'b',2
UNION ALL SELECT false, 'c',3
)
SELECT SUM(IF (is_vowel,1,0)) AS num_vowels FROM example
그리고 이보다 더 나은 방법은 COUNTIF를 사용하는 것이다.
WITH example AS(
SELECT true AS is_vowel, 'a' AS letter, 1 AS position
UNION ALL SELECT false, 'b',2
UNION ALL SELECT false, 'c',3
)
SELECT COUNTIF(is_vowel) AS num_vowels FROM example