“테이블이 커서 파티셔닝했다"는 말을 자주 듣는다. 그리고 그 뒤에 “그런데 안 빨라졌다"가 따라오는 경우도 자주 본다.

파티셔닝은 쿼리를 빠르게 하는 기능이 아니다. 조건에 맞는 파티션만 읽게 하는 기능이다. 그 조건이 안 갖춰지면 이득은 없고 비용만 남는다.

파티션 프루닝

주문 테이블을 월별로 나눴다고 하자.

CREATE TABLE orders (
  id         bigint,
  created_at timestamptz NOT NULL,
  ...
) PARTITION BY RANGE (created_at);

CREATE TABLE orders_2026_08 PARTITION OF orders
  FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');
CREATE TABLE orders_2026_09 PARTITION OF orders
  FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');

이 쿼리는 9월 파티션 하나만 읽는다.

SELECT * FROM orders
WHERE created_at >= '2026-09-01' AND created_at < '2026-09-15';

플래너가 조건과 각 파티션의 범위를 비교해서 겹치지 않는 파티션을 계획에서 뺀다. 이게 프루닝이다. 파티션이 36개면 35개를 안 읽는다. 여기서 성능이 나온다.

EXPLAIN으로 보면 계획에 orders_2026_09만 등장한다.

프루닝이 안 되는 모양

키에 함수를 씌웠다.

WHERE date_trunc('month', created_at) = '2026-09-01'
WHERE to_char(created_at, 'YYYY-MM') = '2026-09'
WHERE created_at::date = '2026-09-10'

플래너는 date_trunc(created_at)의 결과와 파티션 범위를 비교할 수 없다. 파티션 범위는 created_at 자체로 정의돼 있으니까. 전체 파티션을 읽는다.

조건을 범위로 바꾼다.

WHERE created_at >= '2026-09-01' AND created_at < '2026-10-01'

키가 조건에 없다.

SELECT * FROM orders WHERE user_id = 12345;

user_id로는 어느 파티션인지 알 수 없다. 36개 파티션을 전부 읽는다. 각 파티션에 user_id 인덱스가 있으면 36번 인덱스 스캔이다. 단일 테이블이었으면 한 번이다.

파티션 키가 아닌 컬럼으로 자주 조회하는 테이블은 파티셔닝이 손해일 수 있다. 어떤 쿼리가 주로 오는지가 파티션 키 선택보다 먼저다.

조인으로만 키가 결정된다.

SELECT * FROM orders o
JOIN events e ON e.order_created_at = o.created_at
WHERE e.id = 100;

o.created_at의 값은 events를 읽어야 알 수 있다. 계획 단계에서는 모른다. PostgreSQL 11부터 실행 시점 프루닝이 있어서, 중첩 루프 조인의 안쪽이라면 바깥에서 값이 정해질 때마다 파티션을 고를 수 있다. 하지만 해시 조인이나 머지 조인이면 안 된다.

이런 경우 조건을 명시적으로 넣어주는 게 낫다. 애플리케이션이 created_at을 알고 있다면 WHERE에 직접 쓴다.

OR 조건. WHERE created_at = A OR user_id = B는 두 번째 조건 때문에 프루닝이 안 된다. UNION으로 쪼갠다.

프루닝이 되는지 확인하기

프루닝은 세 시점에 일어난다.

시점조건의 형태EXPLAIN에서
계획상수제거된 파티션이 아예 안 보임
초기화프리페어드 파라미터Subplans Removed: N
실행서브쿼리, 중첩 루프 파라미터Subplans Removed: N
EXPLAIN SELECT * FROM orders WHERE created_at >= $1;

상수면 계획에 파티션 하나만 나온다. 파라미터면 전체가 나오되 Subplans Removed가 붙는다. 이것도 없이 전체 파티션이 다 나오면 프루닝이 안 된 것이다.

enable_partition_pruning은 기본 on이다. 꺼져 있을 일은 거의 없지만 확인은 해볼 만하다.

파티션 수의 비용

프루닝이 되어도 파티션이 많으면 비용이 있다.

플래너는 파티션마다 계획을 세운다. 프루닝으로 뺀다 해도 그 판단 자체에 시간이 든다. 잠금도 파티션마다 잡는다. PostgreSQL 12에서 크게 개선됐지만, 수천 개가 되면 단순 조회의 계획 시간이 실행 시간보다 길어지는 상황이 생긴다.

일 단위 파티션을 3년 유지하면 천 개가 넘는다. 월 단위면 36개다. 프루닝 효과는 대개 월 단위로도 충분하다. 파티션 하나가 너무 커서 곤란한 게 아니라면 굵게 나누는 게 낫다.

파티션마다 통계도 따로 있고 autovacuum도 따로 돈다. 이건 장점이다. VACUUM 시리즈에서 큰 테이블의 autovacuum이 늦게 도는 문제를 다뤘는데, 파티셔닝하면 각 파티션이 작아져서 그 문제가 완화된다.

그래서 언제 파티셔닝하나

성능이 목적이라면 조건이 있다. 주요 쿼리가 파티션 키로 범위를 제한하는가. 그렇지 않으면 파티셔닝으로 빨라지지 않는다.

파티셔닝의 더 확실한 이득은 다른 데 있다. 오래된 데이터를 파티션째 떼어내는 것이다. 억 단위 행을 DELETE하면 몇 시간이 걸리고 죽은 튜플이 남지만, 파티션을 DROP하면 순간이다. 이건 프루닝과 무관하게 항상 이득이고, 다음 편에서 다룬다.

자주 묻는 질문

Q. 테이블을 파티셔닝했는데 쿼리가 빨라지지 않는 이유는 무엇인가요?

파티셔닝의 성능 이득은 파티션 프루닝, 즉 WHERE 조건으로 불필요한 파티션을 건너뛰는 데서 나옵니다. 조건에 파티션 키가 없거나, 키에 함수를 씌웠거나, 조인으로만 키가 결정되면 프루닝이 안 되어 모든 파티션을 읽습니다. 이 경우 파티션마다 별도 스캔이 일어나 단일 테이블보다 오히려 느릴 수 있습니다.

Q. 파티션 프루닝이 되는지 어떻게 확인하나요?

EXPLAIN 결과에서 실제로 나타나는 파티션 수를 봅니다. 상수 조건이면 계획 단계에서 제거되어 해당 파티션만 계획에 등장하고, 파라미터나 서브쿼리 조건이면 실행 시점에 제거되어 Subplans Removed: N이 표시됩니다. 전체 파티션이 계획에 다 나오면 프루닝이 안 된 것입니다.

Q. 파티션은 몇 개까지 만들어도 되나요?

정해진 상한은 없지만 파티션마다 계획 비용이 들어 수가 늘수록 계획 시간이 길어집니다. PostgreSQL 12부터 크게 개선되어 수백 개는 무리가 없고, 수천 개부터는 프루닝이 되더라도 계획 단계의 오버헤드가 체감됩니다. 일 단위보다 월 단위처럼 파티션 수를 수백 개 안으로 유지하는 설계가 일반적입니다.