[나만무] ClickHouse의 엔진Engine이란? - merge와 part

Curric·2025년 7월 13일

선요약

ClickHouse의 MergeTree 엔진은 '쓰기'와 '읽기' 성능을 모두 잡기 위한 이중 전략을 사용한다.

  1. 빠른 쓰기 (Part): 데이터가 들어오면, 일단 'Part'라는 작은 단위로 빠르고 간단하게 저장하여 쓰기 속도를 극대화.
  2. 빠른 읽기 (Merge): 이후, 백그라운드에서 이 'Part'들을 정렬하며 큰 덩어리로 'Merge'(병합)하여 읽기(쿼리) 속도를 최적화.

즉, 'Part'는 빠른 입력을, 'Merge'는 빠른 조회를 담당하는 효율적인 분업 구조.

배경

RPS 10k를 감당하고 빠른 쿼리를 위한 DB로 clickhouse를 선택했다. 그 선택의 근거가 될 수 있는 내용들을 정리해보았다.

db의 엔진이란 ?

  • db의 테이블이 디스크에 쓰이고 병합‧쿼리되는 방식을 정의하는 모듈

clickhouse의 기본값·표준엔진 = MergeTree

MergeTree 계열 테이블 엔진은 높은 데이터 수집(ingest) 속도와 거대한 데이터 볼륨을 처리하도록 설계되었다.

이 모든 성능을 뒷받침 하는 것은 Part'와 'Merge'라는 핵심 동작 원리에 있다.

데이터 삽입 작업은 '테이블 파트(table parts)'를 생성하며, 이 파트들은 백그라운드 프로세스에 의해 다른 파트들과 병합(merge)된다.

핵심 동작 원리: Part와 Merge

MergeTree의 동작 방식은

  • 일단 들어오는 짐(데이터)을 창고(테이블)에 빠르게 던져 넣고(Part 생성), 나중에 시간이 날 때 깔끔하게 정리(Merge)하는 효율적인 전략에 비유할 수 있다.

1. 빠른 쓰기(Write)를 위한 'Part' 생성

ClickHouse는 데이터 삽입(INSERT) 요청을 받으면, 기존 데이터를 수정하는 복잡한 과정을 거치지 않습니다. 대신, 새로운 데이터 덩어리인 'Part'를 생성하여 디스크에 추가하기만 한다.

  • 불변성(Immutability): 각 'Part'는 한 번 생성되면 절대 수정되지 않는 불변
  • 쓰기 성능 극대화: 이 방식 덕분에 여러 클라이언트가 동시에 데이터를 삽입해도 Lock 경쟁이 거의 발생하지 않아, 초당 수백만 건에 달하는 높은 쓰기 처리량을 달성할 수 있음.

2. 압도적인 읽기(Read) 성능을 위한 Merge

빠른 쓰기를 위해 생성된 수많은 'Part'들은 읽기 작업에는 비효율적. 쿼리 한 번에 수많은 파일을 열어야 하기 때문이다. Merge 프로세스는 이 문제를 해결한다.

  • 점진적 최적화: 백그라운드 스레드가 같은 파티션 내의 작은 'Part'들을 더 크고 효율적인 'Part'로 끊임없이 병합한다.
  • 데이터 재정렬 및 인덱싱: 병합 과정에서 데이터는 기본 키(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. 그래뉼 크기 설정

주요 구성 요소 설명

  1. ENGINE = MergeTree(): 이 테이블의 데이터 저장/처리 방식으로 MergeTree를 지정.
  2. PARTITION BY: toYYYYMM(timestamp)를 기준으로 데이터를 월별로 분리하여 저장. 쿼리에 월 조건이 포함되면 다른 월의 데이터는 아예 읽지 않아(파티션 프루닝) 성능이 향상.
  3. ORDER BY: 테이블의 기본 키(Primary Key). 데이터는 이 키를 기준으로 정렬되며, 쿼리 성능에 가장 결정적인 영향을 미친다 .
  4. SETTINGS index_granularity: 인덱스가 가리키는 최소 데이터 단위인 그래뉼의 크기를 8192행으로 설정한다.

빠른 쓰기 - part

ClickHouse는 초당 수백만 건의 데이터를 수집해야 하는 환경을 위해 설계되었다. 이를 위해 데이터 삽입(INSERT) 시의 작업을 최소화해야 한다.

  • INSERT 쿼리가 들어오면, ClickHouse는 기존 데이터를 수정하거나 복잡한 트랜잭션을 처리하는 대신, 단순히 새로운 데이터 덩어리인 '파트(Part)'를 만들어 디스크에 추가하기만 한다.
    • 이 part는 불변(Immutable)의 속성을 가진다. 이 덕분에 여러 클라이언트가 동시에 데이터를 삽입해도 서로를 기다릴 필요가 없어(Lock 경쟁 최소화) 매우 높은 쓰기 처리량을 달성할 수 있다.

결론: 쓰기와 읽기의 완벽한 분업

Part는 데이터가 들어오는 즉시 빠르게 받아 적기 위한 쓰기 최적화 전략이고, Merge는 이렇게 쌓인 데이터를 나중에 분석하기 좋은 형태로 가공하는 읽기 최적화 전략.

이처럼 쓰기와 읽기 작업을 분리하고 각 단계에 최적화된 방식을 적용함으로써, ClickHouse는 두 마리 토끼를 모두 잡는 압도적인 성능을 제공한다.

p.s.
insert batch를 키운 것도 part를 줄임으로써 merge를 최적화 하기 위한 전략

profile
curric

0개의 댓글