DB Study

5장. Table Partitions — 파티션

파티셔닝은 쿼리를 빠르게 하는 마법이 아닙니다. 데이터 lifecycle을 관리하는 도구입니다 — 오해하면 실측 7.5배 느려지는 이유까지, 숫자로 확인합니다.

중급 ⏱ 약 28분 🎬 애니메이션 3개 선수 지식: 4장 Table Parts
🧭
이 장에서 배우는 것

① PARTITION BY가 INSERT를 바꾸는 방식 (파티션마다 별도 파트) ② "같은 파티션끼리만 머지"라는 절대 규칙 ③ 파티션의 진짜 용도 — TTL·tiered storage·DROP PARTITION ④ 파티션 프루닝이 통하는 조건과 역효과 실측

5.1 파티션 = 파트들의 논리적 그룹

4장의 파트가 물리적 저장 단위라면, 파티션은 파트들을 특정 기준(시간 범위, 카테고리 등)으로 묶는 논리적 상위 개념입니다. 테이블 생성 시 PARTITION BY 절로 정의합니다.

CREATE TABLE uk.uk_price_paid_simple_partitioned
(
    date   Date,
    town   LowCardinality(String),
    street LowCardinality(String),
    price  UInt32
)
ENGINE = MergeTree
ORDER BY (town, street)
PARTITION BY toStartOfMonth(date);   -- 월 단위 파티션

toStartOfMonth(date) 값이 같은 행들은 같은 파티션에 속합니다. 2020년 1월 데이터는 2020-01-01 파티션, 2월 데이터는 2020-02-01 파티션이 되는 식이죠. 시작하기 전에 이 장의 결론을 공식 문서 한 문장으로 미리 밝힙니다.

📌
이 장의 핵심 메시지

"Partitioning is primarily a data management technique and not a query optimization tool." — ClickHouse 공식 Best Practices

5.2 INSERT가 달라진다 — 파티션마다 별도의 파트

파티셔닝이 없으면 INSERT 하나는 파트 하나를 만듭니다(4장). 파티셔닝이 있으면 ClickHouse는 삽입할 행들을 먼저 파티션 키 값으로 분류한 뒤, 파티션마다 별도의 파트를 만듭니다.

INSERT 4행 (한 번에) 행1: 2020-01-05 · London 행2: 2020-01-22 · York 행3·행4: 2020-03-xx · Leeds 파티션 키 toStartOfMonth(date)로 분류 📁 파티션 2020-01-01 202001_1_1_0 (행1, 행2) 정렬→분리→압축→기록 📁 파티션 2020-03-01 202003_2_2_0 (행3, 행4) 정렬→분리→압축→기록 + date의 MinMax 인덱스 자동 생성 + date의 MinMax 인덱스 자동 생성 ⚠️ INSERT 1번 → 파트 2개. 파티션 30개가 섞이면? → 파트 30개!

5.3 절대 규칙 — 머지는 같은 파티션 안에서만

파티셔닝의 가장 중요한 동작 특성입니다.

🚨
파티션 간 머지는 절대 일어나지 않는다

ClickHouse는 같은 파티션 내의 파트만 머지합니다. 파티셔닝이 없는 테이블은 모든 파트가 머지 대상이라 소수의 큰 파트로 수렴하지만, 파티셔닝된 테이블은 파티션별로만 머지되므로 파티션 수 × 파티션당 파트 수만큼의 파트가 유지됩니다. 공식 권장: 파티션 키 카디널리티는 1,000~10,000 이하.

여기서 고카디널리티 파티션 키의 재앙이 시작됩니다. 실제로 어떻게 무너지는지 애니메이션으로 봅시다.

PARTITION BY toDate(event_time) -- 날짜 단위 ❌ 3년치 데이터 = 파티션 1,095개 📁 2024-01-01 📁 2024-01-02 📁 2024-01-03 … ×1,095 INSERT 1번에 날짜 30개가 섞이면 → 파트 30개 동시 생성 파티션 안에서만 머지 ⭕ 파티션 간 머지 ❌ 절대 불가 1,095개 파티션 × 파티션당 수십 파트 = 전체 파트 수만 개 📈 DB::Exception: Too many parts (3000+) — INSERT 거부!

5.4 파티션의 진짜 용도 — 데이터 lifecycle 관리

그럼 파티셔닝은 언제 쓰는 걸까요? 데이터를 덩어리째 관리해야 할 때입니다. 파티션 단위 작업은 행 단위 DELETE(mutation)와 달리 파트를 다시 쓰지 않는 메타데이터 수준 연산이라 수억 행도 즉시 처리됩니다.

-- ① TTL: 12개월 지난 데이터 자동 삭제 (만료 파티션을 통째로 제거)
CREATE TABLE uk.uk_price_paid_simple_partitioned ( ... )
ENGINE = MergeTree
PARTITION BY toStartOfMonth(date)
ORDER BY (town, street)
TTL date + INTERVAL 12 MONTH DELETE;

-- ② Tiered storage: 오래된 데이터를 저렴한 볼륨으로 이동 (hot/cold 분리)
TTL date + INTERVAL 12 MONTH TO VOLUME 'slow_but_cheap';

-- ③ 수동 파티션 관리 — 전부 메타데이터 수준이라 즉시 완료
ALTER TABLE mytable DROP PARTITION '2023-01-01';              -- 삭제
ALTER TABLE mytable DETACH PARTITION '2023-01-01';            -- 분리
ALTER TABLE mytable ATTACH PARTITION '2023-01-01';            -- 재연결
ALTER TABLE source MOVE PARTITION '2023-01-01' TO TABLE archive;
💡
파티션 키 선택의 3원칙

