날짜: 2025-08-23
-- 주문/고객 간단 스키마
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
status TEXT NOT NULL, -- 'paid', 'refund', ...
total_amount NUMERIC(12,2) NOT NULL, -- 합계 금액
ordered_at TIMESTAMPTZ NOT NULL
);
-- 인덱스
CREATE INDEX idx_orders__user_id ON orders(user_id);
CREATE INDEX idx_orders__status__ordered_at ON orders(status, ordered_at DESC);
CREATE INDEX idx_orders__ordered_at ON orders(ordered_at DESC);
EXPLAIN
= 계획만 추정, EXPLAIN ANALYZE
= 실제 실행 + 시간/행수 포함.EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE status='paid' ORDER BY ordered_at DESC LIMIT 20;
포인트
Actual Rows
vs Plan Rows
차이(카디널리티 오차)Buffers: shared hit/read
(캐시 적중/디스크 I/O)문제 쿼리:
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE user_id = 123 AND status='paid';
개선 팁:
CREATE INDEX idx_orders__user_id__status ON orders(user_id, status);
체크포인트:
Actual Rows
가 조건에 맞는 소수로 줄었는지 확인.문제 쿼리:
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, ordered_at FROM orders
WHERE status='paid'
ORDER BY ordered_at DESC
LIMIT 20;
개선 인덱스(커버링 지향):
-- 정렬열이 선두가 되도록 설계하면 Top-N 빠름
CREATE INDEX idx_orders__status__ordered_at__cover
ON orders(status, ordered_at DESC, id);
효과:
Index Scan Backward
또는 Index Only Scan
으로 Sort
제거/축소.LIMIT
과 잘 맞아 I/O 급감.문제 쿼리:
EXPLAIN (ANALYZE, BUFFERS)
SELECT u.email, o.total_amount
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE o.status='paid' AND o.ordered_at >= now() - interval '7 days';
체크:
Hash Join
이 일반적. Hash
빌드가 어느 테이블에서 일어나는지, Hash Batches
/Disk
발생 확인.
개선:-- 조인에 자주 쓰이는 조건 인덱스 강화
CREATE INDEX idx_orders__status__ordered_at__user_id
ON orders(status, ordered_at DESC, user_id);
효과:
Hash Build
대상이 작아지고, Rows Removed by Filter
감소.문제 쿼리:
EXPLAIN (ANALYZE, BUFFERS)
SELECT date_trunc('day', ordered_at) d, count(*)
FROM orders
WHERE status='paid'
GROUP BY 1
ORDER BY 1 DESC
LIMIT 7;
개선 후보:
status, ordered_at
복합 인덱스,read
(디스크) 비중이 높으면 I/O 병목 신호.ANALYZE
소홀 → 카디널리티 오차로 잘못된 플랜 선택.VACUUM (ANALYZE)
또는 auto_analyze
확인.ALTER TABLE ... ALTER COLUMN ... SET STATISTICS N;
로 샘플 업.max_parallel_workers_per_gather
/parallel_tuple_cost
등 점검.SELECT query, calls, total_time, mean_time
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 20;
-- postgresql.conf 예시
shared_preload_libraries = 'pg_stat_statements,auto_explain'
auto_explain.log_min_duration = '200ms'
auto_explain.log_analyze = on
auto_explain.log_buffers = on
pg_stat_statements
로 후보 찾기 →EXPLAIN (ANALYZE, BUFFERS)
로 병목 노드 확인 →EXPLAIN (ANALYZE, BUFFERS)
SELECT id, ordered_at
FROM orders
WHERE status='paid'
ORDER BY ordered_at DESC
LIMIT 20;
/*
Sort (cost: 높음) -> Seq Scan orders
Actual ... Buffers: read 다수, time 수백 ms
*/
CREATE INDEX CONCURRENTLY idx_orders__status__ordered_at__cover
ON orders(status, ordered_at DESC, id);
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, ordered_at
FROM orders
WHERE status='paid'
ORDER BY ordered_at DESC
LIMIT 20;
/*
Index Only Scan using idx_orders__status__ordered_at__cover
Actual time: 수 ms, Buffers hit 위주, Sort 제거
*/
pg_stat_statements
로 상시 추적Postgres는 쿼리를 실행할 때 여러 실행계획 후보를 만들어두고, 각각의 비용(Cost)을 계산한 후 가장 저렴한 플랜을 고릅니다. 여기서 Cost는 절대값이 아니라 상대값이에요. 즉, “플랜 A가 B보다 몇 배 빠르다” 정도로만 쓰입니다.
ANALYZE
로 테이블/컬럼 통계 수집 → 옵티마이저는 행 개수(rows), 데이터 분포, null 비율, 상관관계 등을 추정합니다.ANALYZE
실행 필수.
(autovacuum만 믿지 말고 batch 작업 후 수동으로 돌려주기)Postgres는 CPU와 I/O 비용을 아래 값으로 단순화해서 계산합니다:
seq_page_cost
: 순차 페이지 읽기 비용 (기본 1.0)random_page_cost
: 랜덤 페이지 읽기 비용 (기본 4.0 → SSD 환경이면 1.1~1.5 권장)cpu_tuple_cost
: 튜플 처리 비용cpu_index_tuple_cost
: 인덱스 튜플 처리 비용effective_cache_size
: OS 캐시 메모리 크기 추정치 (커야 인덱스 플랜 선호↑)👉 실무 팁:
random_page_cost
낮추면 인덱스 스캔을 더 잘 씁니다.effective_cache_size
는 실제 서버 RAM의 50~75% 정도로 설정.조건(selectivity)
× 총 행 수
= 예상 반환 row 수(outer_rows × inner_rows) × 선택도
👉 실무 팁:
EXPLAIN (ANALYZE)
로 예상 rows vs 실제 rows 비교 → 오차 크면 통계 개선 필요.플랜 노드별로 다음을 합산합니다:
(읽어야 할 페이지 수 × 페이지 비용)
+ (처리할 튜플 수 × CPU 비용)
+ (정렬/해시/병렬 등 추가 비용)
모든 후보 플랜 중 total cost가 가장 낮은 것 선택.
통계 최신화
ANALYZE
→ 쿼리 성능 급변 방지.환경 반영한 비용 조정
random_page_cost = 1.1~1.5
effective_cache_size
튜닝.실행계획 검증 루틴
EXPLAIN (ANALYZE, BUFFERS)
로 예상 vs 실제 rows 오차 확인.CREATE STATISTICS
(다중 컬럼 상관, distinct 값 등) 고려.Prepared Statement 주의
PREPARE
로 빠르게.plan_cache_mode=force_custom_plan
).random_page_cost
, effective_cache_size
를 꼭 조정.ANALYZE
·CREATE STATISTICS
로 보정.