[Snowflake] 쿼리 실행 순서 & Query Profile 성능 튜닝

차지예·2026년 6월 9일

Snowflake

목록 보기
34/49
post-thumbnail

쿼리 한 줄을 "요리 과정" 으로 비유

재료 손질(ROWS) → 그릇에 담기(GROUPS) → 플레이팅(RESULT)
   FROM/JOIN/WHERE     GROUP BY/HAVING      SELECT/ORDER BY/LIMIT

이 흐름 안에서 성능 문제 5가지가 터집니다:

단계터지는 문제한 줄 요약
JOIN조인 폭발출력 행 > 입력 행
메모리스필링메모리 넘쳐 디스크로 샘
ORDER BY정렬 비용LIMIT 붙이면 약해짐
서브쿼리ORDER BY 위치안쪽 정렬은 헛수고
GROUP BY카디널리티고유값 많으면 느림

1️⃣ 실행 순서 — "쓰는 순서 ≠ 도는 순서"

🎯 핵심 한 줄

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)

📌 3단계로 묶어서 기억

┌─────────────┐     ┌──────────────┐     ┌──────────────────────┐
│   ① ROWS    │ ──▶ │  ② GROUPS    │ ──▶ │     ③ RESULT          │
│             │     │              │     │                       │
│  FROM       │     │  GROUP BY    │     │  SELECT               │
│  JOIN       │     │  HAVING      │     │  DISTINCT             │
│  WHERE      │     │              │     │  ORDER BY             │
│             │     │              │     │  LIMIT                │
└─────────────┘     └──────────────┘     └──────────────────────┘
   행 처리             그룹 처리              결과 만들기

💡 꼭 구분! WHERE vs HAVING

언제 거르나비유
WHERE집계 , 개별 행재료 다듬을 때 상한 것 버림
HAVING집계 , 그룹다 만든 요리 중 별로면 버림

시험 포인트: 왼쪽(ROWS)에서 데이터를 많이 줄일수록 성능 이득이 큼

📎 QUALIFY는 윈도우 함수 결과를 필터링한다. (GROUP BY + 집계함수에 HAVING이 하는 일을, 윈도우 함수에 대해 하는 것) → 윈도우 함수 계산 후 평가됨.


2️⃣ 조인 폭발 (Join Explosion) — "출력 행 > 입력 행"

🎯 핵심 한 줄

조인 키에 매칭되는 행이 여러 개면, 출력 행이 입력보다 폭발적으로 늘어난다.

📌 왜 터지나?

  • 한 테이블의 행이 다른 테이블의 여러 행과 매칭되면 출력이 수백만 행을 넘는 "폭발" 조인이 됨
  • 조인 조건을 누락하면 의도치 않은 Cartesian product(곱집합) 가 되어 폭발
  • 조인 키의 고유값(cardinality)이 낮으면, 원본 테이블이 작아도 폭발 위험 ↑

📌 폭발이 잘 나는 다중 조인

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
1T4.C = T1.C (INNER)49.97 🔴
3T3.B = T1.B (INNER)116.07 🔴
5T2.A = T1.A (INNER)12.21 🔴

세 조인 모두 출력이 입력의 수십~백 배 → 전부 폭발 중!

📌 Query Profile에서도 보임

Join 연산자의 출력 레코드 수가 비정상적으로 많고, 시간을 많이 소비하면 폭발 신호.
CartesianJoin 이라고 라벨링되기도 함.

📌 해결 / 예방

① 조인 조건을 정확히 명시하라 (누락 = Cartesian product)
② ROW_MULTIPLE이 큰 조인의 조인 조건을 검토하라
③ 조인 키의 고유값을 높이거나, 양쪽 데이터를 줄여라

3️⃣ 스필링 (Spilling to Disk) — "넘치면 옆 테이블로"

🎯 핵심 한 줄

메모리가 부족하면 디스크로 데이터가 샌다. 멀리 샐수록 느려진다.

📌 새는 순서 (식당 비유)

🧠 메모리        →   💾 로컬 디스크    →   ☁️ 원격 스토리지
(빠름, 식탁 위)     (느림, 옆 테이블)      (제일 느림, 옆집 빌림)

연산이 메모리에 안 맞으면 먼저 로컬 디스크로, 로컬도 부족하면 그 다음 원격 스토리지로 스필링한다.

📌 2가지 지표

지표필드명심각도
로컬 스필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;

📌 해결법

① 더 큰 웨어하우스 사용 (메모리/로컬 공간 ↑)
② 데이터를 더 작은 배치로 처리 (처리량 ↓)

4️⃣ ORDER BY + LIMIT — "Top N만 필요하면 LIMIT 붙여라"

🎯 핵심 한 줄

ORDER BY 혼자는 전체를 정렬해 비싸다. LIMIT을 붙이면 Top-K Pruning이 작동해 빨라진다.

📌 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 해도 첫 번째 컬럼만 고려됨.

📌 VARIANT 컬럼 — 되는 케이스 / 안 되는 케이스

-- ✅ 가능 (내부 타입으로 캐스팅됨)
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

집계 쿼리는 ① 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;

💡 외우기: "첫 컬럼이 집계가 아닌 그룹 컬럼이어야 빨라진다"


5️⃣ ORDER BY 위치 — "안쪽 정렬은 헛수고"

🎯 핵심 한 줄

서브쿼리 안의 ORDER BY는 바깥 쿼리 순서를 보장하지 않는다 → 불필요한 비용.

📌 핵심

  • 서브쿼리/OVER() 안의 ORDER BY는 그 컨텍스트 안에서만 적용됨
  • 바깥(outer) 쿼리 레벨의 순서는 보장 X

📌 다른 레벨의 ORDER BY / LIMIT (예측 불가 예시)

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 한 번만.


6️⃣ GROUP BY — "고유값 많으면 느려진다"

🎯 핵심 한 줄

그룹 키의 고유값(cardinality)이 많을수록 메모리를 많이 먹는다.

📌 카디널리티와 메모리

  • GROUP BY 컬럼의 distinct 값이 적으면(Low cardinality) → 메모리 부담 적음
  • GROUP BY 컬럼의 distinct 값이 많으면(High cardinality) → 메모리 부담 큼

💡 스필링과 연결: 고유값(그룹)이 많으면 집계가 메모리에 안 들어가 스필링으로 이어질 수 있음.

📌 튜닝 우선순위

행 단위(row) → 그룹 단위(group) → 분석 함수(analytic) → 결과 생성(result)

왼쪽(앞 단계)을 튜닝할수록 효과가 큼. → 1️⃣의 ROWS→GROUPS→RESULT 흐름과 동일!


✅ 시험 직전 30초 암기 카드

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면 메모리 부담 ↑ → 스필 위험

0개의 댓글