아키텍처 개요 (Academic Overview)

아키텍처 개요 (Academic Overview)

ClickHouse 분류
Core Architecture
Type
Research
작성자

Ken

각 섹션에서는 ClickHouse 공식 Academic/Architecture Overview 문서와 최신 자료를 바탕으로 개념을 작성하였습니다.

image
  • 1. MergeTree 기반 스토리지 구조
  • 2. 컬럼 기반 저장 포맷 (.bin / .mrk2 파일 구조)
  • 3. 쿼리 실행 엔진 설계 (벡터화 처리 및 실행 파이프라인)
  • 4. 메모리 관리 구조 (Arena Allocator, Spill, QueryContext)
  • 5. 분산 쿼리 처리 구조 (Distributed 테이블, 샤딩, 복제, ZooKeeper/Keeper)
  • 6. 백그라운드 작업 (머지, 뮤테이션, TTL, Fetch 등)
  • 7. 데이터 정합성 및 지속성 구조 (일관성 모델과 Atomic DDL)
  • 8. ClickHouse의 옵티마이저 및 쿼리 재작성 전략
  • 9. 최신 기술 동향 및 기능 (Iceberg 연동, SharedMergeTree, Lazy Materialization 등)

1. MergeTree 기반 스토리지 구조

MergeTree는 ClickHouse의 핵심 컬럼스토어 테이블 엔진 패밀리로, 대량 데이터의 효율적 저장과 조회를 위해 고안되었습니다. MergeTree의 주요 아이디어는 immutable sorted parts백그라운드 머지(merge)입니다. 데이터를 테이블에 INSERT하면 일정량의 행(batch)이 모여 하나의 "파트(part)" 파일 집합으로 저장되고, 각 파트 내부에서는 미리 정의된 프라이머리 키(primary key) 순서로 행들이 정렬됩니다. 이러한 파트는 불변(immutable)이며, 시스템은 백그라운드에서 다수의 작은 파트를 주기적으로 병합(merge)하여 더 큰 파트를 생성함으로써 파트 개수를 제어하고 조회 성능을 최적화합니다. (이렇게 여러 파트를 합치는 동작 때문에 "MergeTree"라는 이름이 붙었습니다.)

  • Part와 Partition: 각 MergeTree 파트는 디스크상의 하나의 디렉토리로 구현되며, 해당 파트에 속한 모든 컬럼 파일들과 메타데이터(기록 파일, 마크 파일 등)를 포함합니다. 파트는 종종 파티션(partition)이라는 논리적 그룹으로 조직될 수도 있는데, 예를 들어 날짜 단위로 파티션 키를 지정하면 같은 달의 데이터들이 동일한 파티션 디렉토리 아래 여러 파트로 나뉘어 관리됩니다. 그러나 여기서 말하는 파트(part)는 물리적인 데이터 조각 그 자체를 가리킵니다 (각 파트는 특정 파티션에 속할 수 있습니다).
  • Primary Key와 정렬: MergeTree 테이블은 프라이머리 키로 지정된 하나 또는 복수의 컬럼(튜플) 기반으로 각 파트 내부가 정렬되어 저장됩니다. 이 정렬된 구조 덕분에, 범위 조회나 순서 기반 연산에서 성능 향상을 얻을 수 있습니다. 다만 MergeTree의 primary key는 테이블 전역에서 유일성을 보장하지는 않으며, 키 충돌이 있을 경우도 허용됩니다 (한 파트 내에서도 동일 키 값을 가진 여러 행이 있을 수 있음). Primary key는 단지 데이터 클러스터링(군집화)에 사용될 뿐, 고유 인덱스로 작용하지는 않습니다.
  • Granule(그라뉼)과 Index: MergeTree에서는 인덱스 그라뉼(index granularity)이라는 개념으로 인덱스 간격을 정의합니다. 기본값으로 8192행당 하나의 그라뉼을 구성하며, 각 파트마다 별도의 희소 인덱스(sparse index) 파일 (primary.idx)이 존재하여 각 그라뉼의 첫 번째 행의 프라이머리 키 값을 저장합니다. 예컨대 기본 설정에서 8192번째 행마다 그 키 값을 기록해 두므로, 인덱스는 모든 행을 가지지 않고 N행마다 한 번씩만 키 정보를 갖는 희소 구조입니다. 덕분에 인덱스 크기를 매우 작게 유지할 수 있어 (수조 행이라도 메모리에서 관리 가능) 메모리 부담을 줄입니다. 하지만 이로 인해 인덱스는 개별 행을 정확히 가리키진 못하고 그라뉼 단위로만 접근하므로, point query(특정 키 한 건 조회)에는 비효율적일 수 있습니다 – 요청된 키가 속한 8192개 행 블록 전체를 읽어야 하고, 해당 블록의 모든 컬럼 압축블록을 풀어서 확인해야 하기 때문입니다. 다시 말해, 희소 인덱스로 메모리를 아끼는 대신, 필요 시 불필요한 행 일부를 추가 읽는 트레이드오프를 택한 것입니다.
  • 데이터 읽기 시 인덱스 활용: SELECT 질의를 통해 데이터를 읽을 때 ClickHouse는 각 파트의 primary.idx (메모리에 상주)에 접근하여 원하는 키 또는 범위에 해당할 가능성이 있는 그라뉼들의 범위를 계산합니다. 이후 각 대상 컬럼의 column.mrk 파일(마크 파일)을 참조하여 해당 그라뉼 시작 지점의 오프셋을 알아낸 뒤, 그 오프셋부터 해당 컬럼 .bin 파일의 데이터 블록을 읽고 디스크에서 역직렬화합니다. 마크 파일에는 각 그라뉼에 대해 "해당 그라뉼 시작시점의 압축파일 오프셋 + 압축해제 후 바이트 오프셋" 정보가 기록되어 있어서, 쿼리 엔진이 정확히 어느 위치부터 읽어야 할지 계산할 수 있습니다. 일반적으로 압축 블록 경계와 인덱스 그라뉼 경계는 일치하도록 기록되므로(decompressed offset이 0인 경우), 마크를 이용해 곧바로 필요한 블록의 압축 데이터를 가져올 수 있습니다. 이러한 과정을 통해 ClickHouse는 인덱스된 컬럼으로 데이터 범위를 대폭 "pruning"(잘라내기)하여 디스크 I/O와 디코딩 작업을 줄입니다. 단, 희소 인덱스 특성상 정확한 범위보다 약간 초과하여 읽는 오버헤드는 존재하며, 이는 OLAP 시나리오(대량 데이터를 한꺼번에 스캔하는 분석 쿼리)에서는 큰 문제가 아니지만, OLTP 스타일의 자주 반복되는 단건 조회에는 부적합합니다.
  • TTL(Time-To-Live) 및 데이터 수명: MergeTree 엔진은 TTL 규칙을 통해 데이터의 수명 주기 관리를 자동화할 수 있습니다. 테이블 생성시 TTL 절을 사용하면, 각 행 혹은 파트에 TTL 타임스탬프를 부여해 일정 시간이 지나 만료된 데이터 행을 자동 삭제하거나 다른 볼륨으로 이동할 수 있습니다. TTL이 지난 데이터의 삭제는 백그라운드 머지 작업 중에 이루어집니다. 예컨대 행 단위 TTL 삭제의 경우, MergeTree는 백그라운드에서 해당 파트를 머지할 때 만료된 행들을 거르고 새로운 파트를 써넣는 방식으로 "삭제"를 수행합니다. 또한 TTL 규칙으로 특정 기간 지난 데이터는 저렴한 스토리지로 자동 이동(TO DISK/VOLUME)하거나, 오래된 파트를 재압축/집계하는 등의 동작도 지정할 수 있습니다. 모든 TTL 처리는 lazy drop 원칙에 따라 eventually 하게 일어나며 (만료 즉시 바로 지우기보다는, 약간 지연되어 배치 처리), merge_with_ttl_timeout 설정으로 TTL 머지 빈도를 조절할 수도 있습니다. 또한 ttl_only_drop_parts 설정을 통해 파트 전체가 TTL 만료된 경우 통째로 파트를 날리는 최적화도 가능합니다. 이러한 TTL 메커니즘은 클릭하우스에서 데이터 라이프사이클 관리를 단순화하여, 오래된 데이터의 자동 축출 및 스토리지 비용 최적화를 지원합니다.
  • 기타 MergeTree 파생 엔진: MergeTree 패밀리에는 기본 MergeTree 외에도 CollapsingMergeTree, ReplacingMergeTree, SummingMergeTree, AggregatingMergeTree 등 여러 변종이 있습니다. 이들은 백그라운드 머지 과정에서 추가적인 데이터 처리를 수행하도록 설계되었는데, 예를 들어 CollapsingMergeTree는 동일 키의 중복 레코드 중 최신 하나만 머지 시 남기고 이전 레코드를 제거함으로써 update 효과를 내고, AggregatingMergeTree는 미리 지정된 집계 함수의 중간 상태를 모아 같은 키별로 부분 집계 결과를 합산하며, ReplacingMergeTree는 동일 키에 대해 마지막 레코드만 남기는 등 머지 단계에서 유사 업데이트 기능을 제공합니다. 이러한 엔진들은 실시간 갱신보다는 머지를 통한 eventual consistency를 추구하는 ClickHouse의 철학에 맞춰 설계된 것으로, 사용자가 UPDATE/DELETE를 자주 하지 않고 주기적 batch insert + 머지를 통해 데이터를 정제하는 워크로드에 적합합니다.

정리: MergeTree 스토리지 구조는 정렬된 불변 파트희소 인덱스 개념으로 대규모 컬럼 지향 데이터 저장을 가능케 합니다. 이는 쓰기 시 작은 batch를 신속히 받아들이고, 읽기 시에는 프라이머리 키 기반으로 대량 데이터를 prune하여 효율을 얻는 설계입니다. 백그라운드에서는 자동 머지와 TTL 삭제 등이 이루어져 스토리지 최적화와 데이터 수명 관리가 투명하게 진행됩니다. 이러한 구조 덕분에, 수십억~조 단위 행을 가진 테이블도 한 서버에서 운영할 수 있는 고성능 OLAP 데이터베이스로 ClickHouse가 자리매김하게 되었습니다.

2. 컬럼 기반 저장 포맷 (.bin / .mrk2 파일 구조)

