희소 프라이머리 인덱스와 ORDER BY / PARTITION BY 설계
ClickHouse 공식 문서(clickhouse.com/docs) 및 기술 블로그 종합 — MergeTree부터 분산 아키텍처까지 (웹 리서치, 2026) — ClickHouse 공식 문서 + Altinity/Tinybird 등 기술 블로그 교차 확인
이 모듈을 다 읽으면
- 희소(sparse) 인덱스가 일반 B-Tree 인덱스와 어떻게 다른지 설명할 수 있다
- ORDER BY(정렬/프라이머리 키)와 PARTITION BY의 역할이 서로 다르다는 것을 판단할 수 있다
- 잘못 설계된 PARTITION BY가 왜 병합 효율을 해치는지 설명할 수 있다
ClickHouse 성능 설계에서 가장 중요한 두 결정 — 정렬/프라이머리 키(ORDER BY)와 파티션 키(PARTITION BY) — 의 역할 차이와 설계 원칙을 정리한다.
희소 프라이머리 인덱스 — 행이 아니라 그래뉼(granule) 단위
일반 RDB의 인덱스는 보통 모든 행을 개별적으로 가리킨다. ClickHouse의 프라이머리 인덱스는 '희소(sparse)'하다 — 모든 행이 아니라 그래뉼(granule, 기본 8192행 단위)마다 딱 한 행씩만 인덱싱한다. 이 인덱스는 메모리에 다 올라갈 만큼 작게 유지되며, 쿼리 시점에는 '어느 그래뉼을 통째로 읽을 필요가 없는지' 걸러내는(skip) 용도로 쓰인다 — 정확히 한 행을 찾아주는 게 아니라 '읽어야 할 그래뉼의 범위'를 좁혀주는 역할이다.
핵심 포인트
- 프라이머리 인덱스는 모든 행이 아니라 그래뉼(기본 8192행)마다 하나씩만 인덱싱 — 메모리에 다 올라갈 만큼 작다
- 정확한 행을 찾는 게 아니라 '읽지 않아도 되는 그래뉼'을 걸러내는(skip) 용도
ORDER BY — 물리적 정렬 순서를 정하는 가장 중요한 설계 결정
ORDER BY 키는 각 파트 안에서 행이 물리적으로 어떤 순서로 저장될지를 정한다(PRIMARY KEY를 따로 안 정하면 ORDER BY가 곧 프라이머리 키가 된다). 이 순서가 압축 효율과, 어떤 쿼리가 그래뉼을 잘 걸러낼 수 있는지를 결정한다. 예를 들어 ORDER BY (event_date, user_id)로 정의된 테이블에서 WHERE user_id = 12345 단독 조건은 느릴 수 있다 — 데이터가 event_date로 먼저 정렬돼 있어 특정 user_id의 행들이 여러 그래뉼에 넓게 흩어져 있고, 인덱스가 event_date 순서로만 걸러줄 수 있어 user_id 필터는 그래뉼을 거의 못 좁혀주기 때문이다(정렬키의 앞쪽 컬럼일수록 필터링 효과가 크다).
핵심 포인트
- ORDER BY = 파트 내 물리적 정렬 순서. 압축 효율과 쿼리의 그래뉼 스킵 효과를 결정하는 가장 중요한 설계
- 정렬키의 앞쪽에 없는 컬럼으로 단독 필터링하면 그래뉼을 잘 못 걸러내 느려질 수 있다
PARTITION BY — ORDER BY와 완전히 다른 역할
PARTITION BY는 정렬이나 쿼리 가속을 위한 것이 아니라, 데이터를 논리적으로 분리된 파티션(예: 월별)으로 나눠 관리 단위를 만드는 것이다. 가장 중요한 실무 규칙: **병합(merge)은 같은 파티션 안에서만 일어난다** — 파티션을 너무 잘게(고카디널리티 키, 예: user_id나 request_id로) 나누면 파트들이 서로 다른 파티션에 흩어져 병합이 거의 안 일어나고, 그 결과 '너무 많은 파트' 문제(모듈 7)로 직결된다. 그래서 파티션 키는 카디널리티가 낮아야 하며(경험칙 1000 미만), 보통 TTL 기반 보존 정책과 맞물리는 시간 단위(월별 등)로 잡는 것이 권장된다. 파티션 하나의 적정 크기는 대략 1~300GB 정도로 본다.
핵심 포인트
- PARTITION BY는 정렬/가속용이 아니라 데이터 관리 단위(TTL 삭제 등)를 만드는 것
- 병합은 같은 파티션 안에서만 일어난다 — 고카디널리티 파티션 키는 병합을 막아 파트 폭증으로 이어진다
- 파티션 키는 카디널리티 1000 미만 권장, 파티션 하나 크기는 대략 1~300GB가 적정