백엔드 데이터 인프라 40편 — EXPLAIN 쿼리 계획 읽기

2026-05-17백엔드 데이터 인프라

백엔드 데이터 인프라 40편. EXPLAIN 으로 쿼리 계획 읽기 — Seq Scan·Index Scan·Hash Join·Sort 노드 의미와 비용 해석 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 40편 — EXPLAIN 쿼리 계획 읽기

이 글은 백엔드 데이터 인프라 시리즈 70편 중 40편이에요. "이 쿼리 왜 느린가" 의 답이 EXPLAIN. PG 운영 깊이의 핵심 도구.

EXPLAIN 기본

EXPLAIN SELECT * FROM users WHERE email = 'alice@example.com';

결과:

                          QUERY PLAN
─────────────────────────────────────────────────────────
 Index Scan using idx_users_email on users
   (cost=0.42..8.44 rows=1 width=128)
   Index Cond: (email = 'alice@example.com'::text)

읽기: - Index Scan = 사용 방법 - cost=0.42..8.44 = 추정 비용 (시작 비용..총 비용) - rows=1 = 추정 결과 행 수 - width=128 = 행 평균 크기

EXPLAIN ANALYZE — 실제 실행

EXPLAIN ANALYZE SELECT ...;

ANALYZE = 실제 실행 + 실측 추가.

 Index Scan using idx_users_email on users
   (cost=0.42..8.44 rows=1 width=128)
   (actual time=0.025..0.026 rows=1 loops=1)
   Index Cond: (email = 'alice@example.com')
 Planning Time: 0.150 ms
 Execution Time: 0.045 ms

actual time = 실측. cost 와 비교 — 큰 차이 = 통계 부정확.

옵션 추가

EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT ...;
옵션 의미
ANALYZE 실제 실행 + 실측
BUFFERS 디스크·메모리 읽기 통계
VERBOSE 상세 출력
COSTS (기본) 비용 표시
TIMING (기본 ANALYZE 시) 시간 측정
SETTINGS 영향 설정
FORMAT TEXT/JSON/YAML 출력 형식
WAL WAL 생성 통계

운영 진단 표준 = EXPLAIN (ANALYZE, BUFFERS) ....

스캔 노드 4가지

Seq Scan          → 풀스캔 (인덱스 미사용)
Index Scan        → 인덱스 + 테이블 접근
Index Only Scan   → 인덱스만으로 (Heap Fetches: 0 일 때 최고)
Bitmap Index Scan → 여러 인덱스·범위

Seq Scan — 풀스캔

Seq Scan on users (cost=0.00..18334.00 rows=100 width=...)
  Filter: (email = '...')
  Rows Removed by Filter: 999900

Rows Removed 가 크면 — 인덱스 박을 곳.

Index Scan

Index Scan using idx_users_email on users
  Index Cond: (email = '...')

Index Cond = 인덱스에서 찾는 조건. Filter 가 추가로 있으면 = 인덱스 + 추가 필터.

Index Only Scan — 최고

Index Only Scan using idx_users_email on users
  Index Cond: (email = '...')
  Heap Fetches: 0

Heap Fetches: 0 = 테이블 안 봐도 답. INCLUDE 인덱스 또는 "필요한 컬럼 모두 인덱스에".

Bitmap Index Scan

Bitmap Heap Scan on users
  ->  BitmapOr
        ->  Bitmap Index Scan on idx_users_city
        ->  Bitmap Index Scan on idx_users_country

여러 인덱스 결합. "city=Seoul OR country=Korea" 같은 OR 검색.

JOIN 노드 3가지

Nested Loop  → 한쪽 행마다 다른 쪽 스캔 (작은 테이블)
Hash Join    → 해시 테이블 + 룩업 (중간)
Merge Join   → 양쪽 정렬 후 병합 (인덱스 정렬)

Nested Loop

Nested Loop  (cost=...)
  ->  Index Scan on users  (rows=10)
  ->  Index Scan on orders
        Index Cond: (user_id = users.id)

사용자 10명 × 각자 orders 인덱스 검색. 작은 외부 + 인덱스 내부 = 매우 빠름.

Hash Join

Hash Join
  Hash Cond: (users.id = orders.user_id)
  ->  Seq Scan on orders
  ->  Hash
        ->  Seq Scan on users

users 로 해시 테이블 만들고 — orders 스캔. 큰 테이블 + 작은 빌드 시 효율.

Merge Join

Merge Join
  Merge Cond: (users.id = orders.user_id)
  ->  Index Scan using idx_users_id
  ->  Index Scan using idx_orders_user_id

양쪽 인덱스 정렬 = 한 번에 병합. 매우 빠름.

PG 계획자가 자동 선택. "왜 Nested Loop 가 아닌 Hash 인가" 같은 의문 = 통계·비용 추정 차이.

정렬·집계 노드

Sort  (cost=... )
  Sort Key: created_at DESC
  Sort Method: quicksort  Memory: 25kB
  ->  ...

Sort Method = quicksort (메모리) · external merge (디스크). 디스크 정렬 = 느림. work_mem 늘리기.

HashAggregate  (cost=...)
  Group Key: category
  ->  ...

GROUP BY 의 해시 집계. 큰 그룹 = 메모리 부담.

Aggregate  (cost=...)
  ->  ...

집계 함수 (COUNT·SUM 등). 단순한 경우.

Limit 노드

Limit
  ->  Sort
        Sort Key: created_at DESC
        ->  Seq Scan

LIMIT 가 잘 박히면 — Limit ▸ Index Scan 형태로 정렬 X. 인덱스 ORDER BY 방향 활용.