ClickHouse는 철저한 컬럼 지향(column-oriented) 저장 구조를 따릅니다. 각 MergeTree 파트 디렉토리 내에는 테이블의 각 컬럼별로 별도 파일들이 존재하며, 확장자 .bin (혹은 엔진 버전에 따라 .bin/.bin2)인 컬럼 데이터 파일과, 이에 대응하는 .mrk (또는 .mrk2) 확장자의 마크(marks) 파일로 구성됩니다. 이 섹션에서는 컬럼 파일의 내부 포맷마크 파일 구조, 그리고 압축 및 I/O 동작에 대해 설명합니다.

  • 컬럼 .bin 파일 (Column Data File): .bin 파일은 해당 컬럼의 모든 행 값들을 순서대로 저장한 바이너리 파일입니다. 이 파일은 성능을 위해 다수의 작은 압축 블록들로 구성되어 있는데, 일반적으로 한 압축 블록당 64KB~1MB 가량의 비압축 데이터 (원본 행 값 기준) 크기를 갖습니다. 각 압축 블록에는 해당 컬럼의 일정 범위 (예: 여러 그라뉼) 값들이 연속적으로 나열되어 있으며, 기본 압축 알고리즘은 LZ4 (빠른 디코딩이 장점)이고 사용자가 필요에 따라 ZSTD, ZLIB 등의 코덱을 지정하거나 복합 코덱(예: Delta+LZ4)도 적용할 수 있습니다. 중요한 점은 컬럼별로 데이터가 분리되어 있으므로, 쿼리 시 특정 컬럼만 참조하면 해당 컬럼 파일만 읽게 되고, 필요 없는 컬럼의 I/O는 완전히 생략된다는 것입니다. 또한 같은 컬럼의 값들이 인접해 저장되므로 높은 압축률을 보입니다 (특히 값 분포가 정렬 등에 의해 군집화되어 있으면 압축 효율 극대화). 예를 들어, 733MB의 원본 데이터가 207MB로 압축되어 디스크에 저장되었다는 보고가 있습니다.
  • 마크 .mrk2 파일 (Marks File): 각 컬럼에 대응하는 .mrk/.mrk2 파일은 위의 .bin 데이터에서 인덱스 그라뉼 경계에 해당하는 위치를 기록해 둔 파일입니다. 마크(mark) 하나가 하나의 그라뉼을 나타내며, 보통 8192행(혹은 adaptive granularity 사용 시 가변 행 수)마다 한 마크가 있습니다. 마크에는 두 개의 숫자 쌍이 저장되는데: (1) 해당 그라뉼 시작 행이 위치한 압축 블록의 파일 오프셋, (2) 그 압축 블록 내에서의 오프셋입니다. 일반적으로 하나의 그라뉼이 하나의 압축 블록 경계와 일치하면 두 번째 오프셋은 0이지만, 경우에 따라 한 압축 블록 안에 여러 그라뉼이 들어갈 수 있고 그때는 블록 내 오프셋이 0이 아닐 수 있습니다. 이러한 마크 파일은 쿼리 수행 시 정확한 읽기 시작 지점을 알려주는 역할을 합니다. 쿼리 엔진이 특정 그라뉼부터 데이터를 읽고 싶을 때, column.mrk에서 해당 마크를 찾아 .bin 파일의 특정 바이트 오프셋으로 시크(seek) 한 뒤, 거기서부터 필요한 블록을 읽어들이게 됩니다. ClickHouse에서는 프라이머리 키 희소 인덱스 (primary.idx)로 후보 그라뉼 범위를 정한 후, 각 컬럼의 마크를 사용해 디스크 접근 범위를 산출하는 이중 단계의 인덱싱을 사용하고 있습니다. 마크 파일은 OS 페이지캐시 등에 의해 주로 메모리에 캐시되어 빠르게 접근되며, 그 크기는 인덱스 그라뉼당 몇 바이트 수준이므로 매우 작습니다 (예: 887만 행에 1083개 마크엔트리, 약 97KB).
  • 예시 – on-disk 구조: 예를 들어, UserID UInt64, URL String, EventTime DateTime 컬럼이 있는 테이블을 생각해 봅시다. UserIDURL을 프라이머리 키로 정의하고 EventTime까지 포함해 sorting key로 삼았다면, 한 파트의 디렉토리 구조는 다음과 같습니다 (wide 포맷 가정):
  • .bin 파일은 해당 컬럼의 값 0번 행부터 N-1번 행까지 프라이머리 키 정렬 순서로 쭉 기록해두고, .mrk2는 예를 들어 0번, 8192번, 16384번, ... 번째 행이 각각 .bin 파일의 어디 위치에 있는지를 기록해 둔다고 볼 수 있습니다. primary.idx는 0번, 8192번, ... 행의 (UserID, URL) 키 값을 순서대로 담고 있습니다. 따라서 쿼리에서 "WHERE UserID = 12345" 같은 조건이 들어오면, primary.idx를 이분 탐색하여 12345가 들어갈 수 있는 그라뉼 범위를 찾아내고 (예를 들어 mark 50~mark 55 사이), 그 다음 각 컬럼 .mrk2를 참고해 mark 50과 55에 해당하는 오프셋을 얻어, 그 사이에 위치한 블록들만 각 .bin 파일에서 읽는 식입니다. 이때 URL처럼 where에 직접 쓰이지 않은 컬럼은 일단 읽지 않을 수 있고 (엔진이 미리 읽을 컬럼늦게 읽을 컬럼을 조정함, 이는 Lazy Materialization에서 추가 논의), 필요한 컬럼만 최소한으로 읽게 됩니다.
  • 소트 및 압축 최적화: MergeTree는 정렬된 저장컬럼별 압축 덕에, 디스크 공간 효율과 I/O 효율이 모두 높습니다. 특히 여러 행이 primary key에 따라 정렬되어 붙어 있으므로, 비슷한 값들이 근처에 위치하게 되어 압축률이 향상됩니다. 또한 ClickHouse는 파일 포맷 레벨에서 데이터 타입별 특화 저장을 부분적으로 활용합니다. 예를 들어 LowCardinality(String) 같은 타입은 사전+인덱스 방식을 통해 .bin 파일에 값을 효율적으로 저장하고, Map 같은 복잡 타입도 내부적으로 키/값을 별도 컬럼 세그먼트로 분리해 저장합니다. 다만 저장 포맷은 주로 "일반적인 컬럼ar 파일 + 마크" 구조를 따르며, 고정된 프레임(데이터 블록) 안에서 압축이 이루어집니다. ClickHouse는 자체 바이너리 프로토콜로 .bin을 저장하기에, 별도의 C++ 라이브러리를 거의 전체 엔진에 링크하지 않고서는 외부에서 쉽게 읽을 수 없지만, 이것이 성능상의 trade-off입니다 (사용자는 SELECT ... INTO OUTFILE 등의 기능으로 데이터를 내보낼 수 있습니다).
  • Adaptive Granularity: 참고로 인덱스 그라뉼 크기를 적응적(adaptive)으로 운영하는 옵션도 있습니다. Adaptive mode에서는 각 마크 간격을 고정 행 수가 아닌 고정 바이트 크기 기준(예: 10MB)으로 조절합니다. 즉 8192행이 되기 전에 데이터 크기가 일정 임계치를 넘으면 그 시점에 마크를 찍는 방식으로, 카디널리티나 압축률에 따라 그라뉼 당 행 수가 달라질 수 있습니다. 이 모드의 장점은 하나의 그라뉼이 너무 비대해지는 것을 막아주는 것이지만, 일반적으로 기본 모드(8192행 고정 + 10MB 제한 혼합)가 많이 사용됩니다. Adaptive 모드에서는 마지막에 한 개 "final" 마크가 추가로 들어갈 수 있음을 주의해야 합니다.

요약하면, ClickHouse의 컬럼 저장 포맷은 각 컬럼별 별도 파일로 값을 저장하고, 마크 파일로 블록 위치를 인덱싱하는 구조입니다. 이러한 설계를 통해 불필요한 컬럼을 완전히 건너뛸 수 있고, 필요한 데이터만 정확히 찾아 읽는 실용적인 I/O 최적화를 달성합니다. 고효율 압축과 결합되어, 디스크 사용량을 최소화하면서도 CPU 효율을 높이는 구조입니다.

3. 쿼리 실행 엔진 설계 (벡터화 처리 및 실행 파이프라인)

