1. 들어가며: OLTP vs OLAP, 왜 ClickHouse인가
데이터베이스를 선택할 때 가장 먼저 구분해야 하는 것은 워크로드의 성격이다. OLTP(Online Transaction Processing)는 짧은 트랜잭션과 행 단위 읽기/쓰기에 최적화되어 있고, OLAP(Online Analytical Processing)는 수십억 행에 걸친 집계 쿼리와 분석에 특화되어 있다.
| 특성 | OLTP | OLAP |
| ---------------- | ----------------------------------- | ------------------------------------------------------- |
| 대표 쿼리 | `SELECT * FROM users WHERE id = 42` | `SELECT country, COUNT(*) FROM events GROUP BY country` |
| 데이터 접근 패턴 | 행 단위, 포인트 룩업 | 컬럼 단위, 전체 스캔/집계 |
| 동시성 | 수천 TPS의 짧은 트랜잭션 | 소수의 무거운 분석 쿼리 |
| 지연시간 목표 | 1~10ms | 100ms~수 초 |
| 대표 엔진 | PostgreSQL, MySQL, Oracle | ClickHouse, BigQuery, Druid |
PostgreSQL에 `pg_analytics` 확장을 붙이거나, TimescaleDB를 활용해 분석 쿼리를 처리하는 방법도 있다. 하지만 데이터가 수십억 행을 넘어가고, 실시간에 가까운 대시보드를 운영해야 하며, 서브초(sub-second) 수준의 응답이 필요하다면 전용 OLAP 엔진이 필수다.
ClickHouse는 Yandex에서 웹 분석 서비스 Metrica를 위해 2016년 오픈소스로 공개한 컬럼 지향 분석 데이터베이스다. 현재 ClickHouse Inc.가 개발을 주도하고 있으며, 2025~2026년에 걸쳐 벡터 검색, SharedCatalog, AI 통합 등 혁신적인 기능들이 추가되고 있다. 단일 서버에서도 초당 수십억 행을 스캔할 수 있으며, 클러스터로 확장하면 페타바이트 규모의 데이터를 서브초 지연시간으로 분석할 수 있다.
ClickHouse를 선택해야 하는 시나리오는 명확하다.
- 이벤트/로그/메트릭 데이터가 일 단위로 수억 건 이상 적재되는 경우
- 대시보드가 수백 개의 GROUP BY 집계를 서브초로 반환해야 하는 경우
- INSERT는 배치 단위로 많이, UPDATE/DELETE는 거의 없는 append-heavy 워크로드
- 컬럼 단위 압축으로 스토리지 비용을 극적으로 줄이고 싶은 경우
반대로 ClickHouse가 적합하지 않은 경우도 있다. 행 단위 포인트 룩업이 주 워크로드이거나, ACID 트랜잭션이 필수이거나, 빈번한 UPDATE/DELETE가 필요한 경우에는 PostgreSQL이나 MySQL이 더 적합하다.
2. ClickHouse 아키텍처: 컬럼 스토리지와 압축
컬럼 지향 스토리지의 원리
전통적인 행 지향(row-oriented) 데이터베이스는 하나의 행에 속한 모든 컬럼 값을 연속된 블록에 저장한다. `SELECT country FROM events`처럼 단일 컬럼만 필요한 쿼리에서도 나머지 모든 컬럼 데이터를 디스크에서 읽어야 하므로 I/O 낭비가 크다.
ClickHouse는 각 컬럼의 값을 별도 파일에 연속으로 저장한다. 분석 쿼리가 필요로 하는 컬럼만 읽으므로 디스크 I/O가 극적으로 줄어든다. 동일 타입의 값이 연속으로 배치되므로 압축률도 훨씬 높다.
행 지향 (PostgreSQL):
┌──────┬─────────┬─────────┬──────────┐
│ id │ country │ browser │ duration │ ← 행 1
├──────┼─────────┼─────────┼──────────┤
│ id │ country │ browser │ duration │ ← 행 2
└──────┴─────────┴─────────┴──────────┘
컬럼 지향 (ClickHouse):
┌──────────────────────┐
│ id: 1, 2, 3, 4, ... │ ← 컬럼 파일 1
├──────────────────────┤
│ country: KR, US, ... │ ← 컬럼 파일 2
├──────────────────────┤
│ browser: Chrome, ... │ ← 컬럼 파일 3
├──────────────────────┤
│ duration: 42, 17, ...│ ← 컬럼 파일 4
└──────────────────────┘
압축 알고리즘: LZ4 vs ZSTD
ClickHouse는 기본적으로 LZ4 압축을 사용한다. LZ4는 압축률은 중간이지만 압축/해제 속도가 매우 빠르다. 더 높은 압축률이 필요하면 ZSTD를 선택할 수 있다.
| 알고리즘 | 압축률 | 압축 속도 | 해제 속도 | 권장 시나리오 |
| ----------------- | ------------ | --------- | --------- | ------------------------------- |
| LZ4 | 보통 (3~5x) | 매우 빠름 | 매우 빠름 | 실시간 쿼리 위주, 핫 데이터 |
| ZSTD | 높음 (5~10x) | 빠름 | 빠름 | 콜드 데이터, 스토리지 절약 우선 |
| Delta + ZSTD | 매우 높음 | 보통 | 보통 | 타임스탬프, 연속 증가 정수 |
| DoubleDelta + LZ4 | 높음 | 빠름 | 빠름 | 메트릭 데이터(거의 일정한 간격) |
컬럼별로 코덱을 지정할 수 있다는 점이 ClickHouse의 강력한 장점이다.
CREATE TABLE events
(
event_time DateTime CODEC(DoubleDelta, LZ4),
user_id UInt64 CODEC(Delta, ZSTD(3)),
country LowCardinality(String) CODEC(ZSTD(1)),
duration Float32 CODEC(Gorilla, LZ4),
raw_json String CODEC(ZSTD(5))
)
ENGINE = MergeTree()
ORDER BY (event_time, user_id);
`event_time`처럼 단조 증가하는 타임스탬프에는 DoubleDelta 코덱이 효과적이고, `duration`처럼 부동소수점 메트릭에는 Gorilla 코덱이 적합하다. 자주 접근하지 않는 큰 JSON 필드에는 ZSTD 레벨을 높여 스토리지를 절약한다.
벡터화 쿼리 실행
ClickHouse는 한 번에 하나의 행이 아니라 수천 개의 값을 묶은 컬럼 벡터 단위로 연산을 수행한다. 이를 통해 CPU 캐시 효율이 극대화되고, SIMD 명령어(SSE4.2, AVX2, AVX-512)를 활용한 병렬 처리가 가능하다. 단일 코어에서도 초당 수억 행을 처리할 수 있는 이유가 바로 이 벡터화 실행 엔진에 있다.
3. MergeTree 엔진 패밀리
MergeTree는 ClickHouse의 핵심 테이블 엔진이다. 이름 그대로 데이터를 파트(part) 단위로 저장하고, 백그라운드에서 파트를 병합(merge)하는 LSM-Tree 유사 구조를 따른다.
MergeTree 기본 동작
1. **INSERT**: 데이터가 메모리 버퍼에 쌓이다가 임계값에 도달하면 정렬된 파트로 디스크에 기록된다.
2. **Merge**: 백그라운드 스레드가 작은 파트들을 주기적으로 병합하여 큰 파트를 만든다. 병합 과정에서 중복 제거, 집계 등의 특수 로직이 실행될 수 있다.
3. **SELECT**: 쿼리 시 Primary Key(Sparse Index)를 활용해 필요한 그래뉼(granule)만 읽는다. 기본 그래뉼 크기는 8,192행이다.
엔진 패밀리 비교
| 엔진 | 핵심 기능 | 사용 사례 |
| ---------------------------- | ----------------------------------------------- | ------------------------------- |
| MergeTree | 기본 엔진, ORDER BY 기반 정렬 저장 | 범용 분석 테이블 |
| ReplacingMergeTree | 동일 ORDER BY 키의 중복 행을 최신 버전으로 대체 | CDC 파이프라인, 상태 스냅샷 |
| SummingMergeTree | 병합 시 수치 컬럼을 자동 합산 | 카운터, 누적 메트릭 |
| AggregatingMergeTree | 병합 시 `AggregateFunction` 상태를 자동 병합 | Materialized View 사전 집계 |
| CollapsingMergeTree | sign 컬럼(+1/-1)으로 행 삽입/취소 | 변경 로그 기반 실시간 집계 |
| VersionedCollapsingMergeTree | Collapsing + 버전 관리 | 순서가 보장되지 않는 CDC 스트림 |
ReplacingMergeTree 예제
CDC(Change Data Capture) 파이프라인에서 같은 키에 대한 여러 버전의 행이 들어올 때 최신 상태만 유지하고 싶다면 ReplacingMergeTree를 사용한다.
CREATE TABLE user_profiles
(
user_id UInt64,
name String,
email String,
updated_at DateTime
)
ENGINE = ReplacingMergeTree(updated_at)
ORDER BY user_id;
-- 동일 user_id로 여러 번 INSERT 가능
INSERT INTO user_profiles VALUES (1, '김영주', 'yj@example.com', '2026-03-01 10:00:00');
INSERT INTO user_profiles VALUES (1, '김영주', 'yj_new@example.com', '2026-03-06 09:00:00');
-- 병합 전에는 두 행이 모두 보일 수 있음
-- FINAL 키워드로 최신 버전만 조회
SELECT * FROM user_profiles FINAL WHERE user_id = 1;
**주의**: `FINAL` 키워드는 쿼리 시점에 중복 제거를 수행하므로 성능 오버헤드가 있다. 프로덕션에서는 `OPTIMIZE TABLE ... FINAL`을 정기적으로 실행하거나, 쿼리 레벨에서 `argMax` 함수를 활용하는 것이 더 효율적인 경우가 많다.
4. 스키마 설계 베스트 프랙티스
ORDER BY 설계 원칙
ORDER BY는 ClickHouse에서 가장 중요한 설계 결정이다. Primary Key(Sparse Index)로도 기능하며, 데이터의 물리적 정렬 순서를 결정한다.
**핵심 원칙**:
- 가장 자주 필터링하는 컬럼을 앞에 배치한다.
- 카디널리티가 낮은 컬럼을 앞에, 높은 컬럼을 뒤에 배치한다.
- 시계열 데이터는 `(low_cardinality_dim, timestamp)` 순서가 일반적이다.
-- 잘못된 설계: 고카디널리티 컬럼이 앞에
CREATE TABLE events_bad
(
event_id UUID,
user_id UInt64,
event_type LowCardinality(String),
event_time DateTime
)
ENGINE = MergeTree()
ORDER BY (event_id); -- UUID가 첫 번째: sparse index 효율 극히 낮음
-- 올바른 설계: 필터 패턴에 맞춘 정렬
CREATE TABLE events_good
(
event_id UUID,
user_id UInt64,
event_type LowCardinality(String),
event_time DateTime
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_type, user_id, event_time);
파티셔닝 전략
PARTITION BY는 데이터를 물리적으로 분리하여 오래된 데이터의 삭제(TTL 또는 DROP PARTITION)를 효율적으로 만들고, 쿼리 시 파티션 프루닝을 가능하게 한다.
| 파티션 키 | 파티션 수(1년 기준) | 권장 시나리오 |
| -------------------------- | ------------------- | ---------------------------------- |
| `toYYYYMM(event_time)` | 12개 | 월 단위 보존 정책, 대부분의 시계열 |
| `toYYYYMMDD(event_time)` | 365개 | 일 단위 TTL이 필요한 로그 |
| `toMonday(event_time)` | 52개 | 주 단위 분석이 주 패턴 |
| `intDiv(user_id, 1000000)` | 가변 | 사용자 ID 범위 기반 조회 |
**경고**: 파티션 수가 지나치게 많아지면 (수천 개 이상) ZooKeeper/ClickHouse Keeper의 메타데이터 부하가 급증하고, 파트 관리 오버헤드가 커진다. 월 단위 파티셔닝이 가장 무난하다.
LowCardinality 타입
카디널리티가 수천 이하인 문자열 컬럼에는 반드시 `LowCardinality(String)`을 사용한다. 내부적으로 딕셔너리 인코딩을 적용하여 저장 공간을 줄이고 쿼리 성능을 높인다.
-- LowCardinality 적용 전후 비교
CREATE TABLE logs_v1 (level String) ENGINE = MergeTree() ORDER BY tuple();
CREATE TABLE logs_v2 (level LowCardinality(String)) ENGINE = MergeTree() ORDER BY tuple();
-- 데이터 삽입 후 크기 비교
SELECT
table,
formatReadableSize(sum(data_compressed_bytes)) AS compressed,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed
FROM system.columns
WHERE database = currentDatabase() AND table IN ('logs_v1', 'logs_v2')
GROUP BY table;
카디널리티가 10,000을 넘어가면 LowCardinality의 이점이 줄어들고 오히려 오버헤드가 될 수 있으므로, 적용 전에 `SELECT uniq(column) FROM table`로 확인한다.
5. Materialized View와 사전 집계
Materialized View(MV)는 ClickHouse에서 실시간 사전 집계를 구현하는 핵심 메커니즘이다. PostgreSQL의 Materialized View와는 근본적으로 다르다. ClickHouse의 MV는 INSERT 트리거처럼 동작하여, 원본 테이블에 데이터가 삽입될 때마다 자동으로 변환/집계된 결과를 대상 테이블에 기록한다.
AggregatingMergeTree + State/Merge 패턴
가장 강력한 사전 집계 패턴은 AggregatingMergeTree 엔진과 `-State`/`-Merge` 콤비네이터를 결합하는 것이다.
-- 1단계: 원본 이벤트 테이블
CREATE TABLE raw_events
(
event_time DateTime,
event_type LowCardinality(String),
user_id UInt64,
revenue Float64
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_type, event_time);
-- 2단계: 사전 집계 대상 테이블 (AggregatingMergeTree)
CREATE TABLE hourly_stats
(
hour DateTime,
event_type LowCardinality(String),
user_count AggregateFunction(uniq, UInt64),
total_rev AggregateFunction(sum, Float64),
p99_rev AggregateFunction(quantile(0.99), Float64)
)
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (event_type, hour);
-- 3단계: Materialized View 생성
CREATE MATERIALIZED VIEW hourly_stats_mv
TO hourly_stats
AS SELECT
toStartOfHour(event_time) AS hour,
event_type,
uniqState(user_id) AS user_count,
sumState(revenue) AS total_rev,
quantileState(0.99)(revenue) AS p99_rev
FROM raw_events
GROUP BY hour, event_type;
-- 4단계: 조회 시 -Merge 콤비네이터 사용
SELECT
hour,
event_type,
uniqMerge(user_count) AS unique_users,
sumMerge(total_rev) AS total_revenue,
quantileMerge(0.99)(p99_rev) AS revenue_p99
FROM hourly_stats
WHERE hour >= '2026-03-01'
GROUP BY hour, event_type
ORDER BY hour;
이 패턴의 핵심 이점은 다음과 같다.
- **실시간성**: INSERT가 발생할 때마다 즉시 집계가 업데이트된다.
- **정확성**: `-State`/`-Merge` 함수는 중간 상태를 바이너리로 저장하므로, 파트 병합 후에도 수학적으로 정확한 결과를 보장한다.
- **쿼리 속도**: 수십억 행의 원본 대신 집계 테이블을 조회하므로 응답이 밀리초 단위로 떨어진다.
- **근사값 지원**: `uniq`(HyperLogLog 기반), `quantile`(t-digest 기반) 등 근사 집계 함수도 State/Merge 패턴과 완벽히 호환된다.
**경고**: Materialized View는 INSERT 시점에만 트리거된다. 기존 데이터에 대해 MV를 소급 적용하려면 `INSERT INTO hourly_stats SELECT ... FROM raw_events`로 수동 백필이 필요하다.
6. 쿼리 최적화 기법
PREWHERE: WHERE보다 먼저 필터링
ClickHouse 고유의 PREWHERE 절은 WHERE 조건 중 일부를 컬럼 읽기 전에 먼저 평가하여, 불필요한 컬럼 데이터의 디스크 I/O를 줄인다. 최신 버전에서는 옵티마이저가 자동으로 WHERE를 PREWHERE로 변환하지만, 명시적으로 지정할 수도 있다.
-- PREWHERE를 명시적으로 사용
SELECT user_id, raw_json
FROM raw_events
PREWHERE event_type = 'purchase'
WHERE revenue > 100.0;
위 쿼리에서 `event_type` 컬럼만 먼저 읽어 필터링한 후, 통과한 행에 대해서만 `raw_json`(큰 String 컬럼)을 읽는다. 큰 컬럼이 포함된 테이블에서 PREWHERE의 효과는 극적이다.
샘플링으로 대략적 결과 빠르게 얻기
데이터 탐색 단계에서는 정확한 집계보다 빠른 피드백이 더 중요할 수 있다. SAMPLE 절을 사용하면 데이터의 일부만 읽어 근사 결과를 반환한다.
-- 테이블 생성 시 SAMPLE BY 지정 필요
CREATE TABLE events_sampled
(
event_time DateTime,
user_id UInt64,
event_type LowCardinality(String),
revenue Float64
)
ENGINE = MergeTree()
ORDER BY (event_type, sipHash64(user_id))
SAMPLE BY sipHash64(user_id);
-- 전체 데이터의 10%만 샘플링하여 조회
SELECT
event_type,
count() * 10 AS estimated_count, -- 10배 보정
avg(revenue) AS avg_revenue
FROM events_sampled
SAMPLE 0.1
GROUP BY event_type;
병렬 실행과 리소스 제어
ClickHouse는 기본적으로 가용한 모든 CPU 코어를 사용하여 쿼리를 병렬 실행한다. 프로덕션 환경에서는 쿼리 간 리소스 경합을 방지하기 위해 적절한 제한이 필요하다.
-- 쿼리 레벨 리소스 제한
SET max_threads = 8; -- 쿼리당 최대 스레드 수
SET max_memory_usage = 10000000000; -- 쿼리당 최대 메모리 (10GB)
SET max_execution_time = 30; -- 최대 실행 시간 (초)
SET max_rows_to_read = 1000000000; -- 최대 읽기 행 수
-- 사용자/프로필 단위 제한 (config.xml 또는 SQL)
CREATE SETTINGS PROFILE 'analyst' SETTINGS
max_threads = 4,
max_memory_usage = 5000000000,
max_execution_time = 60
TO analyst_role;
쿼리 성능 분석
느린 쿼리를 진단할 때는 `clickhouse-client`의 프로파일링 옵션을 활용한다.
쿼리 실행 통계 포함 실행
clickhouse-client --query "
SELECT event_type, count()
FROM raw_events
WHERE event_time >= '2026-03-01'
GROUP BY event_type
FORMAT PrettyCompactMonoBlock
SETTINGS send_logs_level = 'trace'
"
system.query_log에서 느린 쿼리 분석
clickhouse-client --query "
SELECT
query_duration_ms,
read_rows,
formatReadableSize(read_bytes) AS read_size,
formatReadableSize(memory_usage) AS peak_memory,
query
FROM system.query_log
WHERE type = 'QueryFinish'
AND query_duration_ms > 1000
ORDER BY query_duration_ms DESC
LIMIT 10
"
7. ClickHouse vs PostgreSQL vs BigQuery vs Druid
OLAP 워크로드에서 자주 비교되는 네 가지 엔진의 특성을 정리한다.
| 항목 | ClickHouse | PostgreSQL (+ pg_analytics) | BigQuery | Apache Druid |
| ------------- | ------------------------------- | -------------------------------- | ------------------------------ | ----------------------------- |
| 스토리지 모델 | 컬럼 지향 | 행 지향 (확장으로 컬럼 지원) | 컬럼 지향 (Capacitor) | 컬럼 지향 (세그먼트) |
| 배포 모델 | 셀프호스팅 / ClickHouse Cloud | 셀프호스팅 / 매니지드 | 완전 관리형 SaaS | 셀프호스팅 / Imply Cloud |
| 실시간 수집 | INSERT 즉시 조회 가능 | INSERT 즉시 조회 가능 | 스트리밍 버퍼 (수 초 지연) | 실시간 수집 노드 (수 초 지연) |
| 쿼리 지연시간 | 밀리초~초 | 초~분 (대규모 집계) | 초~십 초 (콜드 스타트) | 밀리초~초 |
| 압축률 | 매우 높음 (10~40x) | 보통 (2~4x) | 높음 (관리형) | 높음 (5~10x) |
| SQL 호환성 | ClickHouse SQL (표준 호환 높음) | 표준 SQL 완전 지원 | 표준 SQL (GoogleSQL) | Druid SQL (제한적) |
| JOIN 성능 | 제한적 (대형 JOIN 주의) | 우수 (해시, 머지, 네스티드 루프) | 우수 (분산 셔플) | 미지원 (룩업 JOIN만) |
| UPDATE/DELETE | 비동기 Mutation (무거움) | 즉시 처리 (MVCC) | DML 지원 (비용 발생) | 미지원 |
| 비용 모델 | 인프라 비용 (예측 가능) | 인프라 비용 (예측 가능) | 스캔 바이트 과금 (예측 어려움) | 인프라 비용 (예측 가능) |
| 학습 곡선 | 중간 | 낮음 | 낮음 (SQL 친숙) | 높음 (세그먼트 개념) |
**핵심 선택 기준**:
- **이미 PostgreSQL을 쓰고 있고 데이터가 수십 GB 이하**: PostgreSQL에 머물러라. pg_analytics 확장으로 충분할 수 있다.
- **수십 TB 이상, 자체 인프라, 서브초 응답 필요**: ClickHouse가 최적이다.
- **인프라 관리를 원하지 않고 비용에 유연**: BigQuery가 적합하다.
- **고성능 실시간 대시보드가 핵심이고 JOIN이 불필요**: Druid도 선택지다.
8. 2025~2026년 주요 신기능
ClickHouse는 2025년 한 해 동안 50개 이상의 릴리스를 통해 대규모 기능 업데이트를 진행했다. 2026년 초까지의 주요 변화를 정리한다.
벡터 검색 (Vector Search)
ClickHouse 25.1부터 `usearch` 인덱스를 활용한 ANN(Approximate Nearest Neighbor) 검색이 정식 지원된다. 별도의 벡터 DB 없이도 분석 데이터와 임베딩 벡터를 같은 테이블에서 관리할 수 있다.
CREATE TABLE embeddings
(
doc_id UInt64,
content String,
vector Array(Float32),
INDEX vec_idx vector TYPE usearch(256) GRANULARITY 1
)
ENGINE = MergeTree()
ORDER BY doc_id;
-- 코사인 유사도 기반 검색
SELECT doc_id, content,
cosineDistance(vector, [0.1, 0.2, ...]) AS distance
FROM embeddings
ORDER BY distance ASC
LIMIT 10;
SharedCatalog와 오브젝트 스토리지 분리
ClickHouse Cloud에서 도입된 SharedCatalog는 컴퓨팅과 스토리지를 완전히 분리하여, 여러 컴퓨팅 노드가 S3/GCS의 동일 데이터에 접근할 수 있게 한다. 이를 통해 읽기 전용 레플리카를 즉시 스케일아웃할 수 있다.
AI 통합과 쿼리 생성
ClickHouse는 LLM 기반 자연어-to-SQL 변환, AI 기반 쿼리 최적화 제안 등의 기능을 ClickHouse Cloud 콘솔에 통합하고 있다. 또한 UDF(User Defined Function)를 통해 외부 ML 모델을 쿼리 파이프라인에 직접 연결할 수 있다.
기타 주요 개선
- **Parallel Replicas**: 단일 쿼리를 여러 레플리카에 분산 실행하여 지연시간을 줄이는 기능이 GA 되었다.
- **Lightweight DELETE/UPDATE**: Mutation 대신 행 마스킹 기반의 경량 삭제가 안정화되었다.
- **Refreshable Materialized View**: 주기적으로 전체 갱신되는 MV 지원이 추가되었다 (기존의 INSERT 트리거 방식과 별도).
9. 운영 시 주의사항
디스크 관리
ClickHouse는 백그라운드 머지 과정에서 일시적으로 추가 디스크 공간을 사용한다. 프로덕션에서는 최소 30%의 여유 디스크 공간을 확보해야 하며, 디스크 사용률 모니터링은 필수다.
/etc/clickhouse-server/config.d/storage.yaml
멀티 디스크(핫/콜드) 스토리지 정책
storage_configuration:
disks:
hot:
type: local
path: /data/clickhouse/hot/
cold:
type: s3
endpoint: https://s3.ap-northeast-2.amazonaws.com/my-bucket/clickhouse/
access_key_id: '${S3_ACCESS_KEY}'
secret_access_key: '${S3_SECRET_KEY}'
policies:
tiered:
volumes:
hot_volume:
disk: hot
max_data_part_size_bytes: 10737418240 # 10GB
cold_volume:
disk: cold
move_factor: 0.8 # 핫 디스크 80% 차면 콜드로 이동
TTL(Time to Live)을 활용하면 오래된 데이터를 자동으로 콜드 스토리지로 이동하거나 삭제할 수 있다.
ALTER TABLE raw_events
MODIFY TTL
event_time + INTERVAL 30 DAY TO VOLUME 'cold_volume',
event_time + INTERVAL 365 DAY DELETE;
복제와 고가용성
프로덕션에서는 반드시 ReplicatedMergeTree 엔진을 사용한다. ClickHouse Keeper(ZooKeeper 호환 합의 프로토콜)가 레플리카 간 메타데이터 동기화를 담당한다.
-- Replicated 테이블 생성
CREATE TABLE raw_events ON CLUSTER 'production'
(
event_time DateTime,
event_type LowCardinality(String),
user_id UInt64,
revenue Float64
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/raw_events', '{replica}')
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_type, event_time);
**ClickHouse Keeper 운영 주의사항**:
- 반드시 홀수 노드(3 또는 5)로 구성한다. 3노드 구성에서 1노드 장애까지 허용한다.
- Keeper 노드에는 전용 SSD를 할당한다. 디스크 지연이 합의 프로토콜 성능에 직접 영향을 준다.
- `raft_logs_level`을 적절히 설정하여 디버깅이 가능하도록 한다.
- Keeper 로그와 스냅샷의 보관 기간을 설정하여 디스크 폭발을 방지한다.
백업 전략
ClickHouse 내장 백업 기능을 활용하거나, `clickhouse-backup` 도구를 사용한다.
내장 BACKUP 명령어 (ClickHouse 22.8+)
clickhouse-client --query "
BACKUP TABLE raw_events
TO S3('https://s3.ap-northeast-2.amazonaws.com/my-backup-bucket/raw_events_20260306/',
'${S3_ACCESS_KEY}', '${S3_SECRET_KEY}')
SETTINGS compression_method = 'lz4'
"
복원
clickhouse-client --query "
RESTORE TABLE raw_events
FROM S3('https://s3.ap-northeast-2.amazonaws.com/my-backup-bucket/raw_events_20260306/',
'${S3_ACCESS_KEY}', '${S3_SECRET_KEY}')
"
clickhouse-backup 도구 사용 (더 세밀한 제어)
clickhouse-backup create --tables="default.raw_events" daily_20260306
clickhouse-backup upload daily_20260306
clickhouse-backup list remote
INSERT 배치 최적화
ClickHouse에 데이터를 삽입할 때 가장 흔한 실수는 행 단위로 INSERT를 보내는 것이다. 매 INSERT마다 새로운 파트가 생성되고, 과도한 파트 수는 "Too many parts" 오류를 유발한다.
**권장사항**:
- 최소 1,000행 이상을 한 번에 배치 INSERT한다. 이상적으로는 10,000~100,000행이다.
- Kafka, RabbitMQ 등에서 소비할 때는 ClickHouse의 테이블 엔진(Kafka engine)을 활용하여 자동 배치를 구성한다.
- `async_insert=1` 설정을 활용하면 ClickHouse가 클라이언트의 작은 INSERT를 서버 측에서 모아 배치로 처리한다.
-- async_insert 활성화
SET async_insert = 1;
SET wait_for_async_insert = 1; -- INSERT 응답 전 실제 기록 대기
SET async_insert_max_data_size = 10485760; -- 10MB마다 플러시
SET async_insert_busy_timeout_ms = 1000; -- 최대 1초 대기
10. 실패 사례와 복구 절차
사례 1: "Too many parts" 오류
**증상**: INSERT가 `DB::Exception: Too many parts (N). Merges are processing significantly slower than inserts` 오류와 함께 실패한다.
**원인**: 행 단위 INSERT를 초당 수천 번 실행하여 파트 수가 폭증했다. 기본 임계값은 파티션당 300개이다.
**복구 절차**:
1. INSERT 트래픽을 즉시 중단하거나 배치 크기를 늘린다.
2. 백그라운드 머지가 따라잡을 때까지 대기한다. `system.merges` 테이블에서 진행 상황을 모니터링한다.
3. 필요시 `OPTIMIZE TABLE events FINAL`을 수동 실행한다. (주의: 이 명령은 디스크 I/O가 매우 크므로 피크 시간을 피해야 한다.)
4. 근본 원인을 해결한다: async_insert 활성화, 배치 크기 증가, Kafka 엔진 도입 등.
-- 파티션별 파트 수 확인
SELECT
partition,
count() AS part_count,
formatReadableSize(sum(bytes_on_disk)) AS total_size
FROM system.parts
WHERE table = 'raw_events' AND active
GROUP BY partition
ORDER BY part_count DESC;
-- 머지 진행 상황 모니터링
SELECT
table, partition_id,
progress, elapsed,
formatReadableSize(total_size_bytes_compressed) AS size
FROM system.merges
WHERE table = 'raw_events';
사례 2: 레플리카 불일치 (Replica Divergence)
**증상**: 두 레플리카의 데이터가 서로 달라 `SELECT count()`결과가 다르다. `system.replicas`에서 `is_session_expired` 또는 `queue_size`가 비정상적으로 높다.
**원인**: ClickHouse Keeper 연결 끊김, 네트워크 파티션, 또는 레플리카 중 하나가 장기간 다운된 후 복구되면서 발생할 수 있다.
**복구 절차**:
1. `system.replicas`에서 상태를 확인한다.
2. 복제 큐가 정상적으로 처리되고 있는지 확인한다.
3. 큐가 막혀 있다면 `SYSTEM RESTART REPLICA table_name`을 시도한다.
4. 최악의 경우 문제 레플리카의 데이터를 제거하고 정상 레플리카에서 다시 복제한다.
-- 레플리카 상태 확인
SELECT
database, table,
is_leader, is_readonly, is_session_expired,
future_parts, parts_to_check,
queue_size, inserts_in_queue, merges_in_queue,
log_pointer, total_replicas, active_replicas
FROM system.replicas
WHERE table = 'raw_events';
-- 복제 재시작 시도
SYSTEM RESTART REPLICA raw_events;
-- 그래도 안 되면 레플리카를 정상 복제본에서 재동기화
-- (주의: 해당 레플리카의 로컬 데이터가 삭제됨)
SYSTEM RESTORE REPLICA raw_events;
사례 3: 디스크 풀로 인한 서비스 중단
**증상**: 백그라운드 머지 실패, INSERT 실패, 극단적인 경우 서버 시작 불가.
**원인**: TTL 미설정, 압축 코덱 미적용, 또는 예상보다 빠른 데이터 증가.
**복구 절차**:
1. 가장 큰 파티션을 식별하여 불필요한 데이터를 `DROP PARTITION`으로 즉시 제거한다.
2. 임시 디스크 공간을 확보한다(시스템 로그, 임시 파일 삭제).
3. TTL 정책을 추가하여 자동 삭제/이동을 설정한다.
4. 멀티 디스크 정책으로 콜드 스토리지(S3)를 추가한다.
-- 파티션별 디스크 사용량 확인
SELECT
partition,
formatReadableSize(sum(bytes_on_disk)) AS disk_usage,
min(min_time) AS oldest_data,
max(max_time) AS newest_data,
count() AS part_count
FROM system.parts
WHERE table = 'raw_events' AND active
GROUP BY partition
ORDER BY sum(bytes_on_disk) DESC;
-- 오래된 파티션 즉시 삭제 (비가역적!)
ALTER TABLE raw_events DROP PARTITION '202501';
사례 4: Materialized View 데이터 불일치
**증상**: 원본 테이블과 MV의 집계 결과가 맞지 않는다.
**원인**: MV 생성 전에 이미 존재하던 데이터는 MV에 반영되지 않는다. 또한 INSERT 중 서버 장애가 발생하면 원본에는 기록되었으나 MV에는 반영되지 않을 수 있다.
**복구 절차**:
1. MV 대상 테이블을 TRUNCATE한다.
2. 원본 테이블에서 수동으로 백필한다.
3. 대규모 백필 시에는 날짜 범위별로 나누어 실행하여 메모리 사용을 제어한다.
-- MV 대상 테이블 초기화 후 백필
TRUNCATE TABLE hourly_stats;
INSERT INTO hourly_stats
SELECT
toStartOfHour(event_time) AS hour,
event_type,
uniqState(user_id) AS user_count,
sumState(revenue) AS total_rev,
quantileState(0.99)(revenue) AS p99_rev
FROM raw_events
WHERE event_time >= '2026-01-01' AND event_time < '2026-02-01'
GROUP BY hour, event_type;
-- 월별로 반복 실행
11. 프로덕션 체크리스트
ClickHouse를 프로덕션에 배포하기 전에 확인해야 할 항목들을 정리한다.
스키마 설계
- [ ] ORDER BY 키가 주요 쿼리 패턴과 일치하는가
- [ ] PARTITION BY가 적절한 단위(월/주)로 설정되었는가 (파티션 수 1,000개 미만)
- [ ] 저카디널리티 문자열에 `LowCardinality` 타입이 적용되었는가
- [ ] 컬럼별 최적 압축 코덱이 지정되었는가
- [ ] Nullable 사용을 최소화했는가 (Nullable은 추가 컬럼 파일 생성)
데이터 수집
- [ ] INSERT 배치 크기가 최소 1,000행 이상인가
- [ ] async_insert가 적절히 설정되었는가 (소규모 INSERT가 빈번한 경우)
- [ ] Kafka/RabbitMQ 연동 시 테이블 엔진을 활용하고 있는가
- [ ] "Too many parts" 알림이 설정되어 있는가
쿼리 성능
- [ ] 대시보드 쿼리에 Materialized View 사전 집계가 적용되었는가
- [ ] 사용자/역할별 리소스 제한(max_threads, max_memory_usage)이 설정되었는가
- [ ] system.query_log 기반 슬로우 쿼리 모니터링이 구축되었는가
- [ ] 대형 JOIN 대신 딕셔너리 또는 IN 서브쿼리를 사용하고 있는가
운영/안정성
- [ ] ReplicatedMergeTree로 최소 2 레플리카를 구성했는가
- [ ] ClickHouse Keeper가 3+ 홀수 노드로 운영되는가
- [ ] 디스크 사용률 80% 알림이 설정되어 있는가
- [ ] TTL 정책으로 데이터 보존 기간이 관리되고 있는가
- [ ] 백업이 자동화되어 있고 복원 테스트를 정기적으로 수행하는가
- [ ] 핫/콜드 스토리지 티어링이 구성되어 있는가 (대용량 데이터의 경우)
모니터링 지표
- [ ] `system.metrics`에서 `BackgroundMergesAndMutationsPoolTask` 모니터링
- [ ] `system.asynchronous_metrics`에서 `MaxPartCountForPartition` 모니터링 (300 초과 시 경고)
- [ ] `system.replicas`에서 `queue_size`, `is_readonly` 모니터링
- [ ] Prometheus + Grafana 대시보드로 주요 지표 시각화
12. 참고자료
- [ClickHouse Academic Overview - 아키텍처 심층 분석](https://clickhouse.com/docs/academic_overview)
- [ClickHouse Query Optimisation - The Definitive Guide](https://clickhouse.com/resources/engineering/clickhouse-query-optimisation-definitive-guide)
- [ClickHouse 2025 Roundup - 연간 주요 기능 정리](https://clickhouse.com/blog/clickhouse-2025-roundup)
- [ClickHouse vs PostgreSQL with Extensions - OLAP 성능 비교](https://www.tinybird.co/blog/clickhouse-vs-postgresql-with-extensions)
- [Data Modeling Guide for Real-Time Analytics with ClickHouse](https://www.rilldata.com/blog/data-modeling-guide-for-real-time-analytics-with-clickhouse)
- [ClickHouse 공식 문서 - MergeTree 엔진 패밀리](https://clickhouse.com/docs/engines/table-engines/mergetree-family)
- [ClickHouse 공식 문서 - Materialized View](https://clickhouse.com/docs/guides/developer/cascading-materialized-views)
현재 단락 (1/465)
데이터베이스를 선택할 때 가장 먼저 구분해야 하는 것은 워크로드의 성격이다. OLTP(Online Transaction Processing)는 짧은 트랜잭션과 행 단위 읽기/쓰기에...