ClickHouse — 열 지향 분석 DB 를 속까지 · 파티션 · 讲解
파티션 — 거르는 단위이기 전에 관리하는 단위
한 줄 요약
PARTITION BY 는 행을 파티션 키 값마다 서로 다른 파트에 나눠 담게 한다. 병합은 파티션을 넘지 않으므로 한 달치 행은 언제나 그 달의 파트에만 있다. 그래서 한 달을 통째로 지우고 떼고 옮기는 일이 파일 단위로 끝난다. 쿼리가 빨라지는 것은 덤이고, 그 몫은 생각보다 작다 — 건너뛰기의 대부분은 정렬 키가 한다.
왜 이게 필요했나
로그·주문 같은 시계열 표는 결국 "오래된 것을 지운다" 는 요구를 만난다. 앞 모듈에서 본 대로 파트는 바뀌지 않는다. 행 몇 개를 지우려면 그 행이 든 파트를 새로 써야 하는데(뮤테이션), 석 달 전 자료를 지우려고 표 전체를 다시 쓰는 것은 말이 안 된다. 행이 달마다 따로 파트에 담겨 있다면 이야기가 달라진다. 7월 파트들을 표에서 떼어 버리면 끝이다.
공식 문서(Table partitions)가 파티션을 "주로 데이터 관리 기능" 이라고 부르는 이유가 이것이다. 같은 문서는 모든 파티션을 가로지르는 쿼리가 파티션 없는 표보다 대개 느리다고도 적는다. 파티션을 "인덱스처럼" 쓰려고 잘게 나누는 순간 파트 수가 폭증하고, 앞 모듈의 TOO_MANY_PARTS 로 되돌아온다.
어떻게 동작하나
INSERT 가 파티션마다 파트를 쓴다. 석 달치 150만 행을 한 번에 넣으니 파트가 202607_1_1_0, 202608_2_2_0, 그리고 9월은 블록이 둘로 잘려 두 개가 생겼다. 이름 앞의 202607 이 partition_id 다. OPTIMIZE ... FINAL 은 파티션마다 따로 합쳐 파트가 셋이 된다 — 7월과 8월을 합친 파트는 절대 생기지 않는다.
가지치기는 두 겹이다. 파트마다 파티션 키에 쓰인 열(ts)의 최솟값·최댓값이 적혀 있어서, 8월 범위로 거르면 EXPLAIN 에 이렇게 나온다.
Min-Max Keys: ts Parts: 1/3 Granules: 62/183Partition Keys: toYYYYMM(ts) Parts: 1/1Min-Max 가 7·9월 파트를 열지도 않고 버렸다. 남은 파트 안에서는 앞 모듈들의 정렬 키 인덱스가 다시 그래뉼을 고른다. 파티션 키로 바꿀 수 없는 조건(예: toDayOfWeek(ts) = 1)은 Parts: 3/3 — 아무것도 버리지 못한다.
그런데 파티션을 빼도 거의 같다. 같은 정렬 키 (region, ts) 에 파티션만 없는 표에서 같은 8월 쿼리는 581,632행을 읽었다(파티션 표는 505,454행). ts 가 정렬 키 두 번째 열이고 앞 열 region 이 다섯 가지뿐이라, 앞 모듈의 generic exclusion search 가 8월 구간을 거의 다 찾아낸다. region 과 8월을 함께 거르면 오히려 파티션 없는 표가 덜 읽었다(106,496 대 114,688). 파티션이 준 이득은 경계 그래뉼 몇 개 수준이다.
관리 작업은 파트 단위다. ALTER TABLE ... DROP PARTITION 202607 은 7월 파트를 표에서 뗀다. system.mutations 에 아무것도 남지 않고, 8·9월 파트는 이름조차 그대로다. 같은 행을 ALTER TABLE ... DELETE WHERE toYYYYMM(ts) = 202607 로 지우면 뮤테이션이 하나 생기고, part_log 에 MutatePart 가 세 번 — 7월이 하나도 없는 8·9월 파트까지 새 버전(이름 끝에 _5)이 됐다. 뮤테이션 자체는 마지막 모듈에서 깊이 다룬다.
DETACH PARTITION 은 파트를 표의 detached/ 디렉터리로 옮기고 표는 그 존재를 잊는다(system.detached_parts 에 보인다). ATTACH PARTITION 으로 되돌리면 파트가 새 블록 번호를 받아 202609_3_4_1 이 202609_5_5_0 으로 돌아왔다. ATTACH PARTITION ... FROM 다른표 는 구조가 같은 표에서 파티션을 복사해 온다 — 문서대로 원본에서도 대상에서도 지우지 않고, INSERT 쿼리 없이 파트가 그대로 넘어온다. 한 달치를 보관용 표로 떼어 두는 흔한 방법이다.
너무 잘게 나누면 막힌다. PARTITION BY (toDate(ts), region) 은 석 달이면 460개 파티션이다. 그 표에 전체를 넣으니 Too many partitions for single INSERT block (more than 100) 오류(252)로 INSERT 전체가 거절되고 한 행도 들어가지 않았다. 상한은 설정 max_partitions_per_insert_block(기본 100)이다. 오류 문장 자체가 "파티션은 SELECT 를 빠르게 하려는 것이 아니다" 라고 적고 있다.
현장에서 만나는 모습
가장 흔한 실수는 "날짜로 자주 거르니 날짜로 파티션" 이다. 하루 파티션은 1년이면 365개이고, 사용자·지역을 섞으면 수천 개가 된다. 파티션마다 파트가 따로 있고 병합은 파티션을 넘지 않으니 작은 파트가 끝없이 쌓인다. 문서(Choosing a partitioning key)는 카디널리티가 낮은 파티션 키를 고르라고 한다 — 대개 월이면 충분하다. 날짜 범위 필터는 정렬 키에 시간 열을 넣어 해결한다.
반대로 파티션이 빛나는 곳은 보관 주기다. "13개월 넘은 자료 삭제" 가 DELETE 면 매번 뮤테이션이 전체 파트를 다시 쓰지만, 월 파티션이면 DROP PARTITION 한 줄로 끝난다. 재처리가 필요한 달을 떼어 내고(DETACH) 고친 자료를 다른 표에서 채워 넣는(ATTACH ... FROM / REPLACE PARTITION) 운영도 같은 원리다.
다음 실습에서 할 것
ptn.sales 를 월 파티션으로 만들어 석 달치를 넣고 파티션별 파트를 적는다. 8월 쿼리의 EXPLAIN 에서 Min-Max 가 남긴 파트 수를 읽고, 파티션 없는 같은 정렬 키 표와 rows_read 를 비교한다. 같은 7월을 DROP PARTITION 과 DELETE 로 지워 뮤테이션 수를 비교하고, 9월을 떼었다 다시 붙여 이름이 바뀌는 것을 보고, 7월을 INSERT 없이 다른 표로 복사한다. 마지막으로 (날짜, 지역) 파티션 표가 INSERT 에서 거절되는 것을 확인한다.