ClickHouse의 쿼리 엔진은 현대 OLAP DBMS의 트렌드를 따른 벡터화(vectorized) 실행 모델을 중심으로 설계되어 있습니다. 이는 한 번에 하나의 행을 처리하는 전통적인 Volcano 모델(예: MySQL, PostgreSQL 등)과 달리, 한 번에 수천~수만 행의 "벡터" 단위로 연산을 수행하는 방식입니다. 또한 ClickHouse는 쿼리 실행 단계를 파이프라인(pipeline)으로 구성하여, 가능한 많은 연산을 병렬화하고 CPU의 SIMD 연산을 적극 활용합니다. 이 섹션에서는 ClickHouse의 쿼리 실행 흐름벡터화 및 병렬화 기법, 그리고 Expression DAG/Query Plan 구조에 대해 설명합니다.

  • 쿼리 실행 단계 개요: 사용자가 보낸 SQL 쿼리는 우선 파싱(parsing)되어 AST(추상 문법 트리)로 변환되고, 분석 및 최적화 단계를 거칩니다. ClickHouse 엔진은 AST를 기반으로 논리 쿼리 플랜을 생성하고, 다양한 규칙 기반 최적화(rule-based optimizations)를 수행합니다. 예를 들어 상수 폴딩(1+2를 3으로 계산), 필터 푸시다운(가능한 조건을 데이터 소스 가까이 이동), 공통 서브표현식 제거 등이 이 단계에서 이루어집니다. 또한 OR 조건을 IN 리스트로 변환하거나 JOIN을 재구성하는 등의 쿼리 재작성도 AST/논리 수준에서 수행됩니다. 다음으로, 물리 쿼리 플랜이 수립되는데, 이때 ClickHouse는 테이블 엔진의 특성을 고려해 최적화합니다. 예컨대 대상이 MergeTree 표이면 이미 정렬되어 있으므로, 쿼리의 ORDER BY가 테이블의 primary key 접두사와 일치할 경우 별도의 정렬 연산을 스킵하며, GROUP BY 키가 정렬키와 맞으면 해시 집계 대신 정렬 기반 집계(sort-based aggregation) 알고리즘을 택하는 등 특화 최적화를 합니다. 마지막으로 실행 계획을 병렬 파이프라인으로 펼쳐서, 각 연산(스캔, 필터, 조인, 집계 등)을 여러 스레드에 분배하고 쿼리 실행을 시작합니다.
  • Vectorized Execution (벡터화 실행): ClickHouse의 모든 연산자는 Block(블록) 단위로 데이터를 주고받습니다. 하나의 Block은 여러 컬럼의 columnar data를 동일한 크기의 배열(vector)로 묶은 것으로, 예컨대 1000개의 행을 담은 Block은 각 컬럼에 1000개씩의 값 배열을 가집니다. 연산 함수들은 이 Block을 입력받아 내부 루프에서 1000개 원소를 한꺼번에 계산하는 식으로 구현됩니다. 이로써 함수 호출 및 분기 오버헤드가 줄고, CPU 캐시 효율이 극대화됩니다. 특히 동일 연산을 여러 데이터에 적용하는 CPU SIMD 명령어를 활용하기에 용이하며, 메모리 계층(Locality)을 활용해 연속된 메모리 접근으로 속도를 높입니다. 예를 들어 WHERE절에 col1 > 100 AND col2 != '' 같은 조건이 있으면, col1의 1024개 값 벡터에 대해 한 벡터 연산으로 비교하고, col2에 대해서도 벡터화된 비교를 한 후, 결과 bitmask로 결합해 한 번에 필터링하는 방식입니다. 전통적인 행 단위 실행과 비교해 벡터화 모델은 약간의 임시 메모리 사용(벡터 버퍼)이 늘지만, 함수 호출/분기 감소로 인한 CPU 파이프라인 효율 이득이 훨씬 큽니다. 참고로 ClickHouse는 VectorWise 등 과 마찬가지로 이 방식을 채택했으며, Postgres/MySQL 등의 row-by-row 실행과 대비됩니다. 연구에 따르면 벡터화와 JIT 코드를 결합한 하이브리드 방식이 가장 효율적이므로, ClickHouse도 기본은 벡터화하되 일부 상황에서 LLVM JIT 컴파일을 도입하고 있습니다.
  • Expression Analysis DAG: ClickHouse의 쿼리 분석기는 복잡한 SELECT문의 계산식들을 DAG(Directed Acyclic Graph) 형태로 표현하여 실행 순서를 최적화합니다. 전통적으로 ClickHouse에서는 ExpressionAnalyzerExpressionActions 구조가 AST를 분석해, "액션 노드"들의 DAG을 생성하고, 이것을 실행 플랜에 반영해 왔습니다. 이 DAG의 노드들은 개별 함수 적용, 컬럼 읽기, 필터, 산출 컬럼 등으로 구성되어 연산간의 의존성을 표현합니다. 예를 들어 SELECT avg(length(URL)) FROM hits WHERE URL != ''라는 쿼리가 있으면, 이를 실행하기 위한 DAG는 대략 "Read URL -> compute URL != '' -> Filter -> compute length(URL) -> aggregate avg -> output" 같은 그래프로 표현됩니다. 이 그래프를 토대로 QueryPipeline이 구성되고, 각 노드가 파이프라인의 연산자(operator)로 구현됩니다. 최신 버전의 ClickHouse에서는 enable_analyzer 설정 하에 새로운 InterpreterSelectQueryAnalyzer가 도입되어, 기존 ExpressionAnalyzer를 대체하고 더 체계적인 QueryPlan & QueryTree 기반 최적화를 수행합니다. 이 신규 분석기는 모듈화된 규칙 적용을 지원하여 옵티마이저의 유연성을 높이고 있습니다 (예: 별도 클래스들로 푸시다운, 조인 재배치 등의 패스 구현). 정리하면, Expression DAG/QueryPlan 구조를 통해 ClickHouse는 복잡한 쿼리를 효율적인 실행단위로 쪼개어 표현하고, 병렬 스케줄링메모리 재사용 최적화를 용이하게 합니다.
  • 파이프라인 및 병렬 실행: ClickHouse의 물리적 실행계획은 Push/Pull 기반의 혼합 파이프라인 모델로 동작합니다. SELECT 쿼리의 경우 Pull 모델로, 상위 연산자가 하위 연산자에게 "데이터를 주세요"라고 당겨오는 형식(IBlockInputStream 인터페이스)을 사용하며, INSERT같이 데이터를 넣는 작업은 Push 모델로 스트림에 블록을 밀어넣는 방식(IBlockOutputStream)으로 동작합니다. 이러한 스트림들은 Block 단위로 데이터를 흘려보내며, 내부적으로 여러 Execution Thread로 병렬 처리됩니다. ClickHouse 서버는 멀티 스레드로 설계되어, 쿼리 수행 시 여러 스레드가 서로 다른 파티나 데이터 범위를 병렬로 처리합니다. 예를 들어 큰 MergeTree 파트 여러 개가 있다면, 각 파트를 읽는 작업을 다른 스레드에 할당하여 병렬 스캔하고, 중간 결과를 합칩니다. 또 하나의 큰 파트라도 내부를 분할(chunk)하여 다중 스레드로 나눠 처리할 수 있습니다. 이때 Repartition/Exchange 연산자가 쓰여, 필터 이후 불균형하게 남은 데이터를 다시 스레드 간에 재분배하여 부하를 고르게 하는 기능도 있습니다. ClickHouse 엔진은 파이프라인의 각 operator를 비동기 상태 기계처럼 구현하여, 한 operator가 I/O 대기 중이면 다른 operator 스레드가 실행되는 식으로 CPU 유휴를 최소화합니다. 또한 코어 친화적 스케줄링으로, 한 스레드가 맡은 데이터 파이프라인 segment는 가능하면 같은 CPU 코어에서 유지하여 캐시 적중률을 높이는 최적화도 들어가 있습니다. 기본적으로 쿼리 파이프라인은 "분할 정복": 로컬에서는 파티션/파트를 나눠 병렬 처리하고, 최종적으로 모여서 merge/join/aggregate를 수행합니다.
  • LLVM JIT 및 런타임 최적화: Vectorized 엔진에도 불구하고, ClickHouse는 일부 컴퓨팅 집약적인 부분에 JIT 코드생성(LLVM)을 도입하여 추가 성능 향상을 꾀합니다. 예를 들어 복잡한 표현식 계산, 다중 키 정렬, 복잡한 사용자 정의 함수 등이 반복될 경우, 런타임에 해당 연산들을 하나의 JIT 함수로 컴파일하여 템플릿/가상함수 오버헤드를 줄이고 CPU 파이프라인 친화적인 코드로 변환합니다. ClickHouse는 여러 연산을 퓨즈(fuse)한 머신 코드를 생성함으로써 루프 내 분기와 함호출을 최소화하고, 레지스터와 캐시 활용을 최적화합니다. 이 컴파일 결과는 캐시에 저장되어 동일한 패턴의 쿼리가 다시 오면 재사용됩니다. 다만, JIT는 초기 컴파일 오버헤드가 있어 매우 자주 실행되는 패턴에만 적용되며, 대부분의 일반 쿼리는 기본 벡터화 처리로도 충분히 빠르게 수행됩니다.
  • Aggregation 파이프라인: OLAP의 핵심 연산인 GROUP BY 집계를 ClickHouse는 고도로 최적화했습니다. 기본 방식은 해시 테이블 기반 집계(hash aggregation)이며, GROUP BY 키 종류와 크기에 따라 30여 가지 이상의 특수화된 해시 테이블 구현 중 하나를 선택합니다. 예를 들어, 키가 64비트 숫자라면 해당 타입에 최적화된 폐쇄 해시맵을, 키 개수가 작을 것 같으면 고정 크기 배열 다이렉트 매핑을, 매우 큰 경우 2단계 해시(two-level hash) 구조를 사용하는 식입니다. 심지어 문자열 키에 특화된 open addressing 테이블, tuple 키에 대해 hash 충돌 최소화를 위한 embedded hash 기술 등 세세한 튜닝이 내장되어 있습니다. 또한 두 단계 집계 기법도 활용합니다: 키 카디널리티가 매우 높아 한 노드 메모리에 다 담기 어려운 경우, 먼저 각 스레드별로 부분 집계를 수행하고 (메모리 usage를 줄이기 위해 2-level 구조: 첫 번째 레벨은 여러 소해시, 두 번째 레벨에서 merge), 마지막에 이를 merge 하는 방식입니다. 필요하면 데이터 분산 시 shard 간 병렬 집계도 일어나고, 최종 결과를 initiator 노드에서 combine 합니다 (분산 처리 자세한 설명은 다음 섹션 참고). 이러한 설계로 ClickHouse는 수억 건 이상의 그룹핑도 비교적 작은 메모리로 빠르게 계산해냅니다. 다만, 만약 GROUP BY 키가 테이블의 primary key와 겹쳐 정렬이 이미 되어 있다면, ClickHouse는 앞서 언급했듯 해시 대신 Sort + 스트리밍 집계를 사용하여 메모리 사용량을 낮출 수 있습니다 (이 경우 대량 키라도 정렬된 순차로 들어오므로 한 번에 하나 키씩 결과를 바로 내보낼 수 있음).
  • JOIN 처리: ClickHouse는 OLAP DB로서 JOIN 연산도 지원하지만, 기본 철학은 denormalization (테이블 미리 결합하여 중복 데이터를 허용)을 권장할 정도로, JOIN 최적화는 제한적입니다. 그럼에도 현재 병렬 해시 조인Merge join, 첨부(Index) 조인 등을 구현하고 있습니다. 해시 조인은 주로 작은 테이블을 메모리에 해시테이블로 만들고 큰 테이블을 스캔하며 probe하는 방식이며, 멀티스레드로 build/probe 단계를 파티셔닝하여 실행합니다. 대용량 테이블 간 조인을 위해 양쪽이 조인 키로 정렬되어 있을 때는 sort-merge join을 사용하기도 합니다. 또한 ClickHouse 사전 테이블(Dictionary) 기능을 이용한 키-값 조인을 최적화한 Dictionary Join (연관 배열 조회처럼 동작)도 존재합니다. 하지만 코스트 기반 최적화(CBO)에 의한 조인 순서 재배치나, 복잡한 조인 전략 탐색은 아직 제한적입니다. 사용자가 힘든 조인 쿼리를 작성하면, ClickHouse는 실행시 메모리 부족이나 성능 문제를 일으킬 수 있고, 이런 경우 JOIN 해시테이블의 외부 메모리 스필이나 Bloom Filter 푸시다운 등의 기능이 활용되기도 합니다. (예: 2024년 이후 버전에 조인 처리도 개선이 진행 중). 요약: ClickHouse의 JOIN 엔진은 기본 해시조인 + 병렬화로 최적화되어 있지만, 다중 테이블 조인을 빈번히 하는 전통 OLTP와는 지향점이 달라 star schema에서 사실상 dimension 테이블을 미리 조인한 wide table 운용을 권장하는 편입니다.

요약: ClickHouse의 쿼리 실행 엔진은 벡터화된 병렬 처리를 통해 CPU와 IO 성능을 극대화합니다. AST->Plan 변환과정에서 여러 최적화를 적용하고, 실행시에는 멀티코어멀티노드를 활용하여 파이프라인을 병렬로 수행합니다. 이러한 디자인은 분석 쿼리에 특화되어 있어, 동일 하드웨어에서 전통 행 기반 DB보다 훨씬 높은 처리량을 보여줍니다. (예를 들어 동일 질의를 벡터화 덕분에 100배 이상 빠르게 실행한 사례들도 다수 보고됩니다.) 마지막으로, ClickHouse는 필요한 경우 코드 생성(JIT)과 같은 런타임 최적화도 병행하여, 컴파일된 C++ + 벡터화 + JIT의 하이브리드 접근을 취합니다.

4. 메모리 관리 구조 (Arena Allocator, Spill, QueryContext)