목적 먼저 — "어떤 데이터 관리 작업을 할 것인가"에서 출발 (쿼리 최적화가 목적이면 프라이머리 키·projection을 먼저 고려) ② 저카디널리티toStartOfMonth, toMonday 등으로 1,000~10,000 이하 유지 ③ 쿼리 패턴과 정합 — 대부분의 쿼리가 특정 파티션만 본다면 도움, 전체 스캔이 잦다면 해악.

5.5 파티션 프루닝 — 도움이 되는 유일한 시나리오

파티셔닝이 쿼리 성능을 도울 수 있는 경우는 조건이 모두 맞아떨어질 때뿐입니다: ① 쿼리가 소수의 파티션만 대상 (이상적으로 1개) ② 파티션 키가 프라이머리 키에 포함되지 않음 ③ WHERE 절에서 파티션 키 컬럼으로 필터. 이때 ClickHouse는 3단계로 스캔 범위를 좁힙니다.

WHERE date BETWEEN '2020-12-01' AND '2020-12-31' AND town='LONDON' 전체: 파트 436개 · granule 3,257개 월 단위 파티션 436개월치 데이터 ① MinMax 인덱스: date 범위 밖 파트 제거 Parts: 1/436 · Granules: 11/3257 ② 파티션 키 조건 확인 Parts: 1/1 · Granules: 11/11 ③ Primary index: town='LONDON' Parts: 1/1 · Granules: 1/11 최종 스캔: 8,192행 · 6ms ⚡
-- 프루닝 각 단계를 눈으로 확인
EXPLAIN indexes = 1
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple_partitioned
WHERE date >= '2020-12-01' AND date <= '2020-12-31'
  AND town = 'LONDON';

-- MinMax:     Parts: 1/436,  Granules: 11/3257
-- Partition:  Parts: 1/1,    Granules: 11/11
-- PrimaryKey: Parts: 1/1,    Granules: 1/11

5.6 역효과 실측 — 파티션 키로 필터하지 않으면

같은 데이터, 같은 쿼리를 파티셔닝 유무만 바꿔 실측한 공식 문서의 비교입니다. 쿼리는 WHERE town = 'LONDON' — town은 프라이머리 키에는 있지만 파티션 키가 아니므로 파티션 프루닝이 작동하지 않습니다.

지표파티셔닝 없음파티셔닝 있음
활성 파트 수1 (완전 머지됨)436
스캔한 파트1/1431/436
스캔한 granule241/3,083671/3,257
처리 행 수약 197만약 548만
소요 시간12ms90ms (7.5배 느림)
피크 메모리62 MiB163 MiB (2.6배)

느려진 이유는 명확합니다. 파티셔닝 없는 테이블은 하나의 큰 파트로 머지되어 primary index가 최대 효율로 작동하는 반면, 파티셔닝된 테이블은 431개 파트를 각각 열어 인덱스를 검사해야 하고, 파트마다 인덱스가 독립적이라 프루닝 효율도 떨어집니다.

⚠️
흔한 실수 3가지

PARTITION BY date — 일 단위 고카디널리티 폭탄 ② 프라이머리 키로 충분한데 파티셔닝 추가 — 불필요한 복잡성 ③ 파티션 키를 프라이머리 키에도 포함 — 이미 PK로 필터 가능하므로 프루닝 이득이 사라짐

5.7 파티션 모니터링

-- 파티션별 파트 수·행 수 확인
SELECT partition, count() AS parts, sum(rows) AS rows
FROM system.parts
WHERE database = 'uk'
  AND `table` = 'uk_price_paid_simple_partitioned'
  AND active
GROUP BY partition
ORDER BY partition;

-- 파티셔닝 건강 진단: 평균 파트/파티션과 총 파티션 수
SELECT
    count() AS total_active_parts,
    countDistinct(partition) AS total_partitions,
    total_active_parts / total_partitions AS avg_parts_per_partition
FROM system.parts
WHERE database = 'mydb' AND `table` = 'mytable' AND active;
-- total_partitions가 수천 이상이면 파티션 키 재검토!

✍️ 이해도 체크

ClickHouse 파티셔닝의 주된 용도로 공식 문서가 강조하는 것은?
✅ "Partitioning is primarily a data management technique and not a query optimization tool." 파티션 단위 DROP/DETACH/MOVE는 메타데이터 수준 연산이라 수억 행도 즉시 처리됩니다. 쿼리 최적화는 프라이머리 키의 몫이며, 파티션 키로 필터하지 않는 쿼리는 오히려 느려집니다(실측 7.5배).
PARTITION BY toDate(event_time)(일 단위)로 3년치 데이터를 운영하면 위험한 이유는?
✅ 3년치 일 단위 파티션은 1,095개. 파티션 간 머지는 절대 불가능하므로 각 파티션의 작은 파트들이 정리되지 못하고 전체 파트 수가 폭증합니다. parts_to_throw_insert(기본 3000)를 넘으면 INSERT가 거부됩니다. toStartOfMonth로 바꾸면 36개 파티션으로 끝납니다.
📚
원문으로 더 깊이

이 장의 원문 문서: chapters/ch05_table_partitions.md — 파티셔닝 유무 실측 비교의 전체 수치, 파티션 키 선택 원칙, 진단 쿼리 모음이 담겨 있습니다.