7.4 파티션 가지치기
파티셔닝의 효과는 partition pruning으로 결정됩니다. 쿼리의 WHERE 절을 보고 건드릴 필요 없는 파티션을 제외하는 메커니즘입니다. pruning이 잘 되면 한 파티션만 스캔, 안 되면 모든 파티션을 스캔 — 결과는 같지만 비용은 100배 이상 차이날 수 있습니다.
두 단계
PostgreSQL은 두 시점에 pruning을 시도합니다.
| 단계 | 시점 | 조건 |
|---|---|---|
| plan-time pruning | 옵티마이저가 plan 만들 때 | WHERE에 상수 또는 안전한 식 |
| execution-time pruning | 실행 직전 | prepared statement의 파라미터·generic plan |
-- plan-time: WHERE의 값이 상수 → 즉시 가지치기
EXPLAIN SELECT * FROM events WHERE created_at >= '2026-05-01';
-- 결과: events_2026_05만 스캔
-- execution-time: 파라미터가 prepare 시점에 모름
PREPARE q AS SELECT * FROM events WHERE created_at >= $1;
EXPLAIN EXECUTE q ('2026-05-01');
-- 결과: "Subplans Removed: N" 형태로 실행 시점에 가지치기EXPLAIN에서 확인
Append (cost=...)
Subplans Removed: 11 -- 12개 중 11개 가지치기
-> Seq Scan on events_2026_05 -- 남은 1개만 스캔Subplans Removed가 보이면 execution-time pruning이 동작한 것.
plan-time이면 아예 다른 파티션 노드가 plan에 안 나타납니다.
동작하는 WHERE
pruning은 옵티마이저가 파티션 경계와 WHERE를 직접 비교 가능할 때만 동작합니다.
| WHERE | pruning |
|---|---|
created_at = '2026-05-15' | ✓ |
created_at BETWEEN ... AND ... | ✓ |
created_at > now() - interval '1 day' | ✓ (stable 식) |
country IN ('KR', 'JP') (LIST) | ✓ |
WHERE date_trunc('day', created_at) = '2026-05-15' | 잘 안 됨 (함수 결과) |
WHERE created_at::date = '2026-05-15' | 잘 안 됨 (cast 함수) |
WHERE created_at = (SELECT max(...) FROM ...) | execution-time만 |
함수가 들어가면 옵티마이저가 결과를 미리 모르므로 모든 파티션을 스캔합니다. WHERE에 파티션 키가 그대로 들어가는 형태가 가장 안전.
동작 사례
range 파티션 + 상수
EXPLAIN SELECT * FROM events
WHERE created_at >= '2026-05-01' AND created_at < '2026-06-01';
Seq Scan on events_2026_05 -- 단일 파티션만list 파티션 + IN
EXPLAIN SELECT * FROM orders WHERE country IN ('KR', 'JP');
Append
-> Seq Scan on orders_kr
-> Seq Scan on orders_jphash 파티션 + 등호
EXPLAIN SELECT * FROM users WHERE id = 42;
Seq Scan on users_p3 -- 해시 결과로 한 파티션만JOIN에서의 pruning
JOIN 키가 파티션 키면 파티션-와이즈 조인이 가능 (PG 11+). 같은 파티션끼리만 join.
SET enable_partitionwise_join = on; -- PG 11~13 기본 off
-- PG 14+: 기본 on Append
-> Hash Join
-> Seq Scan on a_2026_05
-> Hash on b_2026_05
-> Hash Join
-> Seq Scan on a_2026_06
-> Hash on b_2026_06비슷하게 enable_partitionwise_aggregate = on은 파티션별 부분 집계 후 합치는 plan을 활성화합니다.
GROUP BY 가지치기
SET enable_partitionwise_aggregate = on;
SELECT user_id, count(*)
FROM events
WHERE created_at >= '2026-05-01' AND created_at < '2026-06-01'
GROUP BY user_id;
-- 한 파티션 안에서 집계 → 결과 작음함정 1 — now()·current_date
WHERE created_at > now() - interval '1 day'now()는 STABLE 함수라 한 트랜잭션에서 고정. 옵티마이저가 plan-time에 값을 미리 모름 → execution-time pruning만 가능합니다.
실용적으로는 execution-time이면 충분히 빠릅니다. 하지만 EXPLAIN이 generic plan을 만들 때 모든 파티션이 plan에 들어있어 plan 시간이 길어질 수 있습니다.
대응: 애플리케이션에서 상수로 만들어 보냄.
since = datetime.now() - timedelta(days=1)
cur.execute("SELECT ... WHERE created_at > %s", (since,))
-- 이 식은 prepared statement의 $1 → execution-time pruning함정 2 — 캐스팅·함수 호출
WHERE date_trunc('day', created_at) = '2026-05-15' -- 모든 파티션 스캔
WHERE created_at::date = '2026-05-15' -- 같음
WHERE EXTRACT(month FROM created_at) = 5 -- 모든 파티션 스캔대응: 파티션 키를 그대로 둔 범위 비교로 재작성합니다.
WHERE created_at >= '2026-05-15' AND created_at < '2026-05-16' -- 가지치기 OK함정 3 — 파티션 너무 많음
100+ 파티션이 되면 plan time 자체가 길어집니다. 5ms 옵티마이징이 50ms로 자라는 식.
| 완화 | 메모 |
|---|---|
| 활성 파티션 수 제한 | 5년치 월 파티션이면 60개 — OK |
| 오래된 파티션은 분기/년 단위로 합침 (rolling) | pg_partman의 retention 정책 |
plan_cache_mode = force_generic_plan | 매번 plan 안 하고 caching |
함정 4 — IN (subquery)
WHERE created_at IN (SELECT max(t) FROM ...)서브쿼리 결과를 옵티마이저가 미리 모름 → 모든 파티션 스캔. 가능하면 상수 또는 변수로 분리합니다.
EXPLAIN 활용 패턴
운영 중 슬로우 쿼리를 진단할 때 pruning 동작 확인:
EXPLAIN (ANALYZE, BUFFERS)
SELECT ...
;체크:
| 신호 | 의미 |
|---|---|
Append 아래에 한 자식만 | plan-time pruning 성공 |
Subplans Removed: N | execution-time pruning |
| 모든 파티션이 자식으로 나열됨 | pruning 실패 — WHERE 식 재검토 |
Workers Planned: 0 | parallel partition scan이 안 됨 (작은 파티션) |
정리
- pruning은 plan-time과 execution-time 두 단계로 동작
- WHERE에 파티션 키가 그대로 들어가야 안전 — 함수·cast 회피
now()·current_date는 execution-time만 — 보통 충분히 빠름- partition-wise join·aggregate는 PG 11+ 옵션. 14+ 기본 on
- 파티션 100+ 개면 plan time 자체가 비용 — 활성 파티션 수 관리 중요
- 진단은 EXPLAIN의
Subplans Removed와Append의 자식 수
다음 절(7.5)에서는 파티션 생성·삭제·archive를 자동화하는 pg_partman을 봅니다.