특별한 ClickHouse 엔진: MergeTree가 적합하지 않을 때
1. Memory 엔진 — 임시 데이터를 위한 RAM 테이블
배치를 빠르게 처리해야 한다고 상상해 보세요. 그룹화하고 중간 합계를 계산한 후 메인 테이블로 보내야 합니다. 데이터가 일시적이고 쿼리 기간 동안만 필요하기 때문에 디스크에 쓰고 싶지 않습니다.
Memory 엔진은 데이터를 전적으로 RAM에 저장합니다. 디스크 작업, 압축, 인덱스(기본 키 제외)가 없으므로 가장 빠른 엔진입니다. 하지만 대가가 있습니다: ClickHouse가 재시작되면 테이블이 비워집니다. 데이터가 유지되지 않습니다.
사용 시기:
- ETL 프로세스의 스테이징 테이블. 예를 들어, Kafka에서 백만 개의 배치를 로드하고 중복을 제거한 후에야 메인 MergeTree 테이블에 삽입합니다.
- 실시간 배당률 캐시 — 배당률이 매초 변경되며, 기록을 저장할 필요 없이 현재 스냅샷만 필요합니다.
- 작은 조회 테이블(최대 1,000만~1,500만 행)로, 스크립트 실행 시마다 다시 생성됩니다.
실시간 배당률 캐시 예시:
-- 현재 배당률 테이블 (RAM에 상주)
CREATE TABLE live_odds_cache
(
event_id UInt64, -- 이벤트 ID (경기)
market_id UInt32, -- 마켓 ID
selection_id UInt32, -- 선택 ID
odds Decimal(10,3), -- 배당률
updated_at DateTime
)
ENGINE = Memory()
ORDER BY (event_id, market_id, selection_id); -- ORDER BY는 필수지만 인덱스는 비효율적
데이터 삽입 (예: 스트림에서):
-- 새 배당률 도착, 삽입
INSERT INTO live_odds_cache VALUES (100500, 10, 200, 1.85, now());
-- 배팅을 위한 현재 배당률 읽기
SELECT odds FROM live_odds_cache
WHERE event_id = 100500 AND market_id = 10 AND selection_id = 200;
주의사항:
- Memory 테이블은 병합을 지원하지 않습니다. UPDATE(취소와 함께 삽입)를 많이 하면 메모리가 부풀어 오릅니다.
TRUNCATE로 지우세요. - ClickHouse 재시작 시 데이터가 손실됩니다. 중요한 데이터를 여기에 저장하지 마세요.
- 테이블 크기는 사용 가능한 RAM에 의해 제한됩니다. 64GB RAM 서버에서 테이블이 50GB로 커지면 서버가 다운됩니다.
비유: Memory 엔진은 화이트보드와 같습니다. 쓰기도 빠르고 읽기도 빠르지만, 청소부(재시작)가 오면 보드는 비워집니다.
2. Buffer 엔진 — 쓰기 전 INSERT 버퍼링
초당 10,000개의 배치가 있습니다. 각 배치는 별도의 INSERT입니다. 각각을 직접 MergeTree 테이블에 쓰면 ClickHouse가 수천 개의 작은 파트를 생성하여 백그라운드 병합 속도를 늦추고 성능을 저하시킵니다.
Buffer 엔진이 이 문제를 해결합니다: 메모리 버퍼에 INSERT를 모으고 조건(행 수, 크기 또는 시간)이 충족되면 대상 테이블에 큰 배치로 씁니다.
매개변수가 있는 구문:
CREATE TABLE bets_buffer AS bets -- bets 테이블의 구조 복사
ENGINE = Buffer(
'default', -- 대상 테이블 데이터베이스 이름
'bets', -- 대상 테이블 이름 (데이터가 여기로 플러시됨)
16, -- 병렬 플러시 스레드 수
10, -- 최소 지연 시간(초) (min_time)
100, -- 최대 지연 시간(초) (max_time)
10000, -- 플러시를 위한 최소 행 수
1000000, -- 플러시를 위한 최대 행 수
10000000, -- 플러시를 위한 최소 바이트 크기
100000000 -- 플러시를 위한 최대 바이트 크기
);
플러시 매개변수:
| 매개변수 | 값 | 의미 |
|---|---|---|
| min_time | 10초 | 10초 이전에는 플러시하지 않음 |
| max_time | 100초 | 100초 이후에는 반드시 플러시 |
| min_rows | 10,000 | 10,000행이 쌓이면 플러시 가능 |
| max_rows | 1,000,000 | 100만 행이 쌓이면 긴급 플러시 |
| min_bytes | 10MB | 10MB가 쌓이면 플러시 가능 |
| max_bytes | 100MB | 100MB가 쌓이면 긴급 플러시 |
실제 작동 방식:
bets_buffer에 삽입합니다(빠름, 메모리에만 쓰기).- ClickHouse는 충분한 데이터가 쌓일 때까지 기다립니다(예: 100,000행 또는 30초 경과).
- 그런 다음 비동기적으로(백그라운드에서) 배치를 메인
bets테이블(MergeTree)로 플러시합니다. - 결과적으로
bets는 큰 파트(100,000행)를 받아 백그라운드 병합 속도가 빨라집니다.
MergeTree에 직접 삽입하지 않는 이유는? MergeTree에 각 INSERT는 미니 파트를 생성합니다. 초당 10,000개의 INSERT를 수행하면 1분 후에 600,000개의 파트가 생깁니다. 백그라운드 병합이 따라잡을 수 없습니다. ClickHouse는 Too many parts 오류를 내고 삽입 속도가 느려집니다.
주의사항: ClickHouse 재시작 시 버퍼가 손실됩니다. bets로 플러시되지 않은 데이터는 사라집니다. 따라서 Buffer 엔진은 몇 초의 데이터 손실이 허용되는 경우에만 사용하세요(예: 잔액이 아닌 분석용).
3. Null 엔진 — 데이터 블랙홀
Null 엔진은 데이터를 그냥 흡수합니다. 어디에도 기록되지 않고 저장되지 않으며 인덱싱되지 않습니다. 하지만 트릭이 있습니다: Null 엔진이 있는 테이블에 Materialized View가 있으면 해당 뷰가 데이터를 받아 처리합니다.
패턴: Kafka → Null + Materialized View → MergeTree
이것은 Kafka에서 높은 부하의 삽입을 위한 고전적인 아키텍처입니다.
-- 1단계: 싱크 테이블 (블랙홀)
CREATE TABLE bets_null
(
user_id UInt64,
amount Decimal(18,2),
created_at DateTime
)
ENGINE = Null; -- 아무것도 저장하지 않음
-- 2단계: 대상 테이블 (실제 저장되는 곳)
CREATE TABLE bets
(
user_id UInt64,
amount Decimal(18,2),
created_at DateTime
)
ENGINE = MergeTree()
ORDER BY (created_at, user_id);
-- 3단계: Materialized View (브리지)
CREATE MATERIALIZED VIEW bets_mv TO bets AS
SELECT * FROM bets_null; -- bets_null에 들어가는 모든 데이터는 bets에 도착
이제 일어나는 일:
-- 클라이언트(또는 Kafka consumer)가 bets_null에 삽입
INSERT INTO bets_null VALUES (123, 100.00, now()); -- 즉시
-- 데이터는 Materialized View를 통과하여 bets에 저장됨
-- bets_null 자체에는 저장되지 않음
왜 이렇게 하나요?
bets_null은 매우 가벼운 테이블입니다. 디스크에 파일을 생성하지 않습니다.- 모든 구독자(Materialized View)가 동시에 데이터를 받습니다.
- 하나의 Null 테이블에 여러 Materialized View를 연결할 수 있습니다: 하나는 MergeTree에 원시 데이터, 다른 하나는 AggregatingMergeTree에 집계, 또 다른 하나는 ReplacingMergeTree에 중복 제거.
비유: Null 엔진은 바닥에 구멍이 뚫린 우편함과 같습니다. 편지가 들어오지만 머물지 않습니다. 하지만 모든 비서(Materialized View)가 편지를 읽고 자신의 폴더에 복사합니다.
4. Log/TinyLog/StripeLog — 소규모 데이터를 위한 단순 엔진
이 엔진군은 성능과 인덱스가 필요 없는 소규모 테이블(최대 100만~200만 행)을 위한 것입니다.
| 엔진 | 특징 | 사용 시기 |
|---|---|---|
TinyLog |
컬럼당 하나의 파일 | 매우 작은 테이블(<10만 행), 스테이징 |
Log |
각 컬럼이 별도 파일, 병렬 읽기를 위한 마커 있음 | 최대 100만 행 테이블, 빠른 읽기 필요 |
StripeLog |
모든 컬럼이 하나의 파일(컴팩트) | 공간 절약, 드문 읽기 |
리그 조회 테이블(200행) 예시:
-- 축구 리그 조회 (한 달에 한 번 변경)
CREATE TABLE leagues_ref
(
league_id UInt32,
name String,
country String,
updated_at Date
)
ENGINE = TinyLog(); -- 가능한 한 단순, ORDER BY 없음
MergeTree를 사용하지 않는 이유? MergeTree는 인덱스, 파티션, 압축을 생성하는데, 200행에는 과합니다. TinyLog는 공간을 덜 차지하고 유지보수가 더 간단합니다.
주의사항: 이 엔진들은 ALTER DELETE와 ALTER UPDATE를 지원하지 않습니다. 데이터를 수정해야 한다면 테이블을 다시 생성해야 합니다.
5. URL 엔진 — HTTP 엔드포인트로서의 테이블
URL 엔진을 사용하면 HTTP 소스(API)에서 직접 데이터를 읽고 PUT을 통해 데이터를 삽입할 수도 있습니다.
CREATE TABLE currency_rates_url
(
base String,
rate Decimal(10,4),
date Date
)
ENGINE = URL('https://api.exchangerate.com/latest?base=USD', CSV)
SETTINGS
method = 'GET',
format = 'CSV',
headers = 'Authorization: Bearer token123';
사용법:
-- API에서 직접 현재 환율 읽기
SELECT * FROM currency_rates_url;
실제 시나리오: ETL을 설정하고 싶지 않은 소규모 분석 작업. 예를 들어, 한 시간에 한 번 무료 API에서 환율을 읽고 배치와 조인하여 금액을 재계산합니다.
주의사항:
- 인덱스가 없습니다. 각 쿼리는 소스 전체를 스캔합니다.
- API가 오류를 반환하면 쿼리가 실패합니다.
- 높은 부하의 쿼리에는 적합하지 않습니다(데이터가 ClickHouse 내부에 캐시되고 API에서 매번 읽지 않는다고 가정).
6. File 엔진 — 디스크의 파일로서의 테이블
ClickHouse 서버의 로컬 파일 시스템에 있는 파일을 읽고 쓸 수 있습니다. CSV, TSV, JSONEachRow, Parquet 형식을 지원합니다.
-- CSV 파일을 읽는 테이블
CREATE TABLE imported_players
(
user_id UInt64,
username String
)
ENGINE = File(CSV, '/var/lib/clickhouse/user_files/players.csv');
사용 시기:
- 파일에서 데이터 로드(관리자가 새 사용자가 포함된 CSV를 배치함).
INSERT INTO ... SELECT를 통해 쿼리 결과를 파일로 내보내기.
주의사항: ClickHouse는 폴더에 접근할 수 있어야 합니다(보안상 일반적으로 /var/lib/clickhouse/user_files/).
7. S3 엔진 — S3에 대한 직접 쿼리
Amazon S3 버킷(또는 MinIO, Yandex Object Storage)에서 직접 데이터를 읽습니다. 데이터를 ClickHouse로 복사하지 않습니다.
CREATE TABLE logs_s3
(
timestamp DateTime,
message String
)
ENGINE = S3(
'https://mybucket.s3.amazonaws.com/logs/*.parquet',
'AWS_ACCESS_KEY', 'AWS_SECRET_KEY',
'Parquet'
);
사용 시기:
- S3에 테라바이트 규모의 로그가 있고 ClickHouse로 복사하지 않고 가끔 분석 쿼리를 실행하려는 경우.
- 콜드 데이터(S3가 ClickHouse 디스크보다 저렴).
주의사항: 각 쿼리는 S3에서 데이터를 다운로드하므로 느리고 비용이 많이 들 수 있습니다(아웃바운드 트래픽). 드문 쿼리에만 적합합니다.
8. PostgreSQL 엔진 — PostgreSQL의 실시간 데이터
PostgreSQL 엔진을 사용하면 PostgreSQL 테이블을 ClickHouse 테이블처럼 읽고 쓸 수 있습니다.
CREATE TABLE pg_players
(
user_id UInt64,
balance Decimal(18,2)
)
ENGINE = PostgreSQL(
'postgres-host:5432', -- 호스트와 포트
'betting', -- 데이터베이스
'players', -- PostgreSQL의 테이블
'clickhouse_user', -- 사용자
'password' -- 비밀번호
);
사용법:
-- PostgreSQL에서 현재 잔액 읽기
SELECT * FROM pg_players WHERE user_id = 123;
-- ClickHouse 테이블과 JOIN도 가능
SELECT b.user_id, b.amount, p.balance
FROM bets b
JOIN pg_players p ON b.user_id = p.user_id;
사용 시기:
- PostgreSQL에서 ClickHouse로 점진적으로 마이그레이션 중이며 일부 데이터가 여전히 기존 DB에 있는 경우.
- 외부 애플리케이션에 의해 업데이트되는 실시간 데이터가 필요하고 ETL을 설정하고 싶지 않은 경우.
주의사항:
- 각 쿼리는 PostgreSQL로 이동하므로 대량 데이터에 대해 느릴 수 있습니다.
- ClickHouse는 이러한 테이블을 포함하는 효율적인 실행 계획을 수립할 수 없습니다(푸시다운 없음).
9. 배팅을 위한 아키텍처 패턴: Kafka → Buffer → MergeTree
이제 모든 것을 종합해 봅시다. 각 배치 이벤트에 대해 Kafka에서 초당 10,000개 이상의 메시지를 받는다고 상상해 보세요. 최소 지연 시간으로 ClickHouse에 저장하고 수천 개의 작은 파트를 생성하지 않아야 합니다.
준비된 아키텍처:
-- 1. Kafka용 싱크 테이블 (Null)
CREATE TABLE bets_kafka
(
user_id UInt64,
event_id UInt64,
amount Decimal(18,2),
bet_time DateTime
)
ENGINE = Null;
-- 2. 대상 MergeTree 테이블
CREATE TABLE bets
(
user_id UInt64,
event_id UInt64,
amount Decimal(18,2),
bet_time DateTime
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(bet_time)
ORDER BY (bet_time, user_id);
-- 3. 삽입을 완화하는 Buffer 테이블
CREATE TABLE bets_buffer AS bets
ENGINE = Buffer('default', 'bets', 16, 5, 60, 10000, 1000000, 10000000, 100000000);
-- 4. Materialized View: Kafka → Null → Buffer (뷰를 통해)
CREATE MATERIALIZED VIEW bets_kafka_mv TO bets_buffer AS
SELECT * FROM bets_kafka;
-- 5. 실시간 집계를 위한 또 다른 뷰 (선택 사항)
CREATE MATERIALIZED VIEW bets_stats_mv TO bets_hourly_agg AS
SELECT
toStartOfHour(bet_time) AS hour,
countState() AS bet_count,
sumState(amount) AS total_amount
FROM bets_kafka
GROUP BY hour;
데이터 흐름:
- Kafka Connect가
bets_kafka(Null 엔진)에 메시지를 삽입합니다. bets_kafka_mv(Materialized View)가 데이터를bets_buffer로 리디렉션합니다.bets_buffer가 메모리에 배치를 축적합니다(예: 100,000행 또는 60초).- 버퍼가 큰 파트로
bets(MergeTree)에 플러시됩니다. - 동시에 두 번째 Materialized View가 대시보드용 시간별 집계를 구축합니다.
이것이 최적인 이유:
- Kafka는 Null에 씁니다(즉시, 오버헤드 없음).
- Buffer는 수천 개의 작은 파트가 생기는 것을 방지합니다.
- MergeTree는 큰 파트를 받아 병합이 효율적으로 작동합니다.
- 집계는 두 번째 뷰를 통해 실시간으로 구축됩니다.
Kafka에서 MergeTree로 직접 쓰면 어떻게 될까요? 초당 10,000개 이벤트에서 1분에 600,000개의 파트가 생성됩니다. ClickHouse는 Too many parts 오류로 다운됩니다. Buffer 엔진이 이를 방지합니다.
엔진 선택 요령 — 치트 시트
| 작업 | 엔진 | 이유 |
|---|---|---|
| 영구 저장, 분석 | MergeTree (또는 *MergeTree) | ClickHouse의 기초, 인덱스, 압축 |
| 높은 부하 버퍼링 | Buffer | 작은 INSERT를 큰 파트로 결합 |
| 임시 데이터 (세션, 스테이징) | Memory | 최대 속도, 데이터 중요하지 않음 |
| 저장 없는 Kafka consumer | Null + MV | 뷰만을 위한 데이터 |
| 작은 정적 조회 테이블 | TinyLog / Log | 단순함, 적은 메타데이터 |
| PostgreSQL 연결 | PostgreSQL 엔진 | ETL 없는 실시간 데이터 |
| S3에 대한 드문 쿼리 | S3 엔진 | 저렴한 콜드 스토리지 |
| 파일에서 가져오기 | File 엔진 | 일회성 복사 |
다음 단계
일반 MergeTree로는 해결되지 않는 문제를 해결하는 특별한 엔진에 대해 배웠습니다. 이제 다음을 알게 되었습니다:
- Memory — 캐시 및 스테이징용,
- Buffer — 너무 빈번한 INSERT로부터 보호,
- Null — MV를 통해 데이터를 여러 스트림으로 "분기",
- URL / File / S3 / PostgreSQL — 외부 데이터용.
다음에 다룰 주제:
- Kafka 엔진 설정 방법 — 별도의 커넥터 없이 Kafka에 내장된 커넥터.
- 고급 Materialized View — 복잡한 ETL을 위한 뷰 체인.
- 분산 테이블 — 서버 간 데이터 샤딩 방법.
결론: ClickHouse의 모든 작업이 MergeTree로 해결되는 것은 아닙니다. 때로는 Buffer가 삽입으로 서버를 죽이지 않도록, 때로는 Null + MV가 데이터를 다른 집계로 분배하도록, 때로는 Memory가 임시 해싱을 위해 필요합니다. 주요 규칙: 데이터 흐름을 먼저 설계한 다음 엔진을 선택하세요. 그 반대가 아닙니다.
← 이전 글: ClickHouse의 딕셔너리: JOIN 없이 빠른 조회
→ 다음 글: ClickHouse의 구체화된 뷰: 증분 처리의 힘
— Editorial Team
아직 댓글이 없습니다.