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도 보여 줌 |
BUFFERS | shared/local buffer 히트 통계 |
VERBOSE | 출력 컬럼·쿼리 ID 등 표시 |
COSTS off | 비용 숨김 — diff 비교에 유용 |
TIMING off | actual 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 Loop | outer × inner, inner는 outer row마다 한 번 |
| Hash Join | inner를 hash table로 만들고 outer가 probe |
| Merge Join | 양쪽 정렬돼 있을 때 zip |
| Sort | 정렬 |
| HashAggregate / GroupAggregate | GROUP BY |
| Limit | row 수 제한 |
| Append | UNION ALL, 파티션 결과 합침 |
| Gather / Gather Merge | parallel 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)읽는 순서:
- 가장 깊은 노드(
users의 Index Scan)부터 row를 만들기 시작 - 각 row마다 두 번째 자식(
orders의 Index Scan)을 호출 - 결과를 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=8500read가 큰 노드는 디스크에서 읽음 — 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: 985Index Cond는 인덱스로 직접 매칭. Filter는 row를 모두 읽은 뒤 조건 적용 — Removed by Filter가 크면 인덱스가 부족하다는 신호입니다.
시각화 도구
긴 plan을 사람 눈으로 따라가기 힘들면:
| 도구 | 입력 |
|---|---|
| explain.depesz.com | EXPLAIN(ANALYZE) 텍스트 |
| explain.dalibo.com | EXPLAIN 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와 actualrows차이 = plan 미스의 1순위 신호 - BUFFERS로 cache miss 진단, Filter로 인덱스 누락 진단
- 변경 SQL에 EXPLAIN ANALYZE는 트랜잭션으로 감싸 ROLLBACK
다음 절(5.5)에서는 EXPLAIN ANALYZE를 더 깊게 읽는 법을 봅니다.