대용량 데이터를 처리하는 ClickHouse는 메모리 관리(memory management)에도 여러 최적화 기법을 적용하고 있습니다. 주요 특징으로는 Arena Allocator(아레나 할당자)를 통한 작은 객체 효율적 관리, 메모리 트래킹 및 한도 설정, 필요 시 디스크로의 Spill (외부 메모리 사용) 등이 있습니다. 또한 쿼리별 Context를 두어, 각 쿼리 단위로 메모리 사용을 모니터링하고 제어합니다. 이 섹션에서는 ClickHouse의 메모리 관리 전략을 살펴봅니다.

  • 아레나 할당자 (Arena Allocator): ClickHouse는 집계 함수의 중간 상태처럼 짧은 생명주기의 작은 메모리 객체들을 다량 할당/해제해야 하는 경우가 많습니다. 예를 들어 GROUP BY가 수백만 키를 가지면 각 키에 대한 aggregation state를 위한 메모리 블록이 필요합니다. 일반 new/delete로 매번 할당하면 오버헤드가 크고, 메모리 단편화도 일어날 수 있습니다. 이를 해결하기 위해 ClickHouse는 Arena라 불리는 메모리 풀을 사용합니다. Arena Allocator는 큰 연속 메모리 청크를 잡아두고, 그 안에서 필요한 객체들을 bump-pointer 기법으로 순차적으로 할당합니다. 할당 해제는 Arena 단위로 한 번에 이루어지므로, 개별 객체의 delete 비용이 들지 않습니다. 특히 Aggregation state 관리에 Arena가 활용되는데, 고카디널리티 그룹바이에서 수백만 개 상태 객체를 하나의 Arena에 할당함으로써, 할당/해제 비용을 상쇄하고 CPU 캐시 친화성을 높입니다. Arena에 할당된 객체들은 non-trivial한 생성자/소멸자를 가질 수 있으므로, ClickHouse는 state 객체 복사/이동 시 소멸자 호출 순서 등을 주의하여 다루지만, 전체적으로 대용량 메모리 블록을 덩어리로 다루는 방법으로 성능을 개선합니다. (예: AggregateFunctionUniq 같은 경우 내부적으로 HyperLogLog 구조 등을 가지고, Arena에 별도 할당을 하는데, 이를 하나의 pool에서 관리함).
  • 메모리 트래킹과 쿼리 한도: ClickHouse는 메모리 사용량 추적(MemoryTracker)을 통해 각 쿼리가 소비하는 메모리를 실시간 모니터링합니다. 그리고 설정에 따라 쿼리별 메모리 제한 (max_memory_usage)을 초과하면 해당 쿼리를 강제로 중단시키고 Memory limit exceeded 에러를 반환합니다. 이러한 제어는 QueryContext 단위로 이뤄지며, QueryContext는 해당 쿼리 실행에 관련된 자원(메모리, 스레드 등) 정보를 보유합니다. 또한 max_memory_usage_for_all_queries 등의 설정으로 서버 전체 또는 사용자별 메모리 사용을 제한할 수도 있습니다. MemoryTracker는 RSS 대신 논리적 할당량을 추적하며, Arena나 HashTable 등의 커스텀 allocator들은 내부적으로 MemoryTracker에 사용량을 보고하도록 연계되어 있습니다. 이를 통해 운영자는 불량 쿼리가 서버 메모리를 다 써버리는 일을 방지할 수 있습니다. (일례로, 잘못된 JOIN이 수백 GB 메모리를 쓰려 하면, 미리 정한 limit에서 쿼리를 kill시킴으로써 OOM 이전에 차단 가능.) 최신 ClickHouse에는 Memory Overcommit이라는 실험 기능도 있는데, 이는 하둡 Yarn처럼 여유 메모리를 상황에 따라 융통성 있게 공유해, limit 설정을 덜 보수적으로 해도 메모리 낭비 없이 쓰도록 하는 것입니다. 그러나 기본적으로는 보수적 fixed limit이 일반적입니다.
  • Spill 및 외부 메모리 사용: ClickHouse는 설계상 가능하면 메모리 내 연산을 선호하지만, 결과 집합이나 중간 상태가 너무 커서 메모리 한계를 넘을 경우 디스크로 스필(Spill)하는 기능도 가지고 있습니다. 예를 들어 외부 정렬(external sort)과 외부 그룹바이가 지원됩니다. 대규모 ORDER BY에서 메모리가 모자라면 중간 runs를 디스크에 써가며 merge sort를 수행할 수 있고, 그룹바이도 max_bytes_before_external_group_by 설정 등을 통해 일정 메모리를 넘기면 중간 집계 결과를 파일로 떨어뜨렸다가 최종 단계에 다시 합치는 식으로 동작합니다. 또한 분산 쿼리에서 partial aggregation을 사용하다 남은 데이터가 많으면 일부를 디스크 swap하여 처리할 수도 있습니다. Aggregation state도 메모리 부족 시 직렬화하여 디스크에 임시 저장할 수 있도록 설계되어 있습니다. ClickHouse 공식 문서에 따르면, 집계 상태를 네트워크로 보내거나 디스크에 쓸 수 있으며, 심지어 AggregateFunction 타입으로 테이블에 저장까지 가능하다고 언급합니다. 이는 매우 큰 GROUP BY에서 메모리 초과를 피하는 안전장치입니다. 다만 스필 사용은 쿼리 성능에 영향이 크므로 (디스크 I/O 발생), 실제 프로덕션에서는 RAM을 충분히 주고 limit을 여유롭게 두어 스필을 지양하고, 정말 필요할 때만 발동시키는 것이 일반적입니다.
  • QueryContext와 리소스 라이프사이클: ClickHouse는 쿼리 실행 동안 필요한 메모리나 임시 오브젝트를 쿼리 컨텍스트에 묶어 관리합니다. QueryContext는 해당 쿼리 식별자, 세션 정보, 메모리 트래커, thread pool 할당 등 다양한 정보를 포함하고 있습니다. 쿼리가 끝나면 QueryContext가 파괴되면서, 그 안에 속한 Arena나 임시 파일, 메모리 할당 등이 일괄 정리됩니다. 이러한 구조 덕분에 메모리 누수 방지에 유리하며, 하나의 쿼리가 사용한 메모리를 명확히 구분할 수 있습니다. QueryContext에는 메모리 트래커 계층 구조가 있어서, 예컨대 분산 쿼리의 경우 initiator(시작 노드)의 context와 각 remote 서버 context가 연결되어 총량 추적이 가능하고, 서브쿼리들도 부모 쿼리 context 밑에 트리로 관리됩니다. 또한, 각 쿼리 컨텍스트별로 temp 데이터 경로를 지정하여, 필요한 경우 디스크 임시 파일을 생성해 사용할 수도 있습니다. (예: External sort 시 tmp_path에 쿼리당 디렉토리를 만들어 run파일 저장).
  • 메모리 최적화 기법: 추가로, ClickHouse는 파이프라인 실행간 메모리 복사 최소화를 신경 씁니다. 예를 들어 move semantics를 활용해 한 연산자의 결과 블록을 다음 연산자가 복사 없이 소유권 이전하도록 하고, 가능한 in-place 연산을 사용합니다. 또한 공통으로 쓰이는 상수 컬럼이나 Dictionary 등은 재활용하여 중복할당을 피합니다. Hash Join 시 build 측 테이블은 여러 스레드가 공유하도록 전역 할당하고, probe측 스레드들이 공동으로 읽게 합니다. 큰 배열 할당 시 std::aligned_alloc 등을 사용해 페이지 수준 효율을 높입니다. 이러한 미세 최적화들은 코드 레벨에서 이뤄지지만, 슬랩 할당(slab allocator), 프리페치(prefetch) 기술과 함께 전반적인 메모리 처리량을 개선합니다.

정리: ClickHouse의 메모리 관리는 고성능에 초점을 맞춰 설계되어 있습니다. Arena 할당자를 통해 다량의 작은 객체 생성/소멸 오버헤드를 줄이고, 메모리 사용을 철저히 추적하여 쿼리별 한도를 지킵니다. 또한 필요한 경우 디스크를 활용하여 안정성을 높이며, 이러한 기법들은 모두 QueryContext 아래 조직적으로 운용됩니다. 궁극적으로, 이는 수십 GB~수백 GB 메모리를 효율적으로 활용하여, 한 노드에서 초당 수억 행의 데이터 처리를 원활히 하기 위한 기반이 됩니다.

5. 분산 쿼리 처리 구조 (Distributed 테이블, 샤딩, 복제, ZooKeeper/Keeper)

