ClickHouse의 MergeTree 엔진은 '쓰기'와 '읽기' 성능을 모두 잡기 위한 이중 전략을 사용한다.
즉, 'Part'는 빠른 입력을, 'Merge'는 빠른 조회를 담당하는 효율적인 분업 구조.
RPS 10k를 감당하고 빠른 쿼리를 위한 DB로 clickhouse를 선택했다. 그 선택의 근거가 될 수 있는 내용들을 정리해보았다.
MergeTreeMergeTree 계열 테이블 엔진은 높은 데이터 수집(ingest) 속도와 거대한 데이터 볼륨을 처리하도록 설계되었다.
이 모든 성능을 뒷받침 하는 것은 Part'와 'Merge'라는 핵심 동작 원리에 있다.
데이터 삽입 작업은 '테이블 파트(table parts)'를 생성하며, 이 파트들은 백그라운드 프로세스에 의해 다른 파트들과 병합(merge)된다.
MergeTree의 동작 방식은
1. 빠른 쓰기(Write)를 위한 'Part' 생성
ClickHouse는 데이터 삽입(INSERT) 요청을 받으면, 기존 데이터를 수정하는 복잡한 과정을 거치지 않습니다. 대신, 새로운 데이터 덩어리인 'Part'를 생성하여 디스크에 추가하기만 한다.
2. 압도적인 읽기(Read) 성능을 위한 Merge
빠른 쓰기를 위해 생성된 수많은 'Part'들은 읽기 작업에는 비효율적. 쿼리 한 번에 수많은 파일을 열어야 하기 때문이다. Merge 프로세스는 이 문제를 해결한다.
ORDER BY) 순으로 완벽하게 재정렬된다. 정렬된 데이터는 '그래뉼(Granule)' 단위로 구성된 인덱스(Index)의 효율을 극대화하여, 쿼리 시 불필요한 데이터는 건너뛰고 필요한 부분만 정확하고 빠르게 읽어올 수 있게 한다.CREATE TABLE klicklab.events (
event_name String,
-- ... (테이블의 다른 컬럼들)
created_at DateTime DEFAULT now(),
sdk_key String
)
ENGINE = MergeTree() -- 1. MergeTree 엔진 사용
PARTITION BY toYYYYMM(timestamp) -- 2. 월 단위 파티셔닝 (쿼리 범위 축소)
ORDER BY (timestamp, client_id) -- 3. 기본 키 (데이터 정렬 및 인덱싱 기준)
SETTINGS index_granularity = 8192; -- 4. 그래뉼 크기 설정
주요 구성 요소 설명
ENGINE = MergeTree(): 이 테이블의 데이터 저장/처리 방식으로 MergeTree를 지정.PARTITION BY: toYYYYMM(timestamp)를 기준으로 데이터를 월별로 분리하여 저장. 쿼리에 월 조건이 포함되면 다른 월의 데이터는 아예 읽지 않아(파티션 프루닝) 성능이 향상.ORDER BY: 테이블의 기본 키(Primary Key). 데이터는 이 키를 기준으로 정렬되며, 쿼리 성능에 가장 결정적인 영향을 미친다 .SETTINGS index_granularity: 인덱스가 가리키는 최소 데이터 단위인 그래뉼의 크기를 8192행으로 설정한다.ClickHouse는 초당 수백만 건의 데이터를 수집해야 하는 환경을 위해 설계되었다. 이를 위해 데이터 삽입(INSERT) 시의 작업을 최소화해야 한다.
INSERT 쿼리가 들어오면, ClickHouse는 기존 데이터를 수정하거나 복잡한 트랜잭션을 처리하는 대신, 단순히 새로운 데이터 덩어리인 '파트(Part)'를 만들어 디스크에 추가하기만 한다.Part는 데이터가 들어오는 즉시 빠르게 받아 적기 위한 쓰기 최적화 전략이고, Merge는 이렇게 쌓인 데이터를 나중에 분석하기 좋은 형태로 가공하는 읽기 최적화 전략.
이처럼 쓰기와 읽기 작업을 분리하고 각 단계에 최적화된 방식을 적용함으로써, ClickHouse는 두 마리 토끼를 모두 잡는 압도적인 성능을 제공한다.
p.s.
insert batch를 키운 것도 part를 줄임으로써 merge를 최적화 하기 위한 전략