CTE·서브쿼리

CTE Scan on cte_name
  ->  ... (CTE 정의)

PG 12+ 부터 CTE 가 "인라인 자동" (옵션). 옛 동작 (CTE materialize) 이 필요하면 WITH ... AS MATERIALIZED.

비용·실측 분석

비용 의미

cost=START..TOTAL  rows=N  width=W
  • START = 첫 행 반환까지 비용 (LIMIT 에 유리)
  • TOTAL = 모든 행 처리까지 비용
  • rows = 추정
  • width = 평균 바이트

실측 비교

actual time=START..TOTAL rows=N loops=L
  • START..TOTAL = 실제 시간 (ms)
  • rows = 실제 결과
  • loops = 노드 실행 횟수 (Nested Loop 안 inner)

loops=1000 + actual time 10ms = 노드 자체는 10ms, 총 10초.

추정 vs 실측 어긋남

rows=100 (estimated) vs rows=10000 (actual)

100배 차이 = 통계 부정확. ANALYZE table; 실행.

BUFFERS — I/O 통계

Index Scan
  Buffers: shared hit=5 read=2
  • hit = 메모리 캐시 (빠름)
  • read = 디스크 (느림)

read 가 많으면 — 캐시 부족, 메모리 늘리기·자주 안 쓰는 데이터 분리.

시각화 도구

복잡한 EXPLAIN 출력은 — 도구 활용:

  • explain.depesz.com — 가장 유명
  • explain.dalibo.com — Dalibo 시각화
  • pgMustard — 분석 + 권장

JSON 출력을 붙여넣어 시각화.

실전 — 느린 쿼리 진단

1. 느린 쿼리 찾기

-- pg_stat_statements 확장
CREATE EXTENSION pg_stat_statements;

SELECT
    query,
    calls,
    total_exec_time / calls AS avg_ms,
    rows / calls AS avg_rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

운영 환경의 "느린 쿼리 TOP".

2. EXPLAIN ANALYZE

EXPLAIN (ANALYZE, BUFFERS) <slow_query>;

3. 진단

  • Seq Scan + 큰 Rows Removed = 인덱스 박기
  • Sort Method: external = work_mem 늘리기
  • Nested Loop + 큰 loops = JOIN 인덱스
  • 추정 vs 실측 어긋남 = ANALYZE

4. 적용 + 검증

인덱스 추가 → 다시 EXPLAIN → 확인.

함정 5가지

(1) EXPLAIN 만 (ANALYZE 없음)

추정만 — 실제 동작 모름. ANALYZE 무조건.

(2) BUFFERS 안 봄

캐시 hit 율 모름. BUFFERS 표준.

(3) PROD 에 EXPLAIN ANALYZE 무서워서 못 함

EXPLAIN ANALYZE = 실제 쿼리 실행. UPDATE·DELETE 에 ANALYZE 박으면 — 진짜 변경. BEGIN-ROLLBACK 으로 안전.

BEGIN;
EXPLAIN ANALYZE UPDATE users SET ...;
ROLLBACK;

(4) Cost vs Time 혼동

Cost = 추정 단위. Time = 실측 ms. 비교 X.

(5) 통계 갱신 잊음

대량 DML 후 ANALYZE 안 하면 — 계획자 잘못 판단. 주기적.

🎯 진단 4단계

(1) pg_stat_statements 느린 쿼리 TOP. (2) EXPLAIN (ANALYZE, BUFFERS). (3) 진단 (Seq Scan·Sort·loops 등). (4) 인덱스·통계·메모리 조정 후 재검증.

한 줄 정리 — EXPLAIN ANALYZE BUFFERS = 운영 진단 표준. 스캔 4종·JOIN 3종·정렬·집계 노드 의미. 추정 vs 실측 어긋남 = ANALYZE. UPDATE 진단 = BEGIN-ROLLBACK 안전. pg_stat_statements + 시각화 도구.

시험 직전 한 번 더 — EXPLAIN 입문자가 매번 헷갈리는 것

  • EXPLAIN = 추정 계획만
  • EXPLAIN ANALYZE = 실제 실행 + 실측
  • BUFFERS = I/O 통계 (운영 필수)
  • 스캔 = Seq·Index·Index Only·Bitmap
  • Index Only + Heap Fetches: 0 = 최고
  • Rows Removed by Filter = 인덱스 박을 곳
  • JOIN = Nested Loop·Hash·Merge
  • Nested Loop = 작은 외부 + 인덱스 내부
  • Hash Join = 큰 + 작은 빌드
  • Merge Join = 양쪽 정렬
  • Sort Method: quicksort vs external merge
  • work_mem 늘리면 = 메모리 정렬
  • HashAggregate = GROUP BY
  • cost = 추정 비용 단위
  • actual time = 실측 ms
  • loops = 노드 실행 횟수 (총 시간 = time × loops)
  • 추정 vs 실측 차이 100배+ = ANALYZE
  • shared hit·read = 캐시 vs 디스크
  • pg_stat_statements = 느린 쿼리 통계
  • UPDATE/DELETE 진단 = BEGIN-ROLLBACK
  • explain.depesz.com 시각화
  • 통계 부정확 = ANALYZE 수동
  • 한국 백엔드 = EXPLAIN ANALYZE BUFFERS 표준
  • 운영 진단 = 매주 느린 쿼리 TOP 점검

시리즈 다른 편

시리즈 다음 글

다음 글(41편)에서는 성능 팁 종합 — 메모리·디스크·쿼리·인덱스 운영.

공식 문서: PostgreSQL 18 — Using EXPLAIN에서 더 자세한 사양을 확인할 수 있어요.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!