ClickHouse는 분산 모드에서 뛰어난 확장성을 보이도록 설계되었으며, 여러 노드에 데이터를 샤딩(sharding)하고 복제(replication)하여 대용량 데이터와 고가용성을 동시에 처리할 수 있습니다. 이를 지원하는 핵심 메커니즘이 Distributed 테이블 엔진ReplicatedMergeTree 엔진 (및 ZooKeeper/ClickHouse Keeper에 의한 조정)입니다. 이 섹션에서는 분산 환경에서의 ClickHouse 동작: 샤딩 구조, 복제 메커니즘, 분산 쿼리 실행 흐름, ZooKeeper(또는 ClickHouse Keeper)의 역할 등을 다룹니다.

  • 샤딩과 Distributed 테이블: ClickHouse 클러스터는 일반적으로 Shard(샤드)들의 집합으로 구성됩니다. 하나의 샤드는 데이터의 수평 파티션에 해당하며, 여러 샤드를 통해 데이터 양에 비례해 성능을 확장할 수 있습니다. ClickHouse는 사용자가 DDL에서 Distributed 테이블 엔진을 정의함으로써 샤딩된 테이블을 투명하게 조회할 수 있게 합니다. Distributed 엔진은 프록시 테이블로서, 실제 데이터는 샤드별 로컬 MergeTree 테이블들에 저장되고, Distributed 테이블은 각 쿼리를 적절한 샤드로 브로드캐스트/라우팅하는 역할을 합니다. 예를 들어, CREATE TABLE hits_dist AS hits ON CLUSTER myCluster ENGINE = Distributed(myCluster, db, hits_local, shard_key)와 같이 설정하면, hits_dist로 SELECT 시 myCluster 설정에 따라 모든 샤드의 db.hits_local 테이블에 서브쿼리를 날리고 결과를 합칩니다. 분산 쿼리 처리 흐름은 다음과 같습니다: 쿼리를 받은 이니시에이터(initiator) 노드가 쿼리 계획을 분석하여, 가능한 작업을 최대한 각 샤드로 푸시다운합니다. 예를 들어 WHERE 필터, 칼럼추출, 부분 집계 등을 샤드 노드에서 수행하게 쿼리를 변형합니다. 그런 다음 각 샤드의 해당 로컬 노드들에게 SQL을 병렬 전송하고 실행합니다 (프로토콜은 내부 TCP 바이너리 프로토콜). 샤드 노드들은 자신의 로컬 데이터를 처리하여 중간 결과(필요시 partial aggregate 혹은 최종 결과까지)를 initiator로 스트리밍 전송합니다. initiator는 모든 샤드로부터 받은 결과를 필요한 경우 추가로 merge/집계하여 최종 결과를 사용자에게 반환합니다. 이 전체 과정이 사용자에게는 단일 테이블 조회처럼 보이지만, 내부적으로는 다중 노드 병렬 실행이 이뤄집니다. 또한, ClickHouse는 분산 쿼리에서도 최대 성능을 위해 최적화를 합니다. 예를 들어, ORDER BY가 있다면 각 샤드에서 local sort를 수행하고 initiator에서 merge sort를 하며, LIMIT N이 있다면 각 샤드에서 N개씩 미리 자르고 가져온 뒤 합치는 distributed top-N 최적화를 합니다. 이처럼 scatter-gather 패턴으로 동작하는 분산 엔진은 선형에 가까운 스케일 아웃을 가능케 합니다 (이론상 2배 노드에 2배 데이터 넣으면 비슷한 쿼리 시간이 나옴).
  • 복제와 고가용성 (ReplicatedMergeTree): 데이터 고가용성과 장애 허용을 위해, ClickHouse는 복제된 테이블 개념을 제공합니다. MergeTree 엔진의 변종인 ReplicatedMergeTree를 사용하면 동일 데이터를 가진 복제본(replica) 여러 개를 둘 수 있습니다. 각 ReplicatedMergeTree 테이블은 ZooKeeper (또는 자체 ClickHouse Keeper)에 경로(path)를 지정하여, 동일한 경로를 사용하는 테이블들은 같은 복제 그룹으로 묶입니다. 이 그룹 내 각 노드는 동등한 multi-master 복제본으로 동작하며, 모든 복제본이 결국 같은 데이터 상태를 갖도록 비동기 동기화됩니다. 중요한 건 어느 복제본으로든 INSERT를 보낼 수 있다는 점입니다 (multi-master). 하나의 노드에 INSERT된 데이터 파트는 비동기로 다른 복제본들에 전파되며, 결국 모든 노드에 복제됩니다. 이 과정은 ZooKeeper/Keeper를 통한 로그와 큐 메커니즘으로 작동합니다:
    • 중앙 조율자(ZooKeeper)에 replication log라 불리는 테이블별 로그가 있어, 새로운 파트 생성, 파트 merge, 파트 삭제 등의 이벤트를 기록합니다. 예컨대 어떤 복제본에서 새로운 파트를 생성하면, ZK 로그에 "GET_PART all_123_456_7" 같은 액션을 추가합니다.
    • 각 복제본은 ZooKeeper로부터 이 액션 로그를 자신만의 큐로 복사해옵니다. 그리고 백그라운드 스레드가 그 큐를 순차로 실행합니다. GET_PART 액션이면 해당 파트를 가진 복제본으로부터 HTTP/RPC로 압축된 파트 파일들을 가져와 자기 디스크에 저장하고, 메타정보를 업데이트합니다. MERGE_PARTS 액션이면, 그 복제본은 자기 로컬 파트들로 merge 실행 후 결과를 저장합니다. 이처럼 각 복제본은 동일한 로그를 따라감으로써 같은 결과 상태를 갖게 됩니다.
    • 병합(merge) 조정: 여러 복제본이 동시에 서로 다른 머지를 수행하면 최종 상태가 달라질 수 있으므로, ClickHouse는 머지 작업도 조율합니다. 기본 전략은 "deterministic merge": 동일한 입력 파트 세트에 대해서는 모든 복제본이 똑같은 방식으로 merge하도록 합니다. 이를 위해 보통 한 복제본(리더)이 특정 파트들을 합치겠다는 결정을 내리고, ZooKeeper 로그에 해당 merge 액션을 기록합니다. 그러면 다른 복제본들도 그 액션을 보고 동일한 파트들을 병합하여 바이트 동일한 결과를 생성합니다. Merge 알고리즘과 압축시드 등이 동일하므로 결과 checksum이 일치하게 됩니다. 여러 노드가 동시에 다른 merge를 제안할 수도 있는데, 기본적으로 모든 복제본이 리더 역할을 할 수 있는 멀티리더 (leaderless) 구조를 취합니다. 필요하다면 설정으로 한 노드를 리더 되지 않게 막을 수도 있습니다 (replicated_can_be_leader).
    • 결과 일치 및 self-healing: 복제본들은 ZooKeeper에 자신의 현재 보유 파트 목록과 checksum 정보를 주기적으로 기록해둡니다. 만약 어떤 복제본이 장애 후 재시작 등으로 상태가 일탈하면 (예: 특정 파트를 놓쳤거나 손상), ZK의 참조 상태와 비교하여 스스로 결함 복구를 시도합니다. 없는 파트는 다른 복제본에게 가져오고, 로컬에 있는데 ZK 기준 없는 파트는 lost+found 식으로 디렉토리 격리하여 무시합니다. 이를 통해 eventual consistency를 유지합니다.
    • ReplicatedMergeTree의 복제는 물리적 복제입니다: SQL 쿼리를 재실행하는 게 아니라 압축된 파트 파일 자체를 네트워크로 전송합니다. 이것은 대용량 데이터 복제 시 매우 효율적이며, 네트워크 부하를 줄입니다. 큰 merge 결과를 굳이 한 번 더 네트워크 복사하지 않도록, 가능하면 각 노드가 스스로 merge를 해서 동일 결과를 얻게 하는 이유도 여기에 있습니다. 다만 특정 복제본이 많이 뒤쳐졌을 때는 (예: 오래 다운됐다 복귀) 이미 다른 노드들에서 merged된 거대한 파트를 통째로 받아오는 것이 오히려 빠를 수 있어, 그런 경우에만 네트워크 fetch로 가져옵니다.
  • ZooKeeper / ClickHouse Keeper: ZooKeeper는 분산 조정 서비스로, ClickHouse 복제에서 원자적 변경 및 분산 락을 제공하는 핵심 컴포넌트였습니다. 최근에는 ClickHouse가 자체 구현한 Keeper (ZooKeeper와 동일 API를 가지는 Raft 기반 서비스)를 내장하여, 외부 ZooKeeper 없이도 복제를 운용할 수 있습니다. ZooKeeper/Keeper는 전역 일관성 보장을 위해 사용됩니다. 예를 들어 Insert가 들어오면 모든 복제본에서 동일하게 적용되도록 ZK 로그에 기록하고, DDL (CREATE/DROP 등)도 원자적 수행을 위해 ZK를 쓸 수 있습니다. ZK의 사용은 약간의 latency 추가를 의미하지만, 견고한 분산 합의를 통해 (Raft) 멀티마스터 eventually consistent 모델을 구현합니다. ClickHouse Keeper는 C++로 구현되었고 ZK보다 지연이 낮으며, 클러스터 내부에 함께 운용될 수 있습니다. ZooKeeper 메타정보에는 위 언급된 replication log, 각 파트 정보, last merged pointer 등 다양하게 저장됩니다. 또한, ClickHouse는 ZK를 사용해 Distributed DDL(ON CLUSTER queries)의 동기 실행을 맞추기도 합니다 (모든 노드에 테이블 생성 등). ZK/Keeper 장애시를 대비해 복제본들은 마지막 상태를 기반으로 일시 독립적으로 동작하다, ZK 복구 후 다시 동기화합니다.
  • 클러스터 토폴로지와 한계: 기본 ClickHouse 클러스터는 셔드 X 복제 구조입니다. 예: 4샤드 * 각 2복제 = 총 8노드. 이때 셔드 간에는 데이터가 분산되고 복제본 간에는 동일 데이터 유지입니다. ClickHouse는 자동 샤드 리밸런싱이 기본적으로 없습니다. 즉, 새로운 샤드를 추가해도 기존 데이터가 옮겨가지는 않으므로, 처음 클러스터 설계를 고려해야 합니다. 이 부분은 대규모 (수백 노드 이상) 확장 시 수작업 부담을 줄이기 위해, 최근 Cloud (Shared) MergeTree 엔진으로 개선되고 있습니다 (뒤의 최신동향에서 설명). 또한, 분산 환경에서 트랜잭션이 보장되지 않으므로, cross-shard 업데이트나 2단계 커밋 같은 것은 지원하지 않습니다. 기본 모델은 "클러스터 전체 일관성"보다는 "각 샤드 독립 처리 후 상위 레벨에서 결과 수합"입니다. 이러한 디자인은 단순성과 성능을 우선한 것으로, 많은 분석 시스템이 비슷한 접근을 취합니다. 다만, 클릭하우스 클러스터는 eventual consistency 모델이므로, 예를 들어 한 복제본에 INSERT 성공 응답을 받았더라도, 즉시 다른 복제본에서 조회하면 그 데이터가 아직 안 보일 수 있습니다 (전파 지연). 필요 시 insert_quorum 설정으로 다수 복제본에 확신이 된 후 응답하도록 하거나, select_sequential_consistency 같은 세션 설정으로 조회 일관성을 조정할 수 있습니다.

요약: ClickHouse의 분산 구조는 샤딩을 통한 수평 확장복제를 통한 고가용성을 제공하며, 이러한 복잡성을 비교적 단순한 엔진 메커니즘으로 숨겨줍니다. 분산 쿼리는 클러스터 전체를 투명하게 조회할 수 있게 해주며, 복제 메커니즘은 ZooKeeper/Keeper를 통해 결과적 일관성을 확보합니다. 이러한 설계 덕분에 ClickHouse는 수백 노드 규모의 클러스터에서도 대용량 데이터 처리를 빠르고 안정적으로 수행하고 있습니다 (예: 얏덱스 메트리카 서비스는 400노드 클러스터로 일 20TB 이상 데이터 처리 보고).

6. 백그라운드 작업 (머지, 뮤테이션, TTL, Fetch 등)

ClickHouse 서버에는 사용자 질의 외에도 다양한 백그라운드 작업이 동작하여, 데이터 파일들을 관리하고 시스템을 최적 상태로 유지합니다. 주요 백그라운드 태스크로는 MergeTree 파트 병합(merge), 뮤테이션(mutation) 적용, TTL 만료 처리, 복제 파트 fetch 등이 있습니다. 이러한 작업들은 백그라운드 스레드 풀에서 비동기로 수행되어, 사용자 쿼리에 지장을 최소화합니다. 이번 섹션에서는 각 백그라운드 작업들의 역할과 동작 방식을 설명합니다.

  • 파트 병합 (Background Merge): 앞서 MergeTree 구조에서 설명했듯, 다수의 작은 파트들은 지속적으로 합쳐져 더 큰 파트로 병합됩니다. ClickHouse 서버는 테이블별로 백그라운드 머지 작업을 수행하는 스레드 풀(MergeTreeBackgroundExecutor)을 운영하며, 주기적으로 각 테이블의 현재 파트 리스트를 분석해 병합 대상 파트 집합을 선택합니다. 일반적인 MergeTree의 merge 선택 알고리즘은 "가장 작은 파트들부터 합치기", "동일 파티션 내 파트들만 합치기" 등의 euristic을 따릅니다. 선택된 파트들은 백그라운드에서 읽혀져 정렬 병합된 후, 새로운 합쳐진 파트가 생성됩니다. 병합 완료 시, 원래의 작은 파트들은 디스크에서 삭제되거나 (잠시 남겨뒀다가 나중에 제거되어) 새로운 파트로 교체됩니다. Merge 작업은 IO와 CPU를 모두 소비하는 작업이지만 낮은 우선도로 실행되며, 동시에 너무 많은 리소스를 쓰지 않도록 내부적으로 조절됩니다 (예: mergetree_max_bytes_to_merge_at_max_speed 설정 등). 중요한 것은, 머지 도중에도 기존 파트들이 유효하므로, 사용자 SELECT 쿼리는 머지 중에도 끊김 없이 옛 파트들을 읽을 수 있습니다 (머지 완료 전까지 snapshot에 보이는 파트 집합은 그대로 유지). 머지 완료 후에는 원자적으로 교체되어 신규 스냅샷에서만 보이게 됩니다. 이러한 방식으로 ClickHouse는 데이터 입력으로 파트가 많이 생겨도 백그라운드에서 점진적으로 정리하여 읽기 성능을 꾸준히 유지합니다. (단, 너무 작은 batch insert가 계속되면 파트 병합 부담이 증가하므로, ClickHouse는 가능하면 INSERT도 한 번에 많은 행을 넣도록 권장합니다. v21.x 부터는 tiny insert들을 메모리에 모아두다 배치로 병합하는 async insert 모드도 추가되었습니다.)
  • 뮤테이션 (Mutation) 및 Delete: ClickHouse는 기본적으로 UPDATE/DELETE문을 실시간 반영하지 않고, 배치 뮤테이션으로 처리합니다. 사용자가 ALTER TABLE ... UPDATE ... WHERE 또는 ... DELETE WHERE 명령을 내리면, 그것은 곧 뮤테이션 작업으로 스케줄됩니다. 이 작업은 해당 조건에 해당하는 파트들을 새로 다시 써주는 형태로 실행됩니다. 예를 들어 특정 조건의 행을 삭제하는 경우, 해당 행들을 제외하고 나머지 데이터를 새로운 파트로 만들고 기존 파트를 교체합니다. 이 작업은 Merge와 유사하게 백그라운드 병합 형태로 일어나며, Mutation 로그를 통해 복제본들도 동일하게 수행합니다. 주의할 점은, 뮤테이션은 복잡한 연산이고 (사실상 테이블을 다시 쓰는 비용), 즉각적이지 않고 eventually 적용된다는 것입니다. 즉, 뮤테이션이 진행되는 동안 일부 쿼리는 옛 데이터, 일부는 새 데이터를 볼 수도 있습니다 (non-atomic). 특히 Delete는 모든 컬럼 파트를 다시 써야 하므로 비용이 큽니다. 최근 도입된 경량 삭제 (Lightweight Delete)는 이를 개선한 것으로, 행을 실제로 지우는 대신 숨김 마크(bitmap)만 기록해 두고 SELECT 시 필터링하다가, 나중에 진짜 머지 때 삭제하는 방식입니다. 이렇게 하면 DELETE 명령 자체는 매우 빨라지지만, 그 후 SELECT가 조금 오버헤드가 늘고, 최종 삭제는 백그라운드 병합으로 처리됩니다. Mutation 역시 경량 업데이트 개념은 없지만, 특정 AggregatingMergeTree 등은 백그라운드에서 합치며 업데이트 효과를 내기도 합니다. 이렇듯, ClickHouse는 OLAP 최적화를 위해 업데이트/삭제를 lazy하게 처리하며, 모든 변경은 백그라운드 remerge 형태로 수행됩니다.
  • TTL Merge (만료 데이터 처리): TTL에 의한 만료 행/파트 삭제도 백그라운드 작업입니다. 각 MergeTree 테이블은 TTL 룰에 따라 특별한 merge 조건을 가집니다. 백그라운드 병합 시, 만약 어떤 파트의 모든 행이 TTL 만료된 경우, 아예 병합 없이 파트를 통째로 드롭하기도 하고 (ttl_only_drop_parts 옵션), 파트 일부 행만 만료되었다면 merge하면서 해당 행들을 걸러냅니다. 또한 TTL이 설정되어 디스크 이동이 필요한 경우 (예: HOT -> COLD storage 이동), 백그라운드에서 데이터 파일을 지정한 볼륨 경로로 옮기고 메타데이터 갱신을 합니다. 이러한 TTL 처리는 주기적으로 확인되며, 필요한 경우 off-schedule merge라고 하여 원래 merge 주기를 무시하고 즉시 수행하기도 합니다. 예컨대 하루 단위 TTL 삭제인데 24시간이 지나면, 바로 그때 merge를 트리거하여 삭제해줄 수 있습니다. 단 너무 잦은 merge를 피하려고 merge_with_ttl_timeout 같은 설정이 활용됩니다. 결과적으로 TTL에 의한 데이터 lifecycle 관리 역시 일반 merge 메커니즘에 통합되어 백그라운드에서 실행됩니다.
  • 파트 Fetch (복제 동기화): 복제된 테이블에서는 데이터 가져오기(fetch)도 중요한 백그라운드 작업입니다. 앞서 복제 메커니즘에서 설명한 대로, 한 복제본에 새 파트가 생기면 다른 복제본들은 ZooKeeper 로그를 통해 이를 감지하고 자신의 ReplicatedMergeTreeQueue에 fetch 명령을 넣습니다. 백그라운드 스레드는 이 큐를 처리하면서, 필요한 파트를 네트워크로 해당 복제본에서 다운로드합니다. 이 fetch는 HTTP GET/PUT으로 데이터를 주고받거나, 최근에는 replicated fetch 프로토콜로 전송합니다. 또한 오랫동안 다운됐던 노드가 재시작하면, 손실된 파트들을 연달아 fetch하여 따라잡습니다. 만약 어떤 복제본이 merge를 먼저 수행했으면, 뒤처진 복제본은 작은 파트 여러 개 대신 큰 merged 파트를 한 번에 fetch하기도 합니다. Fetch thread는 병합 thread와 별개로 운영되며, 동시에 여러 fetch를 처리할 수도 있지만 보통 대역폭 제한 설정 등이 적용됩니다. 이 fetch 작업이 원활히 돌아가야 클러스터 복제가 eventually sync 됩니다.
  • 그 외 백그라운드 작업: 이 밖에도 ClickHouse 서버에는 System 로그 플러시 (system.part_log 등 메타정보 기록), 버킷 연결 유지 (ODBC/JDBC 외부 디펜던시), Replica 활성도 체크 (ZooKeeper 세션 유지), 카탈로그 정리 (DROP된 테이블 실제 데이터 지우기) 등 다양한 백그라운드 task들이 존재합니다. 이러한 작업들은 BackgroundSchedulePool 등에서 일정 간격으로 실행됩니다. 예를 들어 SystemParts 로그는 새로운 파트 생성/삭제 시 기록을 남기고, 오래된 로그는 주기적으로 삭제됩니다. 또 join, dictionary 같은 엔진은 백그라운드 프리로딩/갱신 작업을 가지기도 합니다. Materialized View도 백그라운드 스레드가 원본 테이블 변경을 수신해 뷰를 갱신합니다. 이러한 여러 가지 작업들이 병렬로 일어나지만, ClickHouse는 기본적으로 다중 core 환경에서 충분히 처리가 가능하도록 스레드 풀과 태스크 스케줄링을 최적화했습니다. 각 작업은 가급적 경합 없이 실행되며, 필요시 리소스 양보를 합니다.

