본문 바로가기
Database2026년 8월 25일6분 읽기

PostgreSQL 파티셔닝 — 수억 행 로그 테이블 다루기

YS
김영삼
조회 5
PostgreSQL 파티셔닝 — 수억 행 로그 테이블 다루기

PostgreSQL 파티셔닝은 큰 테이블을 하나처럼 다루되 내부적으로 여러 개의 작은 테이블(파티션)로 쪼개 저장하는 기능이다. 로그·이벤트·주문처럼 시간에 따라 무한정 쌓이는 데이터에서, 조회는 필요한 파티션만 스캔하고 오래된 데이터는 파티션째 잘라내 지운다.

수억 행짜리 로그 테이블을 통짜로 두면 인덱스가 비대해지고, 오래된 데이터 삭제 한 번에 DELETE가 몇 시간씩 걸린다. 나도 이벤트 테이블이 10억 행을 넘기면서 밤마다 도는 정리 배치가 서비스에 락을 거는 걸 보고서야 파티셔닝으로 갈아탔다.

선언적 파티셔닝 — RANGE로 월별 분할

Postgres 10부터 선언적 파티셔닝이 정식 지원된다. 시계열 데이터는 보통 시간 컬럼 기준 RANGE 파티셔닝으로 월별·일별로 나눈다. 부모 테이블은 데이터를 직접 안 갖고, 라우팅만 한다.

-- 부모: PARTITION BY로 분할 기준 선언
CREATE TABLE events (
  id         bigserial,
  created_at timestamptz NOT NULL,
  user_id    bigint,
  payload    jsonb,
  PRIMARY KEY (id, created_at)   -- 파티션 키를 PK에 포함해야 함
) PARTITION BY RANGE (created_at);

-- 월별 자식 파티션
CREATE TABLE events_2026_08 PARTITION OF events
  FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');
CREATE TABLE events_2026_09 PARTITION OF events
  FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');

여기서 자주 걸리는 게 기본키 제약이다. 파티션 키(created_at)는 반드시 PK나 유니크 제약에 포함돼야 한다. 그래서 (id, created_at) 복합키를 쓴다. 이걸 몰라 PRIMARY KEY (id)로 시작했다가 에러 보고 한참 헤맸다.

왜 빨라지나 — 파티션 프루닝

진짜 이득은 파티션 프루닝(pruning)에서 나온다. 쿼리에 파티션 키 조건이 있으면 플래너가 관련 없는 파티션을 아예 스캔에서 제외한다.

-- created_at 조건이 있으니 8월 파티션만 스캔
EXPLAIN
SELECT count(*) FROM events
 WHERE created_at >= '2026-08-10'
   AND created_at <  '2026-08-20';
-- → events_2026_08 하나만 훑음. 나머지 파티션은 건너뜀.

포인트는 쿼리에 파티션 키 조건이 꼭 들어가야 프루닝이 동작한다는 것이다. WHERE user_id = 5만 있고 시간 조건이 없으면 모든 파티션을 다 뒤진다. 그래서 파티션 키를 뭘로 잡느냐가 이 기능의 성패를 가른다. 대부분의 조회가 시간 범위를 쓰면 시간 파티셔닝이 맞다.

오래된 데이터는 DROP으로 즉시 삭제

파티셔닝의 또 다른 큰 이점. 지난 데이터를 지울 때 DELETE가 아니라 파티션을 통째로 떼어낸다. DROP TABLE이나 DETACH는 거의 즉시 끝나고 테이블 부풀림(bloat)도 안 남긴다.

-- 6개월 지난 파티션을 통째로 제거 (DELETE보다 압도적으로 빠름)
DROP TABLE events_2026_02;

-- 지우기 전 아카이브가 필요하면 먼저 분리
ALTER TABLE events DETACH PARTITION events_2026_02;
작업파티셔닝 없음파티셔닝
기간 조회전체 인덱스 스캔해당 파티션만
오래된 데이터 삭제느린 DELETE+bloatDROP으로 즉시
인덱스 크기거대·단일파티션별로 작음

실전 운영 팁

미래 파티션은 미리 만들어 둬야 한다. 다음 달 파티션이 없는데 그 달 데이터가 들어오면 삽입이 실패한다. pg_partman 확장을 쓰면 파티션 생성·정리를 자동화할 수 있어 손이 편하다. 직접 관리한다면 cron으로 매달 다음 파티션을 미리 CREATE하는 잡을 돌린다.

또 하나, 파티션이 수백 개를 넘으면 플래닝 시간이 늘 수 있다. 무작정 일별로 잘게 쪼개지 말고, 데이터량과 조회 패턴을 보고 월별·주별로 적정 크기를 정하자. 인덱스는 부모에 만들면 각 파티션에 자동 전파되니, 파티션마다 손으로 인덱스를 만들 필요는 없다.

자주 묻는 질문

기존 큰 테이블을 파티션 테이블로 어떻게 바꾸나요?

기존 테이블을 그 자리에서 파티션 부모로 바꿀 수는 없습니다. 새 파티션 테이블을 만들고 데이터를 옮기는 방식이 표준입니다. 무중단이 필요하면 새 테이블에 이중 쓰기하며 과거 데이터를 배치로 마이그레이션한 뒤 전환하거나, pg_partman의 마이그레이션 도구를 활용합니다.

파티션 키는 무엇으로 정해야 하나요?

대부분의 조회가 걸리는 컬럼으로 정합니다. 시계열이면 시간 컬럼이 자연스럽고, 이때 조회에 시간 범위 조건을 넣어야 파티션 프루닝이 동작합니다. 조회가 주로 테넌트별이라면 테넌트 ID 기준 LIST/HASH 파티셔닝이 더 나을 수 있습니다.

파티션을 많이 만들수록 좋은가요?

아닙니다. 파티션이 지나치게 많으면 플래닝 오버헤드와 관리 비용이 커집니다. 각 파티션이 너무 작아도 이점이 줄어듭니다. 파티션당 수백만 행 정도가 되도록 월별·주별로 적정하게 자르는 편이 실용적입니다.

파티셔닝하면 유니크 제약은 어떻게 되나요?

글로벌 유니크 제약을 걸려면 파티션 키가 그 유니크 컬럼에 포함돼야 합니다. 즉 id 단독 유니크는 불가하고 (id, created_at)처럼 파티션 키를 포함한 복합 제약을 씁니다. 파티션 키를 뺀 전역 유일성이 꼭 필요하면 애플리케이션 레벨 보장이나 별도 설계를 검토해야 합니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록