LLM WikiAccess-protected knowledge portal
← 스터디 홈
2편 · 약 11분

MergeTree 엔진 패밀리와 인덱스

PRIMARY KEY와 ORDER BY

MergeTree 테이블을 생성할 때 핵심 설정 두 가지.

  • ORDER BY: 파트 내 행이 물리적으로 정렬되는 기준. 범위 쿼리 성능과 데이터 압축 효율에 직접 영향을 준다.
  • PRIMARY KEY: 스파스 인덱스에 기록될 컬럼. 생략하면 ORDER BY와 동일하게 적용된다.
CREATE TABLE events (
    event_date Date,
    user_id    UInt64,
    event_type String,
    value      Float32
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, user_id);

ClickHouse의 PRIMARY KEY는 RDBMS처럼 유일성(uniqueness)을 강제하지 않는다. 중복을 허용하며, 정렬 기준이자 스파스 인덱스의 탐색 키로만 사용된다.

PARTITION BY는 선택 사항이다. 시간 범위로 파티션을 나누면 쿼리가 해당 파티션만 스캔해 I/O를 먼저 줄인다. 파티션 프루닝은 스파스 인덱스 스킵보다 더 앞 단계에서 일어난다.

스파스 인덱스와 그래뉼

ClickHouse는 B-Tree 행 수준 인덱스 대신 스파스(sparse) 기본 키 인덱스를 사용한다.

핵심 단위는 그래뉼(granule) — 기본값 8,192행 묶음이다.

  • 파트 내 행들은 ORDER BY 순으로 정렬된 뒤 그래뉼 단위로 분할된다.
  • 스파스 인덱스(primary.idx)는 그래뉼당 첫 행의 키 값만 저장한다.
  • 쿼리가 들어오면 인덱스를 이진 탐색해 해당 범위의 그래뉼 번호를 구하고, 그 그래뉼에 해당하는 컬럼 파일 블록만 읽는다.

10억 row 테이블이라면 인덱스 항목 수는 약 12만 개(= 10억 / 8192)다. 이 인덱스 전체가 메모리에 올라가고, 나머지 데이터는 필요한 그래뉼만 디스크에서 읽는다. B-Tree처럼 모든 row를 인덱싱하지 않아도 petabyte 규모에서 인덱스가 메모리를 압도하지 않는다.

primary.idx (그래뉼당 1 항목) gran 0: 2024-01-01, uid=1 gran 1: 2024-01-15, uid=50 gran 2: 2024-01-28, uid=12 gran 3: 2024-03-01, uid=77
→ WHERE event_date BETWEEN '2024-01-15' AND '2024-01-31'
I/O 대상 gran 1, gran 2 → 읽음 ✓ gran 0, gran 3 → 건너뜀 ✗
스파스 인덱스를 이용한 그래뉼 건너뛰기: WHERE 범위 조건이 gran 1과 2만 읽도록 좁힌다

EXPLAIN indexes = 1 SELECT ...으로 실제로 몇 개 그래뉼을 읽는지 확인할 수 있다.

보조 Skip 인덱스

ORDER BY에 없는 컬럼에도 INDEX 구문으로 스킵 인덱스를 추가할 수 있다. 스킵 인덱스는 그래뉼 단위로 "이 그래뉼에는 조건을 만족하는 행이 없다"는 사실을 미리 알려줘 I/O를 추가로 줄인다.

ALTER TABLE events
  ADD INDEX idx_type (event_type) TYPE bloom_filter(0.01) GRANULARITY 4;

주요 타입:

타입동작 방식적합한 경우
minmax그래뉼 내 최솟값·최댓값 저장숫자·날짜 범위 조건
bloom_filter집합 존재 여부 확률적 확인고카디널리티 문자열
set(N)고유값 집합 저장저카디널리티 컬럼

스킵 인덱스는 잘못 설계하면 오히려 쿼리 경로에 부담이 된다. 실제 쿼리 패턴을 먼저 분석하고 추가하는 것이 좋다.

MergeTree 패밀리

MergeTree는 다양한 파생 엔진의 기반이다. 핵심 차이는 병합 시 추가로 수행하는 동작이다.

엔진병합 시 추가 동작사용 사례
MergeTree없음 (단순 병합)일반 이벤트 로그, append-only
ReplacingMergeTree같은 ORDER BY 키 중 최신 버전(지정 컬럼 기준) 1개만 보존OLTP 미러, Upsert 시뮬레이션
SummingMergeTree같은 키의 숫자 컬럼 합산집계 중간 결과 저장
AggregatingMergeTreeAggregateFunction 상태 병합Materialized View 집계
CollapsingMergeTreesign 컬럼(+1/-1)으로 행 취소변경 이력 CDC 적용
ReplicatedMergeTreeZooKeeper/ClickHouse Keeper로 레플리카 간 동기화고가용성 운영
MergeTree (기반) → 파생
ReplacingMergeTree SummingMergeTree AggregatingMergeTree CollapsingMergeTree
+ 복제 Replicated* (위 모두 적용 가능)
MergeTree 패밀리: 기반 엔진에서 용도별로 파생, 복제는 Replicated 접두사로 추가

ReplicatedMergeTree는 독립 엔진이 아니라 복제 레이어를 얹는 방식이다. ReplicatedReplacingMergeTree, ReplicatedSummingMergeTree처럼 어떤 파생 엔진에도 조합할 수 있다.

Distributed 테이블과 2-레이어 아키텍처

운영 환경에서 ClickHouse 클러스터의 표준 구조다.

  1. 로컬 테이블: 각 샤드에 ReplicatedMergeTree를 배치. ZooKeeper/ClickHouse Keeper를 통해 같은 샤드 내 레플리카 간 데이터를 동기화한다.
  2. Distributed 테이블: 모든 샤드에 쿼리를 팬아웃하고 결과를 병합. 데이터 자체는 저장하지 않는 라우터다.
Distributed Table (라우터, 데이터 없음) ↓ 팬아웃
샤드 1 Replica A Replica B
샤드 2 Replica A Replica B
↓ 결과 병합 최종 응답
2-레이어 아키텍처: Distributed가 쿼리를 팬아웃하고 ReplicatedMergeTree 로컬 테이블이 데이터를 보유

샤딩은 수평 확장, 레플리케이션은 고가용성을 위한 것이다. 두 축을 분리해서 설계하는 것이 ClickHouse 클러스터 설계의 시작점이다.

쓰기는 Distributed 테이블에 직접 하거나(Distributed가 각 샤드로 분산), 로컬 테이블에 직접 써도 된다. 읽기는 Distributed 테이블 하나로 전 샤드를 조회한다.

한 줄 정리

ClickHouse MergeTree는 스파스 인덱스로 그래뉼 단위 I/O 스킵을, 엔진 패밀리로 용도별 병합 동작을, ReplicatedMergeTree + Distributed로 분산·고가용성을 제공한다.

References

  • https://clickhouse.com/docs/engines/table-engines/mergetree-family/mergetree
  • https://clickhouse.com/docs/primary-indexes
  • https://pulse.support/kb/clickhouse-index
  • https://medium.com/@anshs5103/clickhouse-under-the-hood-mergetree-lsm-architecture-and-disk-level-optimizations-13ea85fc684d
  • https://clickhouse.com/blog/clickhouse-25-6-coalescingmergetree
  • https://sairamkrish.medium.com/clickhouse-sharding-and-replication-95b3275c873e
  • https://oneuptime.com/blog/post/2026-03-31-clickhouse-data-skipping-sparse-indexes/view