요약: 백그라운드 작업들은 ClickHouse의 셀프 메인터넌스(self-maintenance)를 책임집니다. 데이터 파일 병합, TTL 정리, 복제 동기화, 뮤테이션 적용 등을 자동으로 처리함으로써, 사용자는 일일이 수동 관리할 필요 없이 DB를 운용할 수 있습니다. 이러한 작업들이 분리된 스레드 풀에서 실행되기에, 대규모 데이터 적재/갱신 상황에서도 프론트엔드 질의는 일정 수준의 성능을 유지할 수 있습니다. 다만, 너무 잦은 작은 insert나 빈번한 뮤테이션은 백그라운드 작업 부하를 높여 결국 읽기 성능에도 영향 줄 수 있으므로, 워크로드에 맞게 파라미터 튜닝(예: insert batching, mutation batching)이 중요합니다.

7. 데이터 정합성 및 지속성 구조 (일관성 모델과 Atomic DDL)

데이터베이스 시스템에서 일관성과 내구성(consistency & durability)은 매우 중요한 요소입니다. ClickHouse는 철저히 OLAP 용도로 설계되어, 전통 RDBMS의 강력한 ACID 트랜잭션 보다는 높은 처리량과 최종일관성(eventual consistency)에 초점을 맞추고 있습니다. 그럼에도, 스냅샷 격리(snapshot isolation) 수준의 일관성과 DDL원자성을 보장하고 있어, 대부분의 분석 시나리오에서 문제가 없도록 구현되어 있습니다. 이번 섹션에서는 ClickHouse의 일관성 모델, Atomic DDL 및 Database engine, Durability (내구성) 특성을 설명합니다.

  • 일관성 모델 (Consistency Model): ClickHouse의 일관성은 크게 두 부분으로 나눌 수 있습니다: 단일 노드 일관성분산 복제 일관성.
    • 단일 노드: ClickHouse 서버 내에서 동시 읽기/쓰기 일관성멤컹너식 MVCC와 유사하게 처리됩니다. SELECT 쿼리는 시작 시점에 테이블의 파트들에 대한 스냅샷을 잡고 (현재 활성 파트 집합을 보존) 그 이후로 추가되거나 교체된 파트를 보지 않습니다. 즉, 하나의 SELECT는 그 쿼리 시작 시점 기준으로 일관된 정합성 있는 데이터를 조회합니다 (이는 스냅샷 격리에 해당). Insert는 새로운 파트를 추가하는 방식이라, 기존 파트에는 영향 안 주고 원자적으로 새로운 파트를 visible하게 만들어 줍니다. 따라서 Insert 중인 데이터가 SELECT에 보이거나 깨진 중간 상태가 노출되지 않습니다. 또한 한 테이블에서 백그라운드 머지가 일어나도, SELECT는 머지 완료 전의 파트를 보고 있으며 완료 후에야 새로운 파트를 보게 되므로 반쯤 머지된 상태를 읽을 일도 없습니다. 이처럼 immutable part + snapshot 구조 덕분에 읽기 일관성이 자연스럽게 확보됩니다. 다만, 다중 테이블간의 일관성은 보장하지 않습니다; ClickHouse에는 다테이블 트랜잭션이 없으므로, 동시에 다른 테이블에 Insert한 내용은 각자 독립적입니다. 일부 명령 (예: Atomic Database의 EXCHANGE TABLES)은 두 테이블 swap을 원자적으로 해주지만 일반적이지 않습니다.
    • 분산 복제: 복제본 간에는 앞서 설명한 최종적 일관성(eventual consistency) 모델입니다. 즉, Insert가 한 복제본에서 성공했다 해도, 다른 복제본에 즉시 반영되는 것은 아닙니다 (네트워크/큐 지연). 기본 설정에서는 quorum ack 없이 비동기로 전파하므로, 만약 Insert 직후 해당 레플리카가 다운되면 그 데이터는 다른 복제본에 못 옮기고 유실될 수도 있습니다. 이를 해결하려면 insert_quorum을 설정하여, 지정한 수의 복제본들이 파트를 받고 ACK해야 Insert 성공으로 간주하게 할 수 있습니다. 이 경우 지정된 복제본 수가 미만이면 에러를 내므로, 어느 정도 강한 일관성을 흉내낼 수 있습니다. 그러나 여전히 full multi-node 동시 트랜잭션은 없습니다. SELECT 시에는 기본적으로 로컬 복제본만 보지만 (만약 Distributed table로 조회하면 각 샤드 복제 중 하나만 읽음), 특별히 prefer_localhost_replica=0 등을 주면 다른 복제본도 읽게 할 수 있습니다. 이때 모든 복제본의 데이터 일치를 가정하지만, 완전 실시간은 아니라서 만약 복제 지연이 있다면 동일 시점에 노드별로 다른 결과를 줄 수도 있습니다. 그래서 critical한 실시간 일관성 요구 시 ClickHouse보단 여타 DB를 쓰거나, insert후 잠시 대기(또는 select_sequential_consistency 같은 설정으로 동일 레플리카에서 읽기)해야 합니다. 요약하면, ClickHouse는 한 쿼리 내에서는 일관된 snapshot 보장하지만, 분산환경 전체의 강한 즉시일관성은 보장하지 않습니다.
  • Atomic DDL 및 Database Engine: ClickHouse 20.10부터 Atomic Database Engine이 도입되어, DDL 연산들의 원자성과 concurrency 안전성이 향상되었습니다. Atomic DB를 쓰면 (이게 기본임) CREATE, DROP, RENAME 테이블 등이 저수준에서 원자적으로 처리됩니다. 구체적으로, Atomic DB에서는 각 테이블에 UUID를 부여하고, 테이블의 디스크 경로도 그 UUID를 이용해 관리합니다. 이 덕에 테이블 RENAME을 해도 실제 데이터 폴더는 그대로고 메타데이터만 스왑하므로 즉시 완료되고 다른 쿼리와 충돌하지 않습니다. DROP TABLE도 실제 데이터를 바로 지우지 않고 /metadata_dropped/ 폴더로 메타만 옮기고 나중에 삭제하므로, DROP과 SELECT 등이 충돌하지 않고, DROP 후 바로 CREATE 동명 테이블도 가능해집니다. 또한 EXCHANGE TABLES 라는 특수 DDL (두 테이블의 이름과 내용 교환)도 Atomic하게 지원하여, 타 DB의 RENAME 트릭처럼 스냅쇼트 교체를 구현할 수 있습니다. 이런 DDL은 ZooKeeper 없이 로컬에서 구현되지만, 클러스터 전체 DDL 동기 실행 (ON CLUSTER)은 여전히 ZK를 쓰거나, 새 Distributed DDL 프로세서를 사용합니다. Atomic DB engine은 기본값으로, 예전 Ordinary engine에서는 DDL 중 충돌이나 복제 간 DDL 순서 엇갈림 같은 문제들이 있었으나 이제 많이 해소됐습니다. Altinity 자료에 따르면 20.5에서 첫 소개, 20.10부터 기본 적용되었다 합니다.
  • INSERT 원자성: ClickHouse의 INSERT (한 쿼리 내)을 보면, 한 Batch의 모든 행이 전부 반영되거나 전부 무시되는 원자성을 가집니다. INSERT 쿼리가 분할되어 여러 파트로 쓰일 수도 있지만, 클라이언트에는 하나의 트랜잭션처럼 보장됩니다. 예를 들어, INSERT ... SELECT 문이 실패하면 일부 행만 들어가고 말고 그런 게 아니라, 아예 해당 insert가 적용되지 않습니다 (이때 Already inserted 부분 rollback 위해 파트 지우는 logic 있음). 또한, 분산 테이블에 INSERT의 경우, coordinator가 모든 shard중 일부 실패시 나머지도 취소하도록 (실제로 이미 들어간 건 수동정리 필요하지만) 해줍니다. 최근 2023.7 버전에서는 2PC(2-phase commit) 스타일 분산 트랜잭션도 실험중이라, 미래엔 cross-shard 원자 Insert가 가능해질 수 있습니다. 하지만 현재는 보통 INSERT 실패시 idempotent retry하거나 at-least-once로 애플리케이션 레벨 조정합니다.
  • Durability (내구성): ClickHouse는 데이터를 바로 파일로 기록하는 구조라, WAL(Write-Ahead Log)은 없지만 내구성 자체는 높습니다. Insert 시 데이터는 곧바로 압축되어 파트 파일로 디스크에 flush되므로, 프로세스 크래시가 나도 이미 쓰여진 데이터는 살아있습니다. 단, ClickHouse는 디폴트 설정에서 fsync를 매번 호출하지는 않기 때문에, OS 캐시에 머무른 데이터는 유실 가능성이 있습니다. 예컨대 SSD 성능을 위해 flush on every insert를 안 하므로, 갑작스런 전원 장애 시 몇 초~수초치 최근 insert가 OS 캐시에서 날아갈 수 있습니다. 일반적으로 분석 DB에서는 약간의 최신 데이터 손실을 감내하고 성능을 얻는 쪽을 택하기에, ClickHouse도 기본값이 그런 철학입니다. 필요하면 min_compress_block_size를 조정해 더 자주 작은 파트를 만들어 sync하거나, fsync_metadata=1 등의 설정으로 메타데이터는 매번 동기화하게 할 수 있습니다. 복제본이 있다면 한 노드 장애 시 다른 노드에 데이터가 남아있으므로 내구성 보완이 되고, WAL 없는 설계지만 다중 복제로 신뢰성을 올리는 형태입니다. 요약: ClickHouse는 disk write에 있어 실용적 내구성 (통상 OS flush, 수초 내 flush) 정도이며, 완벽 ACID DB처럼 flush-each-commit 설정은 기본 비활성입니다. snapshot/백업 기능으로 수시 백업하는 방식이 주로 쓰입니다.
  • 일관성 vs 성능 Trade-off: 정리하면, ClickHouse는 ACID 중 Atomicity(원자성: DDL, 단건 insert 레벨에서 보장), Consistency(데이터 무결성, constraint는 거의 없음), Isolation(격리: snapshot isolation 정도) 측면은 상당 부분 지원하지만, Durability는 강하지 않은 기본 설정입니다. 애널리틱스 용도로 설계된 만큼, 분산 환경에서는 Eventually consistent, no multi-tx를 택했고 이는 성능 향상의 뒷받침이 되었습니다.

