6강: 데이터 정리 → NULL 처리, 중복 제거, 날짜/문자열 변환
7강: 고급 SQL 변환 → JSON 파싱, 배열 다루기, JOIN, PIVOT, 고차 함수
8강: SQL UDF → 내 함수 만들어서 등록하고 재사용하기
지저분한 데이터를 정리하고, 복잡한 구조를 다루고, 재사용 가능한 함수를 만드는 방법 정리
count(컬럼)은 NULL을 건너뜀. count(*)은 NULL이 있는 행도 포함.
-- 각 컬럼에 NULL이 몇 개인지 확인
SELECT
count_if(user_id IS NULL) AS missing_user_ids,
count_if(email IS NULL) AS missing_emails,
count_if(updated IS NULL) AS missing_updates
FROM users_dirty
-- 총 행 수 vs NULL 제외 행 수 비교
SELECT count(*), count(user_id) FROM users_dirty
-- ↑ NULL 포함 ↑ NULL 제외
-- 전체 행 수
SELECT count(*) FROM users_dirty
-- 고유 행 수
SELECT count(DISTINCT(*)) FROM users_dirty
-- 특정 컬럼 기준 고유 값 수
SELECT count(DISTINCT user_id) FROM users_dirty
주의: DISTINCT는 NULL이 있는 행을 제외하지 않는다. 모든 값이 NULL인 행도 "하나의 고유한 행"으로 처리된다.
-- user_id + user_first_touch_timestamp 기준으로 중복 제거
-- email은 여러 행 중 NULL이 아닌 값을 가져오기 위해 max() 사용
CREATE OR REPLACE TEMP VIEW deduped_users AS
SELECT user_id, user_first_touch_timestamp,
max(email) AS email,
max(updated) AS updated
FROM users_dirty
WHERE user_id IS NOT NULL -- NULL 행 제거
GROUP BY user_id, user_first_touch_timestamp;
왜 max(email)을 쓰냐면:
같은 user_id가 여러 행이면 GROUP BY 후 email이 여러 개 존재. 그 중 NULL이 아닌 값을 가져오기 위해 max() 사용.
-- user_id가 중복 없는지 확인 (true면 중복 없음)
SELECT max(row_count) <= 1 AS no_duplicate_ids FROM (
SELECT user_id, count(*) AS row_count
FROM deduped_users
GROUP BY user_id
)
SELECT *,
date_format(first_touch, "MMM d, yyyy") AS first_touch_date, -- 사람이 읽기 좋은 날짜
date_format(first_touch, "HH:mm:ss") AS first_touch_time, -- 시간
regexp_extract(email, "(?<=@).+", 0) AS email_domain -- 이메일에서 도메인 추출
FROM (
SELECT *,
CAST(user_first_touch_timestamp / 1e6 AS timestamp) AS first_touch -- Unix 타임스탬프 변환
FROM deduped_users
)
Kafka 같은 시스템에서 오는 데이터는 JSON이 바이너리로 인코딩된 경우가 많다.
-- 바이너리를 문자열로 변환해서 읽기
CREATE OR REPLACE TEMP VIEW events_strings AS
SELECT string(key), string(value)
FROM events_raw;
JSON 안의 중첩 데이터 접근하기:
-- : 구문으로 JSON 필드 접근
SELECT value:device, value:geo:city
FROM events_strings
-- ↑ 최상위 필드 ↑ 중첩 필드 (geo 안의 city)
JSON 문자열을 구조체로 파싱하기:
-- schema_of_json으로 스키마 자동 추론 → from_json으로 파싱
CREATE OR REPLACE TEMP VIEW parsed_events AS
SELECT from_json(value, schema_of_json('{...샘플JSON...}')) AS json
FROM events_strings;
-- 구조체를 개별 컬럼으로 평면화
CREATE OR REPLACE TEMP VIEW new_events_final AS
SELECT json.* -- ← * 로 펼치기
FROM parsed_events;
구조체(struct) 타입은 . 으로 접근:
-- ecommerce 구조체 안의 purchase_revenue_in_usd 접근
SELECT ecommerce.purchase_revenue_in_usd
FROM events
WHERE ecommerce.purchase_revenue_in_usd IS NOT NULL
-- 배열 크기 필터링 (items가 2개 초과인 행만)
SELECT user_id, items
FROM events
WHERE size(items) > 2
explode — 배열을 행으로 펼치기:
-- items 배열의 각 요소를 별도 행으로 분리
SELECT user_id, explode(items) AS item
FROM events
Before: user_1 | [item_A, item_B, item_C]
After: user_1 | item_A
user_1 | item_B
user_1 | item_C
collect_set, flatten, array_distinct — 배열 수집 및 정리:
Q. 이 세 함수가 뭔지 코드만 보고는 이해가 안 됐다.
단계별로 뭘 하는지 데이터로 보면 이해가 된다.
예시 데이터:
user_id | event_name | items
user_1 | click | [A, B]
user_1 | purchase | [B, C]
user_1 | click | [A, D]
① collect_set(event_name)
→ 그룹 안의 값들을 배열로 모으기 (중복 제거)
→ user_1: ["click", "purchase"] ← 중복 "click" 하나로 합쳐짐
② collect_set(items.item_id)
→ 배열 안의 값을 모으면 배열들의 배열이 됨
→ user_1: [ [A,B], [B,C], [A,D] ]
③ flatten([ [A,B], [B,C], [A,D] ])
→ 배열들의 배열을 하나의 배열로 펼치기
→ [A, B, B, C, A, D] ← 하나로 합쳐졌지만 중복 있음
④ array_distinct([A, B, B, C, A, D])
→ 배열에서 중복 제거
→ [A, B, C, D]
세 개를 합치면:
SELECT user_id,
collect_set(event_name) AS event_history,
array_distinct(flatten(collect_set(items.item_id))) AS cart_history
-- → 이 유저가 장바구니에 담았던 모든 상품 ID (중복 없이)
FROM events
GROUP BY user_id
Q. JOIN은 두 테이블의 컬럼을 합치는 거야?
거의 맞다. 정확히는 "조건에 맞는 행끼리 연결해서 컬럼을 합치는 것".
테이블 A (sales) 테이블 B (item_lookup)
order_id | item_id item_id | item_name
1 | M_STAN_K M_STAN_K | Standard King Mattress
2 | P_FOAM_S P_FOAM_S | Small Foam Pillow
JOIN ON a.item_id = b.item_id ← item_id가 같은 행끼리 연결
결과:
order_id | item_id | item_name
1 | M_STAN_K | Standard King Mattress
2 | P_FOAM_S | Small Foam Pillow
Q. ON에 사용한 컬럼명이 다르면 합쳐지지 않고 둘 다 컬럼으로 적히는 거 맞지?
맞다.
컬럼명 같음 (item_id = item_id) → 하나로 합쳐져서 컬럼 1개
컬럼명 다름 (order_item_id = product_id) → 둘 다 컬럼으로 남아서 컬럼 2개
이름이 달라도 값은 같기 때문에, 컬럼명이 다를 때 SELECT * 하면 거의 똑같은 값의 컬럼 두 개가 나란히 생겨서 지저분해 보인다.
-- explode 후 조회 테이블과 JOIN
CREATE OR REPLACE VIEW sales_enriched AS
SELECT *
FROM (
SELECT *, explode(items) AS item
FROM sales) a
INNER JOIN item_lookup b
ON a.item.item_id = b.item_id;
JOIN 종류:
INNER JOIN → 양쪽 다 있는 행만 (가장 많이 씀)
LEFT JOIN → 왼쪽 테이블 기준, 오른쪽에 없으면 NULL
RIGHT JOIN → 오른쪽 테이블 기준, 왼쪽에 없으면 NULL
A: [1, 2, 3] B: [2, 3, 4]
INNER → [2, 3] 공통인 것만
LEFT → [1, 2, 3] A 기준
RIGHT → [2, 3, 4] B 기준
-- UNION: 두 쿼리 결과 합치기
SELECT * FROM events
UNION
SELECT * FROM new_events_final
-- INTERSECT: 두 쿼리에 공통으로 있는 행만
SELECT * FROM events
INTERSECT
SELECT * FROM new_events_final
-- MINUS: 첫 번째에는 있고 두 번째에는 없는 행
SELECT * FROM events
MINUS
SELECT * FROM new_events_final
Q. PIVOT 사용법을 몰라서 문제를 못 풀었다.
PIVOT이 하는 일:
Before (행으로): After (열로):
user_id | event_name user_id | click | purchase | view
user_1 | click → user_1 | 3 | 1 | 2
user_1 | click
user_1 | purchase
user_1 | view
user_1 | view
"event_name 값들을 각각 컬럼으로 만들고, 그 안에 집계값을 넣어라"
PIVOT 문법 구조:
SELECT * FROM (
SELECT 필요한_컬럼들 -- ① 재료 준비 (PIVOT에 필요한 컬럼만)
FROM 테이블
) PIVOT (
집계함수(집계할_컬럼) -- ② 각 칸에 뭘 넣을지 (count, sum 등)
FOR 피벗할_컬럼 IN ( -- ③ 어떤 컬럼을 기준으로 펼칠지
'값1', '값2', '값3' -- ④ 열로 만들 값들 (미리 알고 있어야 함)
)
)
IN 안의 값을 어떻게 아냐면:
-- 먼저 이걸 실행해서 어떤 값들이 있는지 확인
SELECT DISTINCT event_name FROM events
문제 풀이 — "user_id로 그룹화하고 event_name을 피벗하여 각 이벤트 횟수를 열로":
"각 이벤트 유형의 개수" → 집계함수 = count(event_name)
"event_name을 피벗" → FOR event_name IN (...)
"user_id로 그룹화" → 재료에 user_id만 남기면 자동으로 그룹화
SELECT * FROM (
SELECT user_id, event_name -- user_id랑 event_name만 남김
FROM events
) PIVOT (
count(event_name) -- 각 칸에 이벤트 개수
FOR event_name IN (
'cart', 'login', 'main',
'finalize', 'payment_review',
-- SELECT DISTINCT event_name FROM events 로 확인한 값들
'logout', 'home', 'warehouse'
)
)
일반 함수는 배열에 못 씀. 고차 함수를 쓰면 배열 안을 직접 조작 가능.
FILTER — 조건에 맞는 요소만 남기기:
SELECT
order_id,
FILTER(items, i -> i.item_id LIKE "%K") AS king_items
-- ↑배열 ↑반복변수 ↑ 조건 (K로 끝나는 것만 남김)
FROM sales
EXISTS — 배열 안에 조건을 만족하는 요소가 있는지 확인 (true/false 반환):
실습에서 직접 풀었던 문제: items 배열에서 item_name이 "Mattress"로 끝나면 true, "Pillow"로 끝나면 true인 부울 열 생성
CREATE OR REPLACE TABLE sales_product_flags AS
SELECT
items,
EXISTS(items, x -> x.item_name LIKE "%Mattress") AS mattress,
EXISTS(items, x -> x.item_name LIKE "%Pillow") AS pillow
FROM sales;
결과:
items | mattress | pillow
[{item_id: "M_PREM_K", item_name: "Premium King Mattress", ...}] | true | false
[{item_id: "P_FOAM_S", item_name: "Standard Foam Pillow", ...}] | false | true
[{item_id: "M_STAN_F", item_name: "Standard Full Mattress",...}] | true | false
FILTER vs EXISTS 차이:
FILTER → 조건에 맞는 요소만 남긴 배열 반환
[item_A, item_B, item_C] → [item_B]
EXISTS → 조건을 만족하는 요소가 하나라도 있으면 true, 없으면 false 반환
[item_A, item_B, item_C] → true / false
TRANSFORM — 배열의 각 요소 변환:
SELECT
order_id,
TRANSFORM(king_items, k -> CAST(k.item_revenue_in_usd * 100 AS INT)) AS item_revenues
-- ↑배열 ↑반복변수 ↑ 각 요소에 적용할 변환
FROM king_size_sales
자주 쓰는 SQL 로직을 함수로 등록해서 이름만으로 재사용하는 것.
엑셀의 사용자 정의 함수랑 같은 개념
내가 자주 쓰는 수식을 함수로 저장 → 필요할 때 함수 이름만 호출
CREATE OR REPLACE FUNCTION yelling(text STRING)
RETURNS STRING -- ← "STRING 타입을 반환할 거야" 라는 선언 (약속)
RETURN concat(upper(text), "!!!") -- ← 실제로 반환하는 코드 (실행)
Q. RETURNS랑 RETURN이 둘 다 있던데 무슨 차이야?
RETURNS = 메뉴판에 "아메리카노 - 커피입니다" 라고 적어둔 것 (반환 타입 선언)
RETURN = 실제로 커피를 내어주는 것 (실제 반환 코드)
RETURNS는 어떤 타입을 반환할지 미리 선언, RETURN은 실제로 반환하는 코드.
-- 사용하기
SELECT yelling(food) FROM foods
-- 결과: BEEF!!!, BEANS!!!, POTATOES!!!
DESCRIBE FUNCTION yelling -- 기본 정보
DESCRIBE FUNCTION EXTENDED yelling -- Body 필드에서 실제 SQL 로직 확인 가능
SELECT *,
CASE
WHEN food = "beans" THEN "I love beans"
WHEN food = "potatoes" THEN "My favorite vegetable is potatoes"
WHEN food <> "beef" THEN concat("Do you have any good recipes for ", food, "?")
ELSE concat("I don't eat ", food)
END
FROM foods
CREATE FUNCTION foods_i_like(food STRING)
RETURNS STRING
RETURN CASE
WHEN food = "beans" THEN "I love beans"
WHEN food = "potatoes" THEN "My favorite vegetable is potatoes"
WHEN food <> "beef" THEN concat("Do you have any good recipes for ", food, "?")
ELSE concat("I don't eat ", food)
END;
SELECT foods_i_like(food) FROM foods
6강 데이터 정리
→ NULL 파악 (count_if) → 중복 제거 (GROUP BY + max) → 검증 → 날짜/문자열 변환
7강 고급 SQL 변환
→ JSON 파싱 (from_json, schema_of_json)
→ 배열 조작 (explode, collect_set, flatten, array_distinct)
→ 고차 함수 (FILTER, EXISTS, TRANSFORM)
→ 테이블 결합 (JOIN, UNION, INTERSECT)
→ 형태 변경 (PIVOT)
8강 SQL UDF
→ 반복 로직을 함수로 등록 (CREATE FUNCTION)
→ RETURNS: 반환 타입 선언 / RETURN: 실제 반환 코드
→ CASE/WHEN과 결합해서 제어 흐름 함수 만들기
→ 메타스토어에 저장 → 어디서든 재사용