본문으로 건너뛰기
5.4 EXPLAIN 읽는 법

5.4 EXPLAIN 읽는 법

EXPLAIN은 PostgreSQL 옵티마이저가 어떤 실행 계획을 골랐는지 보여 줍니다. 실행하지 않고 추정 비용·행 수·노드 트리만 출력합니다. 운영자가 매일 보는 도구입니다.

가장 단순한 형태

EXPLAIN SELECT * FROM orders WHERE user_id = 42;

                                  QUERY PLAN
-----------------------------------------------------------------------
 Index Scan using orders_user_id_idx on orders  (cost=0.42..23.51 rows=12 width=64)
   Index Cond: (user_id = 42)

각 줄 = 하나의 plan 노드. 들여쓰기는 트리 깊이 — 들여쓴 노드가 위 노드의 자식입니다.

비용·행·너비

(cost=A..B rows=R width=W)

의미
A첫 row까지의 누적 비용 (startup cost)
B마지막 row까지의 누적 비용 (total cost)
R추정 행 수
W한 row의 평균 너비(byte)

비용 단위는 seq_page_cost = 1.0 기준(5.2 참고).

옵션

옵션의미
ANALYZE실제로 실행하고 actual 시간·rows·loops도 보여 줌
BUFFERSshared/local buffer 히트 통계
VERBOSE출력 컬럼·쿼리 ID 등 표시
COSTS off비용 숨김 — diff 비교에 유용
TIMING offactual timing 끔. ANALYZE와 함께
SETTINGS (PG 12+)비기본 설정 표시
WAL (PG 13+)생성된 WAL 양
GENERIC_PLAN (PG 16+)prepared statement의 generic plan
FORMAT json/yaml/xml/text출력 포맷

운영에서 가장 자주 쓰는 조합:

EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT ...;

ANALYZE의 추가 정보

 Index Scan using orders_user_id_idx on orders  (cost=0.42..23.51 rows=12 width=64)
                                                (actual time=0.045..0.187 rows=15 loops=1)
   Index Cond: (user_id = 42)
   Buffers: shared hit=14 read=3
 Planning Time: 0.215 ms
 Execution Time: 0.245 ms
필드의미
actual time=A..B첫 row·마지막 row 실제 ms
actual rows실제 반환 row
loops이 노드가 호출된 횟수 (예: nested loop의 내부)
Buffers: shared hit=X read=Y캐시 히트 / 디스크 읽기 페이지
Planning Time옵티마이저 시간
Execution Time실행 시간

rows vs actual rows: 비율이 10배 이상 다르면 plan이 틀렸을 가능성이 높습니다. 5.3 통계 갱신 1순위 후보입니다.

loops > 1: nested loop에서 inner 노드가 outer row 수만큼 반복됐다는 뜻. inner 노드의 actual time × loops가 실질 시간.

자주 보이는 노드 종류

노드의미
Seq Scan테이블 전체 순차 스캔
Index Scan인덱스로 row 위치 찾고 heap에서 읽기
Index Only Scan인덱스만 보고 답 (4.3 참고)
Bitmap Heap Scan / Bitmap Index Scan큰 row 집합을 인덱스로 위치 모아 한 번에
Nested Loopouter × inner, inner는 outer row마다 한 번
Hash Joininner를 hash table로 만들고 outer가 probe
Merge Join양쪽 정렬돼 있을 때 zip
Sort정렬
HashAggregate / GroupAggregateGROUP BY
Limitrow 수 제한
AppendUNION ALL, 파티션 결과 합침
Gather / Gather Mergeparallel worker 결과 모음
Materialize노드 결과를 임시로 buffer

트리 읽기

 Nested Loop  (cost=0..100 rows=10)
   ->  Index Scan on users  (cost=0..10 rows=5)
         Index Cond: (city = 'Seoul')
   ->  Index Scan on orders  (cost=0..18 rows=2)
         Index Cond: (user_id = users.id)

읽는 순서:

  1. 가장 깊은 노드(users의 Index Scan)부터 row를 만들기 시작
  2. 각 row마다 두 번째 자식(orders의 Index Scan)을 호출
  3. 결과를 Nested Loop가 합쳐 위로 반환

자주 보이는 함정

Rows 추정 오차

Hash Join  (cost=100..200 rows=1)
           (actual time=1..5000 rows=1000000)

추정 1, 실제 1M. 그 위의 노드가 nested loop을 골랐는데 실제로는 1M번 반복돼 5초가 됩니다. 통계가 잘못된 1순위 신호입니다.

Buffer hit / read

Buffers: shared hit=14000 read=8500

read가 큰 노드는 디스크에서 읽음 — cache miss. 같은 쿼리를 두 번 실행하면 read가 줄어드는지 확인해 hot/cold 판정.

Parallel Workers

Workers Planned: 2
Workers Launched: 2

병렬이 활성됐는지, 실제로 워커가 떴는지. max_parallel_workers가 부족하면 Launched가 0이 될 수 있습니다.

Filter vs Index Cond

Index Scan on orders
  Index Cond: (user_id = 42)
  Filter: (status = 'paid')
  Rows Removed by Filter: 985

Index Cond는 인덱스로 직접 매칭. Filter는 row를 모두 읽은 뒤 조건 적용 — Removed by Filter가 크면 인덱스가 부족하다는 신호입니다.

시각화 도구

긴 plan을 사람 눈으로 따라가기 힘들면:

도구입력
explain.depesz.comEXPLAIN(ANALYZE) 텍스트
explain.dalibo.comEXPLAIN JSON
pgAdmin EXPLAIN 탭UI 통합

depesz·dalibo는 느린 노드를 색으로 강조해 줘 큰 plan에서 병목 찾기에 빠릅니다.

EXPLAIN ANALYZE는 실제로 실행한다. UPDATE·DELETE·INSERT 같은 변경 SQL에 쓰면 변경이 적용됩니다. 안전하게 보고 싶으면 트랜잭션으로 감싸 ROLLBACK:

BEGIN;
EXPLAIN (ANALYZE) DELETE FROM orders WHERE ...;
ROLLBACK;

정리

  • EXPLAIN은 plan만, EXPLAIN (ANALYZE)는 실제 실행+측정
  • (cost=A..B rows=R width=W) 형식. A=첫 row, B=마지막 row 비용
  • 트리는 깊은 노드부터 읽음
  • 추정 rows와 actual rows 차이 = plan 미스의 1순위 신호
  • BUFFERS로 cache miss 진단, Filter로 인덱스 누락 진단
  • 변경 SQL에 EXPLAIN ANALYZE는 트랜잭션으로 감싸 ROLLBACK

다음 절(5.5)에서는 EXPLAIN ANALYZE를 더 깊게 읽는 법을 봅니다.