재료 손질(ROWS) → 그릇에 담기(GROUPS) → 플레이팅(RESULT)
FROM/JOIN/WHERE GROUP BY/HAVING SELECT/ORDER BY/LIMIT
이 흐름 안에서 성능 문제 5가지가 터집니다:
| 단계 | 터지는 문제 | 한 줄 요약 |
|---|---|---|
| JOIN | 조인 폭발 | 출력 행 > 입력 행 |
| 메모리 | 스필링 | 메모리 넘쳐 디스크로 샘 |
| ORDER BY | 정렬 비용 | LIMIT 붙이면 약해짐 |
| 서브쿼리 | ORDER BY 위치 | 안쪽 정렬은 헛수고 |
| GROUP BY | 카디널리티 | 고유값 많으면 느림 |
SELECT는 맨 먼저 쓰지만, 거의 맨 나중에 실행된다.
FROM → WHERE → GROUP BY → HAVING → QUALIFY → DISTINCT → ORDER BY → LIMIT
암기 문장 (영어 첫 글자):
Friendly Whales Gather, Hungry Quietly... Dive, Order Lunch
(FROM-WHERE-GROUP-HAVING-QUALIFY-DISTINCT-ORDER-LIMIT)
┌─────────────┐ ┌──────────────┐ ┌──────────────────────┐
│ ① ROWS │ ──▶ │ ② GROUPS │ ──▶ │ ③ RESULT │
│ │ │ │ │ │
│ FROM │ │ GROUP BY │ │ SELECT │
│ JOIN │ │ HAVING │ │ DISTINCT │
│ WHERE │ │ │ │ ORDER BY │
│ │ │ │ │ LIMIT │
└─────────────┘ └──────────────┘ └──────────────────────┘
행 처리 그룹 처리 결과 만들기
| 언제 거르나 | 비유 | |
|---|---|---|
| WHERE | 집계 전, 개별 행 | 재료 다듬을 때 상한 것 버림 |
| HAVING | 집계 후, 그룹 | 다 만든 요리 중 별로면 버림 |
⭐ 시험 포인트: 왼쪽(ROWS)에서 데이터를 많이 줄일수록 성능 이득이 큼
📎 QUALIFY는 윈도우 함수 결과를 필터링한다. (GROUP BY + 집계함수에 HAVING이 하는 일을, 윈도우 함수에 대해 하는 것) → 윈도우 함수 계산 후 평가됨.
조인 키에 매칭되는 행이 여러 개면, 출력 행이 입력보다 폭발적으로 늘어난다.
SELECT *
FROM t1
JOIN t2 ON t1.a = t2.a
JOIN t3 ON t1.b = t3.b
JOIN t4 ON t1.c = t4.c;
이런 다중 조인에서 일부 조인이 폭발할 수 있습니다.
GET_QUERY_OPERATOR_STATS각 조인의 출력 행 ÷ 입력 행 =
ROW_MULTIPLE을 계산
이 값이 1보다 크면 = 폭발하는 조인!
SELECT operator_id,
operator_attributes,
operator_statistics:output_rows / operator_statistics:input_rows AS row_multiple
FROM TABLE(GET_QUERY_OPERATOR_STATS($lid))
WHERE operator_type = 'Join'
ORDER BY step_id, operator_id;
결과 해석 예시:
| OPERATOR_ID | 조인 조건 | ROW_MULTIPLE |
|---|---|---|
| 1 | T4.C = T1.C (INNER) | 49.97 🔴 |
| 3 | T3.B = T1.B (INNER) | 116.07 🔴 |
| 5 | T2.A = T1.A (INNER) | 12.21 🔴 |
세 조인 모두 출력이 입력의 수십~백 배 → 전부 폭발 중!
Join 연산자의 출력 레코드 수가 비정상적으로 많고, 시간을 많이 소비하면 폭발 신호.
CartesianJoin이라고 라벨링되기도 함.
① 조인 조건을 정확히 명시하라 (누락 = Cartesian product)
② ROW_MULTIPLE이 큰 조인의 조인 조건을 검토하라
③ 조인 키의 고유값을 높이거나, 양쪽 데이터를 줄여라
메모리가 부족하면 디스크로 데이터가 샌다. 멀리 샐수록 느려진다.
🧠 메모리 → 💾 로컬 디스크 → ☁️ 원격 스토리지
(빠름, 식탁 위) (느림, 옆 테이블) (제일 느림, 옆집 빌림)
연산이 메모리에 안 맞으면 먼저 로컬 디스크로, 로컬도 부족하면 그 다음 원격 스토리지로 스필링한다.
| 지표 | 필드명 | 뜻 | 심각도 |
|---|---|---|---|
| 로컬 스필 | bytes_spilled_local_storage | 로컬 디스크로 샌 양 | ⚠️ 주의 |
| 원격 스필 | bytes_spilled_remote_storage | 원격 디스크로 샌 양 | 🔴 심각 |
⭐ 시험 단골 문제:
"쿼리가 메모리에 비해 너무 크다는 증상은?" → 정답: 원격 스토리지로 스필링한다
SELECT query_id, SUBSTR(query_text, 1, 50) partial_query_text,
user_name, warehouse_name,
bytes_spilled_to_local_storage,
bytes_spilled_to_remote_storage
FROM snowflake.account_usage.query_history
WHERE (bytes_spilled_to_local_storage > 0
OR bytes_spilled_to_remote_storage > 0)
AND start_time::date > dateadd('days', -45, current_date)
ORDER BY bytes_spilled_to_remote_storage,
bytes_spilled_to_local_storage DESC
LIMIT 10;
① 더 큰 웨어하우스 사용 (메모리/로컬 공간 ↑)
② 데이터를 더 작은 배치로 처리 (처리량 ↓)
ORDER BY 혼자는 전체를 정렬해 비싸다. LIMIT을 붙이면 Top-K Pruning이 작동해 빨라진다.
평소엔 모든 행을 스캔(어떤 행이든 상위 K개에 들 수 있으니까).
Top-K pruning은 "남은 행은 어차피 상위 K개에 못 든다" 고 판단되면 스캔을 멈춤 → 파티션 스킵.
큰 테이블일수록 효과가 큼.
✅ ORDER BY + LIMIT 둘 다 있어야
✅ ORDER BY 첫 컬럼이 지원 타입
(정수형: INTEGER/DATE/TIMESTAMP, 또는 문자/바이너리, 또는 캐스팅된 VARIANT 필드)
✅ DESC + nullable 컬럼이면 → NULLS LAST 명시해야
✅ 조인이 있으면 → ORDER BY 컬럼은 "큰 테이블(fact)" 의 컬럼이어야
⚠️ 캐스팅 표현식(예: 정수 반환 cast)은 첫 컬럼으로 지원 안 됨.
⚠️ 여러 컬럼을 ORDER BY 해도 첫 번째 컬럼만 고려됨.
-- ✅ 가능 (내부 타입으로 캐스팅됨)
SELECT * FROM variant_topk_test ORDER BY var_col:i::NUMBER LIMIT 5;
-- ❌ 불가능 (타입 캐스팅이 없음)
SELECT * FROM variant_topk_test ORDER BY var_col:s LIMIT 5;
-- ❌ 불가능 (내부 타입과 다른 타입으로 캐스팅: i는 숫자인데 VARCHAR로)
SELECT * FROM variant_topk_test ORDER BY var_col:i::VARCHAR LIMIT 5;
집계 쿼리는 ① GROUP BY가 있고, ② ORDER BY 첫 컬럼이 GROUP BY 컬럼(집계 컬럼 X) 일 때만 pruning.
-- ✅ 가능 (첫 ORDER BY 컬럼 c2가 GROUP BY 컬럼이고 집계 컬럼 아님)
SELECT c1, c2, c3, COUNT(*) AS agg_col
FROM mytable
GROUP BY c1, c2, c3
ORDER BY c2, c1, agg_col, c3
LIMIT 5;
-- ❌ 불가능 (첫 ORDER BY 컬럼 agg_col이 집계 컬럼)
SELECT c1, c2, c3, COUNT(*) AS agg_col
FROM mytable
GROUP BY c1, c2, c3
ORDER BY agg_col, c2, c1
LIMIT 5;
💡 외우기: "첫 컬럼이 집계가 아닌 그룹 컬럼이어야 빨라진다"
서브쿼리 안의 ORDER BY는 바깥 쿼리 순서를 보장하지 않는다 → 불필요한 비용.
SELECT * FROM (
SELECT * FROM (
SELECT * FROM my_table
ORDER BY col1 -- 정렬: 가장 안쪽
)
LIMIT 6 -- LIMIT: 중간
)
LIMIT 100; -- LIMIT: 가장 바깥
⚠️ ORDER BY와 LIMIT이 서로 다른 레벨에 있으면 결과 행 수가 예측 불가능.
(6행을 기대해도 더 많거나 적게 나올 수 있음)
ORDER BY와 LIMIT(또는 FETCH)을 같은 쿼리 레벨에 두면 예측 가능.
최종 정렬이 필요하면 최상위 레벨에서 ORDER BY 한 번만.
그룹 키의 고유값(cardinality)이 많을수록 메모리를 많이 먹는다.
💡 스필링과 연결: 고유값(그룹)이 많으면 집계가 메모리에 안 들어가 스필링으로 이어질 수 있음.
행 단위(row) → 그룹 단위(group) → 분석 함수(analytic) → 결과 생성(result)
왼쪽(앞 단계)을 튜닝할수록 효과가 큼. → 1️⃣의 ROWS→GROUPS→RESULT 흐름과 동일!
1. 실행순서 = FROM·WHERE·GROUP·HAVING·QUALIFY·DISTINCT·ORDER·LIMIT
(WHERE = 집계 전 / HAVING = 집계 후)
2. 조인폭발 = 출력행 > 입력행 (ROW_MULTIPLE > 1)
→ GET_QUERY_OPERATOR_STATS로 확인
→ 조인 조건 누락 = Cartesian product
3. 스필링 = 메모리 → 로컬 → 원격
→ "원격 스필" = 메모리 부족 증상 (시험 정답!)
→ 필드: bytes_spilled_local/remote_storage
→ 해결: 큰 웨어하우스 or 작은 배치
4. ORDER BY + LIMIT = Top-K Pruning (빨라짐)
→ 첫 컬럼 지원타입 / DESC면 NULLS LAST
→ 집계 쿼리: 첫 ORDER BY 컬럼이 GROUP BY 컬럼이어야
5. 서브쿼리 ORDER BY = 헛수고 (바깥 순서 보장 X)
→ ORDER BY와 LIMIT은 같은 레벨에!
6. GROUP BY = High Cardinality면 메모리 부담 ↑ → 스필 위험