
결론부터 말씀드리면, 로그 테이블이 수억 행 규모로 커졌다고 해서 파티셔닝부터 꺼내 드는 게 항상 정답은 아닙니다. 조회 조건이 거의 매번 날짜 범위로 걸리고, 오래된 로그를 주기적으로 지워야 하는 구조라면 파티셔닝이 확실한 효과를 냅니다. 반대로 전체 기간을 가로지르는 집계 쿼리가 많거나 파티션 관리를 자동화할 여력이 없다면, 이득보다 운영 부담이 먼저 커질 수 있습니다.
수억 행 로그 테이블은 왜 이렇게 느려질까요
사실 행 수 자체가 문제의 본질은 아닙니다. 체감 지연이 커지는 분기점은 보통 인덱스 크기가 메모리(PostgreSQL의 shared_buffers, MySQL InnoDB의 buffer pool)를 넘어서는 시점입니다. 그 전까지는 인덱스 블록이 캐시에 올라가 있어서 조회가 빠르지만, 넘어가는 순간부터 디스크 랜덤 I/O가 섞이면서 지연이 급격히 늘어납니다.
여기에 로그성 데이터 특유의 패턴이 더해집니다. INSERT는 계속 쌓이는데 오래된 행을 DELETE로 지우면, 지워진 공간이 바로 회수되지 않고 테이블과 인덱스에 블로트(bloat)로 남습니다. PostgreSQL이라면 autovacuum이 수억 행 테이블을 훑는 데 걸리는 시간 자체가 길어지고, 그 사이 다른 쿼리 성능에도 영향을 줍니다.
파티셔닝이 이득을 보는 조건을 정리해봤습니다
로그 파일 성격의 데이터는 시간 순으로 계속 쌓이고, 보존 기간이 지나면 통째로 버려진다는 공통점이 있습니다. 이 특성과 파티셔닝의 작동 방식이 얼마나 맞아떨어지는지가 이득 여부를 가릅니다.
| 조건 | 파티셔닝 효과 | 이유 |
|---|---|---|
| 조회가 거의 항상 날짜 범위로 필터링됨 | 큼 | 해당 기간 파티션만 스캔(파티션 프루닝) |
| 보존 기간 지난 로그를 주기적으로 삭제함 | 큼 | DROP PARTITION은 메타데이터 작업이라 DELETE보다 훨씬 빠르고 WAL/binlog 부담도 적음 |
| 전체 기간 집계(월별 합계 등)를 자주 조회함 | 적음 | 모든 파티션을 훑어야 해서 Append 오버헤드만 추가됨 |
| PK·UNIQUE 제약이 파티션 키를 포함하지 않음 | 없음 또는 제약 발생 | 제약 자체를 걸 수 없거나 스키마 변경이 필요함 |
표에서 보듯 “날짜로 자르고, 날짜로 버린다”는 패턴에 가까울수록 이득이 커집니다. 거꾸로 조회 조건이 파티션 키와 무관한 user_id, status 같은 컬럼 위주라면, 파티셔닝을 해도 체감 성능은 거의 그대로이거나 오히려 약간 떨어질 수 있습니다.
파티션 키를 잘못 고르면 생기는 일
파티션 키는 반드시 실제 쿼리의 WHERE 조건과 일치해야 효과가 납니다. 예를 들어 조회는 늘 created_at 기준인데 파티션 키를 user_id 해시로 잡으면, 모든 쿼리가 거의 모든 파티션을 뒤지게 됩니다. 이러면 단일 테이블일 때보다 오히려 플래너가 처리할 서브플랜 수만 늘어나는 역효과가 납니다.
파티션 개수 자체도 변수입니다. 파티션이 수백~수천 개로 늘어나면 쿼리 플래닝 시간과 락 관리 오버헤드가 함께 증가합니다. PostgreSQL 공식 문서에서도 파티션 수가 과도하게 많아지면 플래닝·실행 비용이 늘어날 수 있다고 안내하고 있어서, 일별 파티션보다 월별이나 주별로 묶는 편이 수억 행 로그 테이블에는 더 안전한 경우가 많습니다.
PostgreSQL 선언적 파티셔닝을 코드로 확인해 보세요
PostgreSQL은 10 버전부터 CREATE TABLE … PARTITION BY 구문으로 선언적 파티셔닝을 지원합니다. access_log 테이블을 월 단위 RANGE 파티션으로 만드는 예시는 다음과 같습니다.
CREATE TABLE access_log (
id BIGSERIAL,
created_at TIMESTAMPTZ NOT NULL,
request_path TEXT,
status_code INT,
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
CREATE TABLE access_log_2026_10 PARTITION OF access_log
FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
CREATE TABLE access_log_2026_11 PARTITION OF access_log
FOR VALUES FROM ('2026-11-01') TO ('2026-12-01');
여기서 PRIMARY KEY가 (id, created_at)로 파티션 키인 created_at을 포함하고 있는 점을 눈여겨봐야 합니다. PostgreSQL은 파티션 테이블의 PK·UNIQUE 제약에 파티션 키 컬럼을 반드시 포함하도록 강제합니다. id 단독으로 PK를 걸고 싶어도 구조상 불가능합니다.
조회 시에는 다음처럼 날짜 범위를 조건에 넣으면 플래너가 해당 월 파티션만 선택합니다.
EXPLAIN SELECT * FROM access_log
WHERE created_at >= '2026-10-05' AND created_at < '2026-10-06';
실행 계획의 Append 노드 아래에 access_log_2026_10 파티션 하나만 자식 노드로 나타나고, 나머지 월 파티션은 아예 플랜에 등장하지 않습니다. 이 동작 방식은 파티션 프루닝 문서에 정리되어 있으며, 조건절이 파티션 키와 어긋나면 이 최적화 자체가 동작하지 않는다는 점도 함께 명시돼 있습니다.
파티셔닝이 치르는 대가: 관리 복잡도와 쿼리 제약
가장 큰 벽은 마이그레이션입니다. PostgreSQL은 이미 수억 행이 쌓인 일반 테이블을 ALTER TABLE 한 번으로 파티션 테이블로 바꿔주는 기능을 제공하지 않습니다. 새 파티션 테이블을 만들고 기존 데이터를 옮기는 작업이 필요한데, 수억 행이면 이 과정만 몇 시간에서 며칠씩 걸릴 수 있고 서비스 영향을 줄이려면 분할 적재나 논리 복제 전략까지 고민해야 합니다.
운영 중에도 신경 쓸 일이 늘어납니다. 다음 달 파티션을 미리 만들어두지 않으면 기본 파티션(DEFAULT PARTITION)이 없는 구조에서는 INSERT 자체가 에러로 막힙니다. 그래서 실무에서는 pg_partman 같은 확장이나 별도 배치 스크립트로 파티션 생성·삭제를 자동화하는 게 사실상 필수입니다.
쿼리 쪽 제약도 따라옵니다. 앞서 본 것처럼 PK·UNIQUE 제약에 파티션 키를 포함해야 하고, 전체 기간을 대상으로 하는 집계 쿼리는 파티셔닝으로 빨라지지 않습니다. 외래 키로 파티션 테이블을 참조하는 구조에도 제약이 있어서, 연관 테이블 설계를 함께 손봐야 하는 경우가 많습니다.
그래도 지금 파티셔닝을 해야 할까요
아래 체크리스트 중 두 개 이상에 해당한다면 파티셔닝을 적극 검토할 만합니다.
- 조회 쿼리의 대부분이 날짜·시간 범위로 필터링되는지
- 보존 기간 정책에 따라 오래된 로그를 정기적으로 삭제해야 하는지
- 파티션 자동 생성·삭제를 운영할 인력이나 자동화 도구가 있는지
- pg_relation_size 같은 함수로 인덱스 크기를 실측해서 메모리 한도를 넘은 걸 확인했는지
해당하는 항목이 하나뿐이거나 없다면, 파티셔닝보다 먼저 인덱스 재설계나 오래된 데이터를 별도 아카이브 테이블로 옮기는 방법부터 시도해 보는 편이 리스크가 작습니다. 마이그레이션 비용을 치르고도 체감 효과가 크지 않을 수 있기 때문입니다. 반대로 체크리스트 대부분에 해당한다면, 지금 단계에서 PARTITION BY RANGE 구조로 새 테이블을 설계하고 점진적으로 데이터를 옮기는 계획을 세워볼 시점입니다.