요약: ClickHouse는 트랜잭션 DB가 아닌 OLAP DB로서, 최종적 일관성과 높은 처리량을 우선합니다. 한 쿼리 내에서는 스냅샷으로 일관된 결과를 주지만, 다수 쿼리를 통합하는 엄격한 SERIALIZABLE 트랜잭션은 없습니다. DDL과 배치 Insert의 원자성은 Atomic 엔진으로 강화되었고, 복제 또한 ZooKeeper를 통해 eventual consistency를 유지합니다. 내구성은 기본 설정에서 약간의 손실 위험을 감수하지만, 복제와 백업으로 보완하는 식입니다. 이러한 설계 철학은 ClickHouse를 성능 지향 DB로 만들었으며, 실제로 대부분의 분석 시나리오에서 일관성 문제가 없도록 잘 동작합니다.

8. ClickHouse의 옵티마이저 및 쿼리 재작성 전략

전통적인 DBMS는 복잡한 코스트 기반 옵티마이저(CBO)를 가지고 다양한 실행 계획을 탐색하지만, ClickHouse는 OLAP 특화 DB로서 규칙 기반 최적화하드웨어 최적화에 무게를 둡니다. 그러나 최근에는 점차 옵티마이저도 고도화되고 있으며, 쿼리 리라이팅(query rewrite), 조인 순서 최적화, 통계 활용 등의 기능이 발전 중입니다. 여기서는 ClickHouse가 쿼리를 어떻게 최적화하는지, 주로 사용하는 전략들을 정리합니다.

  • 규칙 기반 최적화 (Rule-Based): ClickHouse 옵티마이저의 1차 단계는 문법 트리(AST) 단계의 간단한 변환입니다. 상수 계산, 조건 재정렬, 서브쿼리 물리화, IN-서브쿼리 -> JOIN 변환 등 비교적 단순한 규칙들이 적용됩니다. 예를 들어 SELECT * FROM table WHERE X=5 OR X=7 OR X=9은 내부적으로 X IN (5,7,9)로 바뀌어 처리 비용을 줄이고, SELECT count(*) FROM table WHERE cond AND false OR cond2 같은 건 미리 false 부분 제거합니다 (constant folding). 또한 공통된 식은 한 번만 계산하여 재사용하고 (Common subexpression elimination), WHERE의 (cond1 AND cond2) OR cond1 같은 것도 단순화합니다. 이런 규칙들은 ExpressionAnalyzer 단계에서 수행되며, 특별히 사용자가 알아채지 못해도 내부적으로 쿼리가 좀 더 최적 형태로 변합니다.
  • 테이블 엔진 기반 최적화: ClickHouse는 테이블 엔진 (특히 MergeTree)의 특성을 이용한 물리 계획 최적화를 합니다. 가장 큰 예가 "이미 정렬되어 있는 경우 불필요한 연산 생략"입니다. MergeTree 테이블은 정해진 순서로 정렬되어 있으므로, ORDER BY 절이 그 정렬과 호환되면 별도의 sorting 단계를 계획에서 제거합니다. 예를 들어 테이블이 (user_id, timestamp)로 sortKey인데 쿼리가 ORDER BY user_id, timestamp로 요청하면, ClickHouse는 데이터가 이미 정렬되어 있다고 보고 sort 연산을 생략하고 바로 streaming merge 단계로 넘어갑니다. 또 GROUP BY 키가 첫번째 정렬키 prefix와 같으면, 해시테이블을 쓰지 않고 sort 기반 집계로 플랜을 짭니다. 이 경우 추가 sort 없이도 이미 sorted input을 이용해 grouping 할 수 있으므로, 메모리 사용이 줄고 성능이 좋아집니다. 이러한 엔진 인지 optimizations 덕분에, 사용자가 스키마 설계를 정렬키 적절히 하면 ORDER/GROUP 연산이 사실상 공짜가 될 수 있습니다. 그 밖에, 특정 함수가 모노토닉하면 (예: y=f(x) 단조 증가) primary key index를 이용해 WHERE f(col) < c 같은 조건도 index scan 가능하다는 최적화가 있습니다. ClickHouse는 함수 monotonicity를 함수 구현에 표기해 두고, SELECT 조건 파싱 시 활용합니다.
  • 데이터 스키핑(Data Skipping) 및 인덱스 활용: ClickHouse 쿼리 최적화의 핵심은 불필요한 데이터 읽기를 피하는 것입니다. 이를 위해 다양한 인덱스와 통계를 활용합니다. 앞서 설명한 프라이머리 키 희소 인덱스는 WHERE 조건 중 prefix 부분에 대해 relevant granule만 읽도록 해 줍니다. 또한 데이터 스키핑 인덱스(Data Skipping Index)라는 부가 인덱스를 지원하여, min-max, bloom filter, set index 등을 특정 컬럼에 설정할 수 있습니다. 쿼리 시 이러한 보조 인덱스들도 체크하여, 예컨대 WHERE category = 'Shoes' 조건이 있는데 해당 granule의 set-index에 'Shoes'가 없으면 그 granule은 아예 디스크를 읽지 않는 식입니다. Bloom filter 인덱스는 고각(cardinality) 문자열 검색에 유용하여, 부분 문자열이나 UUID 존재 여부 같은 조건에서 거짓 양성 조금 허용하고 대부분 블럭을 스킵하게 합니다. Projections (프로젝션)도 optimizer 단계에서 활용됩니다. Projection은 미리 정의한 다른 sort key로 테이블 일부 컬럼을 물리적으로 복제 저장한 것으로, 만약 쿼리가 projection과 정확히 매칭되면 본 테이블 대신 그 projection을 스캔합니다. 예를 들어 일별 집계 projection이 있으면, GROUP BY date 쿼리에 메인데이터 대신 projection (훨씬 작음)을 사용합니다. ClickHouse는 projection 존재를 optimizer 단계에서 확인해 자동 대체합니다. 이러한 skipping과 projection 기술들은 I/O량을 획기적으로 줄여주는 1차적 옵티마이즈 수단입니다. 특히 최근 lazy materialization 도입과 함께, PREWHERE라는 early filtering 절도 적극 쓰이는데, 이는 한 쿼리 내에서 어떤 컬럼은 먼저 읽어서 필터링에만 쓰고, 다른 컬럼들은 나중에 읽는 최적화를 지정할 수 있는 구문입니다. ClickHouse 엔진은 상황에 따라 WHERE 절을 PREWHERE로 자동 승격시키기도 하여 (primary index 없는 컬럼이라도 먼저 읽어 필터링 후 나머지 컬럼 읽음), 불필요 열 읽기를 줄입니다.
  • JOIN 및 서브쿼리 최적화: ClickHouse는 복잡 조인에는 강력한 코스트 최적화를 하지는 않지만, 몇 가지 정략적 최적화를 합니다. 서브쿼리의 경우, 상관관계 없는 서브쿼리는 먼저 한 번만 실행해서 상수로 취급하고 (즉, scalar subquery caching), IN (subquery) 구조도 가능하면 join이나 set으로 변환합니다. JOIN 순서는 현재는 작성된 순서대로 실행하는 경향이 있고, 비용 기반 재정렬은 제한적입니다 (2025년 시점에도 join reordering은 수동 SQL 힌트 사용 정도). 그러나 dictionary join 같은 특수 케이스는 아예 hash join을 피하고 빠른 경로로 처리합니다. 조인 조건에 대해서도, 가능하면 선택도 높은 필터를 먼저 적용하도록, WHERE 절의 조건 순서를 재배치하는 최적화가 있습니다. 예를 들어 WHERE heavy_condition AND light_condition이 있을 때 light가 99% false면 먼저 평가하여 이후 heavy를 덜 하게 하는 식입니다. 이러한 선택도 추정은 통계 기반이라기보다 휴리스틱이나 간단한 표본으로 합니다. 최근 ClickHouse는 system.columns에 min/max/ndv 통계 정보를 수집하는 기능을 추가하여, 간단한 통계 기반 옵티마이저 기능 (예: filter 순서 결정 등)을 실험 중입니다.
  • Lazy Materialization (지연된 컬럼 읽기): 2025년 도입된 최신 최적화로 Lazy Materialization이 있습니다. 이는 쿼리 처리에서 실제로 필요한 시점까지 컬럼 읽기를 미루는 전략입니다. 예를 들어, 쿼리가 큰 테이블에서 ORDER BY 하지 않은 열들을 SELECT하려고 하지만 LIMIT 10만 원할 경우, 예전에는 1억행에서 모든 컬럼을 다 읽고 1억행을 정렬 후 상위10 출력했지만, lazy mat. 최적화에선 우선 정렬 키 컬럼만 읽어 상위 10개의 행 position을 결정한 다음, 그 10개 행에 대해서만 다른 컬럼을 읽는 식입니다. 실제 사례로 219초 걸리던 쿼리가 0.139초로 단축된 예시가 소개되었는데, 이는 정렬 및 LIMIT 쿼리에서 lazy mat.이 엄청난 I/O 절감을 이뤘기 때문입니다. 구현 측면에서, ClickHouse는 실행 플랜을 분석해 어떤 컬럼이 끝까지 필요없을 수 있는지 판단하고 (예: ORDER BY나 WHERE에 필요 없는 컬럼), 그 컬럼들은 일단 안 읽고 결과가 좁혀진 후 읽도록 파이프라인을 구성합니다. 이 최적화는 현재 ORDER BY + LIMIT 패턴에 주로 적용되지만, 향후 projection, join 등에도 응용될 수 있습니다. Lazy materialization은 vectorized execution의 I/O side 최적화로, CPU 연산을 줄이는 기존 기법과 함께 ClickHouse의 쿼리 성능을 더욱 높여줍니다. (단, 이것도 모든 쿼리에 항상 적용되는 건 아니고, 분석해서 이득 있을 때만 사용됨)
  • 기타 최적화: 이외에도 ClickHouse는 쿼리 캐시 기능(실험적), 재작성 View (Materialized View를 통해 쿼리 자체를 우회) 등도 있습니다. 그러나 일반적인 RDBMS의 복잡한 코스트 기반 조인 재정렬, 파티션 Pruning 등의 부분은 상대적으로 덜 강조되었습니다. ClickHouse의 "옵티마이저"는 상당 부분 디자인 단계의 결정(컬럼식 저장, sparse index, MPP 분산) 덕에 이미 성능을 얻고, 런타임에서는 비교적 단순한 규칙과 통계로 충분한 경우가 많았습니다. 하지만, 최근 요구사항 증가로 Cost-based 요소도 도입되어, 일부 join에서 동적 코스트 평가 (system table 통계 활용)나 parallel index analysis 등이 추가되고 있습니다.

