Files
2nd/10_Wiki/Topics/Coding/DB_Postgres_EXPLAIN.md
T
2026-05-09 21:08:02 +09:00

6.0 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
db-postgres-explain Postgres EXPLAIN — Plan / Index / Cost 분석 Coding draft B conceptual 2026-05-09 2026-05-09
database
postgres
explain
vibe-coding
language applicable_to
SQL / Postgres
Backend
EXPLAIN
EXPLAIN ANALYZE
query plan
sequential scan
index scan
nested loop
hash join

Postgres EXPLAIN

느린 query = plan 부터. EXPLAIN ANALYZE BUFFERS 가 표준. Sequential scan / index scan / join type / actual time 분석. explain.dalibo.com 또는 pgMustard 가 시각화.

📖 핵심 개념

  • EXPLAIN: plan 만 (실행 X).
  • EXPLAIN ANALYZE: 실제 실행 + 시간.
  • BUFFERS: 디스크 vs cache hit.
  • VERBOSE: 상세 정보.

💻 코드 패턴

기본

EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 'u1' ORDER BY created_at DESC LIMIT 10;
Limit  (cost=0.42..1.20 rows=10 width=128) (actual time=0.05..0.15 rows=10 loops=1)
  ->  Index Scan using orders_user_created on orders  (cost=0.42..123.45 rows=1500 width=128) (actual time=0.04..0.14 rows=10 loops=1)
        Index Cond: (user_id = 'u1')
Planning Time: 0.12 ms
Execution Time: 0.18 ms

→ Index Scan + Index Cond + actual rows 적음 = OK.

Sequential scan = warning

Seq Scan on orders  (cost=0.00..50000.00 rows=1000000 width=128)
  Filter: (user_id = 'u1')
  Rows Removed by Filter: 999999

→ 100만 row 모두 읽음. Index 필요.

Buffers 확인

EXPLAIN (ANALYZE, BUFFERS)
SELECT ...;

# Output:
Buffers: shared hit=42 read=8
# hit = cache, read = 디스크
#  read = 디스크 I/O bottleneck

Index 추가 + 재실행

CREATE INDEX orders_user_created ON orders (user_id, created_at DESC);

EXPLAIN ANALYZE SELECT ... -- 다시
-- Index Scan 으로 변환

자주 쓰는 plan node

Seq Scan:       풀 스캔 — 큰 테이블 = 느림
Index Scan:     index 로 row lookup — 빠름
Index Only Scan: index 만 — heap 접근 X (covering index)
Bitmap Heap Scan: 여러 row, bitmap 으로 batch
Hash Join:      양쪽 hash 후 조인 — 큰 join 빠름
Nested Loop:    각 row 마다 다른 쪽 lookup — 작은 한쪽
Merge Join:     양쪽 sort 후 merge
Sort:           sort 작업 — work_mem 초과 시 디스크
Aggregate:      group by / sum
HashAggregate:  hash 기반 aggregate

Statistics — planner 가 잘못 추측

rows=1000 (실제 100만)
→ planner 가 잘못 — Seq Scan 선택할 수 있음
ANALYZE orders;  -- statistics update

→ 자주 쓰는 큰 table = autovacuum 외 명시.

Index 사용 안 하는 경우

-- ❌ 함수 / 변환
SELECT * FROM users WHERE LOWER(email) = 'a@b.com';
-- email 인덱스 못 씀

-- ✅ Functional index
CREATE INDEX users_email_lower ON users ((LOWER(email)));

-- ❌ LIKE leading %
WHERE email LIKE '%@gmail.com'

-- ❌ Type cast
WHERE id::text = '42'

-- ❌ OR with different columns
WHERE name = 'a' OR email = 'b'
-- → UNION 으로 분리

Composite index 순서

-- (user_id, created_at) 인덱스
SELECT ... WHERE user_id = $1                          -- ✅ 사용
SELECT ... WHERE user_id = $1 AND created_at > $2      -- ✅
SELECT ... WHERE created_at > $2                       -- ❌ leading 안 맞음

LIMIT + ORDER BY = matching index

SELECT * FROM events ORDER BY ts DESC LIMIT 10;
-- (ts DESC) index = O(log n)

-- 또는 (user_id, ts DESC)
WHERE user_id = $1 ORDER BY ts DESC LIMIT 10;

EXPLAIN 시각화

explain.dalibo.com
explain.depesz.com
pgMustard (paid)

→ 큰 plan tree 가 직관적.

auto_explain (slow query log)

-- postgresql.conf
shared_preload_libraries = 'auto_explain'
auto_explain.log_min_duration = '500ms'
auto_explain.log_analyze = on
auto_explain.log_buffers = on

→ 500ms+ query 자동 plan log.

pg_stat_statements

CREATE EXTENSION pg_stat_statements;

SELECT query, calls, total_exec_time, mean_exec_time, rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC LIMIT 20;

→ "어떤 query 가 가장 비싸"

Common 문제 + fix

1. Seq Scan + 큰 table → CREATE INDEX
2. Sort 가 외부 디스크 (Sort Method: external merge) → work_mem 증가
3. Nested Loop + 큰 양쪽 → ANALYZE 후 hash join 으로
4. 큰 IN list → 임시 테이블 또는 ANY array
5. Subquery 가 매 row 실행 → JOIN 또는 LATERAL

work_mem

SET work_mem = '64MB';  -- 세션
-- 또는 query
SELECT /*+ work_mem='64MB' */ ...;  -- pg_hint_plan

→ Sort / Hash 가 메모리 안 들어가면 외부 디스크.

Index size 검사

SELECT
  schemaname, tablename, indexname,
  pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_indexes
JOIN pg_class ON pg_class.relname = indexname
ORDER BY pg_relation_size(indexrelid) DESC LIMIT 10;

Unused index

SELECT
  schemaname, relname, indexrelname,
  idx_scan, pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
  AND indexrelname NOT LIKE '%_pkey'
ORDER BY pg_relation_size(indexrelid) DESC;

→ 사용 안 되는 index = drop.

🤔 의사결정 기준

증상 액션
느린 query EXPLAIN ANALYZE
Seq Scan 큰 table Index 추가
Sort 외부 디스크 work_mem 또는 인덱스
Wrong rows estimate ANALYZE
매번 느림 pg_stat_statements
Unused index DROP

안티패턴

  • EXPLAIN 만 + ANALYZE 안 함: 실제 시간 모름.
  • Plan 만 보고 OK 가정: row estimate 가 wrong 일 수 있음.
  • **SELECT ***: column 다 read.
  • 함수 / cast index 컬럼: index 못 씀.
  • Statistics 안 update: planner 잘못.
  • Unused index 방치: 디스크 + INSERT 비용.
  • Production 에 ANALYZE 큰 table: lock. CONCURRENTLY.

🤖 LLM 활용 힌트

  • EXPLAIN (ANALYZE, BUFFERS) 표준.
  • explain.dalibo.com 시각화.
  • pg_stat_statements + auto_explain prod.

🔗 관련 문서