Skip to content

필사 모드: ClickHouse 실시간 OLAP 분석 엔진: MergeTree 최적화부터 프로덕션 운영까지

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

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)는 짧은 트랜잭션과 행 단위 읽기/쓰기에...

작성 글자: 0원문 글자: 17,881작성 단락: 0/465