요약: ClickHouse 쿼리 옵티마이저는 경량의 규칙 기반 최적화들을 다양하게 적용합니다. 프라이머리 키 정렬을 활용한 연산 스킵, 인덱스와 프로젝션으로 디스크 I/O 최소화, 벡터화된 연산 순서 최적화 (selectivity 기반) 등이 그것입니다. 완전한 코스트 기반 옵티마이저는 아니지만, 실제 워크로드에서 자주 중요해지는 부분 – 예를 들면 Top-N 쿼리의 lazy mat., 데이터를 안 읽는 방

9. 최신 기술 동향 및 기능 (Iceberg 연동, SharedMergeTree, Lazy Materialization 등)

마지막으로, 2024~2025년 기준으로 등장한 ClickHouse의 최신 기술 동향과 기능들을 소개합니다. 이는 핵심 구조에 부가되어 클라우드 환경 적응이나 데이터 레이크 통합, 쿼리 최적화 강화를 이루는 방향입니다.

  • Apache Iceberg 연동: Apache Iceberg는 데이터 레이크 파일을 테이블 형식으로 관리하는 오픈 포맷으로, Hive Metastore나 Nessie 등을 통해 스냅샷 기반 ACID 관리를 제공합니다. ClickHouse는 2024년부터 Iceberg와의 통합을 진행하여, Iceberg 테이블을 ClickHouse에서 바로 조회할 수 있는 기능을 출시했습니다. 이를 위해 Iceberg용 Table EngineTable Function을 도입했는데, Iceberg Table Engine은 Iceberg 메타데이터(Snapshot, manifest) 파일을 읽어 경로와 파일 목록을 알아내고, Parquet 등 Iceberg 파일 포맷을 ClickHouse 원본 데이터처럼 스캔하여 쿼리를 실행합니다. 현재 Iceberg 엔진은 read-only로 제공되어, ClickHouse를 Iceberg 데이터 레이크의 쿼리 가속엔진으로 활용할 수 있습니다. 예컨대, S3에 저장된 Iceberg 표를 ClickHouse에 External Table로 붙여 놓고 JOIN이나 UNION을 수행하는 것이 가능합니다. Iceberg Time Travel(과거 snapshot 읽기)도 ClickHouse SQL에서 지원하므로, SELECT * FROM iceberg_table VERSION AS OF ... 식으로 특정 시점 데이터를 조회할 수 있습니다. Iceberg 통합은 Lakehouse 패러다임에 대응하는 것으로, 기업들이 ClickHouse를 단순 OLAP DB에서 레이크하우스 질의엔진으로도 활용하도록 합니다. 다만 아직 Iceberg Table Engine은 schema evolution(스키마 변경)에 제약이 있고, 쓰기 기능은 실험적입니다. ClickHouse 팀은 Iceberg 외에도 Hudi, Delta Lake 등의 개방형 포맷과 연계를 강화하는 추세이며, 데이터 레이크 + 실시간 데이터베이스 융합을 추구하고 있습니다.
  • SharedMergeTree (CloudMergeTree): 앞서 언급한 클러스터 샤딩의 한계를 해결하기 위해, ClickHouse Cloud에서는 SharedMergeTree라는 새 엔진 패밀리를 도입했습니다. SharedMergeTree (개념상 CloudMergeTree라고도 함)는 스토리지와 컴퓨팅을 분리하고, 자동 샤딩/리밸런싱을 제공하는 MergeTree의 클라우드 버전입니다. 기본 아이디어는: 모든 데이터 파트를 오브젝트 스토리지(S3 등)에 저장하고, 여러 서버가 그걸 공유하며 필요 시 가져다 쓰는 것입니다. 이로써 기존 ReplicatedMergeTree에서 각 복제본이 자체 디스크에 데이터를 유지하고 서로 동기화하던 방식을 바꿔, 중앙 스토리지+경량 메타데이터로 일원화합니다. SharedMergeTree에서는 각 파트가 S3에 한번만 저장되면 모든 컴퓨팅 노드가 참조할 수 있고, 메타데이터/락 관리를 ClickHouse-Keeper로 최소한만 합니다. 또한, 새로운 노드가 추가되면 자동으로 shard에 포함시키고 데이터의 핫셋(자주 쓰는 key 범위)을 분산시켜주는 자동 파티셔닝/로컬리티 기능도 목표로 합니다. 이러한 Cloud 테이블은 사용자가 Distributed 테이블+로컬 테이블을 따로 만들지 않아도, 한 번 생성으로 클러스터 전체에서 조회/삽입이 가능하며, 테이블이 커짐에 따라 알아서 샤딩 범위가 세분화되어 규모에 적응합니다. SharedMergeTree는 2023년 말~2024년 초 ClickHouse Cloud에 도입되어, Cloud 서비스에서는 기본 엔진으로 쓰이고 있습니다. 이것으로 자동 스케일 아웃/인 (노드 증설/축소) 시 데이터 rebalancing 문제를 해결하고, 데이터 노드가 stateless해져 장애 복구도 빨라졌습니다. SharedMergeTree는 한마디로 "클라우드 네이티브 MergeTree"입니다. 오픈소스 버전에도 점차 이 개념이 적용될 것으로 보이며, 대규모 클러스터 사용자에게 큰 이점을 줄 것입니다.
  • Late/Lazy Materialization: 앞서 Lazy Materialization 최적화를 상세히 다뤘습니다만, 여기서는 흐름 차원에서 재언급합니다. Lazy Materialization은 ClickHouse의 최근 엔진 최적화 레이어에서 가장 주목받는 개선으로, 불필요한 컬럼 읽기와 연산을 최대한 뒤로 미루어 없애는 것입니다. 특히 ORDER BY + LIMIT 패턴 쿼리에서 최대 1000배까지 성능 개선 사례가 나왔을 정도로 효과적입니다. 2025년 현재 이 기능은 기본 활성화되어 있고 (optimize_read_in_order 등의 세부 설정으로 제어), 향후 다른 쿼리 패턴 (예: 여러 큰 테이블 중 일부만 필요할 때 컬럼 late reading)에도 확장될 계획입니다. Lazy Materialization은 Vectorized execution의 흐름을 일부 변경해야 했기에 (필요 컬럼을 나눠 읽는 pipeline), 도입이 큰 변화였는데, 이는 ClickHouse가 성숙해진 엔진으로 더 폭넓은 최적화를 도입할 수 있음을 보여줍니다. 더불어 워크로드 인식 최적화 (Workload-aware opt.)의 시작으로 평가받습니다.
  • 기타 최신 기능:
    • 벡터 데이터 타입 & AI 통합: 2023년부터 ClickHouse는 벡터 임베딩 데이터를 저장하고, 코사인 유사도나 L2 거리 기반의 벡터 검색 기능을 실험 중입니다. 예컨대 CREATE TABLE vecs (id UInt64, embedding Array(Float32)) 식으로 벡터를 저장하고, embedding ANN INDEX를 붙여 근사 최근접 검색을 최적화합니다. 이는 LLM, GenAI 응용을 위한 것으로, OpenAI 등의 임베딩을 ClickHouse에서 직접 대량 저장/검색하게 해줍니다.
    • MQTT/Edge 통합: 실시간 스트림 수집을 위해 Kafka, RabbitMQ, NATS, MQTT 등 메시지큐 통합 엔진들이 추가되고 있습니다 (ENGINE = Kafka 이미 널리 사용). 이를 통해 ClickHouse가 실시간 데이터 파이프라인의 sink로 활용됩니다.
    • SQL 표준 및 쿼리 편의: WINDOW 함수(21.x), JSON 지원 강화, PostgreSQL 호환 프로토콜 등을 통해 ClickHouse의 SQL이 점점 표준화되고 있습니다. 옵티마이저 측면에선 EXPLAIN이 크게 개선되어 AST, 계획, 파이프라인, 비용 추정 등을 단계별로 볼 수 있게 되었고, 이는 개발자들이 쿼리 튜닝을 이해하는 데 도움을 줍니다.
    • 보안 및 안정성: Atomic database로 DDL atomicity를 높인 후, 오류내성 측면에서도 improvements (프로세스 crash 시 자동 재시작, 분산 DDL 실패시 재동기화 등) 이 이뤄지고 있습니다. 복제는 기존 ZooKeeper에서 Keeper로 점차 이전되어, 운영 복잡성을 낮추고 성능을 높입니다.

맺으며, ClickHouse는 내부 구조 측면에서 컬럼지향 + MergeTree + 벡터화라는 강점을 바탕으로 지속적인 발전을 거듭하고 있습니다. 위에서 살펴본 최신 동향들은 클라우드 네이티브 기능 (SharedMergeTree), 데이터 레이크 연계(Iceberg), 최적화 심화(Lazy Materialization)로 요약되며, 이는 모두 확장성과 호환성, 그리고 성능을 극대화하려는 방향입니다. 이러한 혁신과 더불어 오픈소스 커뮤니티의 활발한 기여 덕분에, ClickHouse는 현대 데이터 스택에서 핵심적인 역할을 하는 초고속 OLAP 데이터베이스로 자리잡고 있습니다.

참고 문헌: ClickHouse 공식 Architecture Overview 및 개발자 문서, ClickHouse Blog 기사 (Lazy Materialization, Iceberg integration 등), ChaosGenius ClickHouse Architecture 101 (2025) 등.