ClickHouse: 컬럼 기반 DBMS가 분석을 압도하는 이유
┌─────────────────────────────────────────────────────────────────────────────┐
│ CLICKHOUSE 컬럼 아키텍처 │
├─────────────────────────────────────────────────────────────────────────────┤
│ 논리적 표현 ──▶ 디스크의 물리적 저장 │
│ │
│ ┌─────┬──────┬─────┬─────┐ ┌──────────────┐ ┌──────────────┐ │
│ │user │ time │amount│odds│ │ user 컬럼 │ │ time 컬럼 │ │
│ ├─────┼──────┼─────┼─────┤ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ 101 │ 12:00│ 50 │ 2.0 │ ───▶ │ │ 101 │ │ │ │ 12:00 │ │ │
│ ├─────┼──────┼─────┼─────┤ │ ├──────────┤ │ │ ├──────────┤ │ │
│ │ 102 │ 12:01│ 100 │ 1.5 │ │ │ 102 │ │ │ │ 12:01 │ │ │
│ ├─────┼──────┼─────┼─────┤ │ ├──────────┤ │ │ ├──────────┤ │ │
│ │ 103 │ 12:02│ 75 │ 3.0 │ │ │ 103 │ │ │ │ 12:02 │ │ │
│ └─────┴──────┴─────┴─────┘ │ └──────────┘ │ │ └──────────┘ │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ 각 컬럼은 자체 디렉토리에 저장: │
│ /data/table/bet_amount/ (LZ4 또는 ZSTD 압축, 3-10배) │
│ /data/table/odds/ (비트맵 인덱스 + 최소/최대 맵) │
└─────────────────────────────────────────────────────────────────────────────┘
행 vs. 열: 고생 끝에 깨달은 차이
한때 PostgreSQL로 베팅 분석 시스템을 구축하려고 했습니다. 테이블은 하루 5천만 건으로 늘어났고, 인덱스는 200GB까지 부풀었으며, "시간별 group by" 쿼리는 몇 분이 걸렸습니다. DBA는 울었고, 비즈니스는 "즉시"를 요구했습니다. 그때는 몰랐습니다. 분석을 위한 기존의 행 기반 데이터베이스는 숟가락으로 참호를 파는 것과 같다는 것을: 기술적으로는 가능하지만, 절대 올바른 도구가 아닙니다.
ClickHouse는 구원자처럼 왔습니다. 하지만 먼저, 익숙한 행 기반 사고방식을 버려야 했습니다.
행 기반 DBMS 내부에서 일어나는 일
PostgreSQL과 MySQL은 데이터를 행 단위로 저장합니다. 각 레코드가 user_id, event_time, bet_amount, odds, outcome이 연속적으로 적힌 카드라고 상상해보세요. 전체 행은 디스크의 한 곳에 위치합니다. "지난 1시간 동안 플레이어 101이 얼마나 베팅했나?"라는 질문에 답해야 할 때, PostgreSQL은 필요하지 않은 열까지 포함한 모든 행의 모든 열을 충실히 메모리로 가져옵니다. 디스크 작업은 시스템에서 가장 느립니다. 마치 우유 가격을 알아보기 위해 슈퍼마켓에 갔는데, 계산원과 경비원까지 딸린 카트 전체를 가져오는 것과 같습니다.
ClickHouse는 더 똑똑하게 처리
컬럼 기반 데이터베이스는 각 컬럼을 별도의 파일에 저장합니다. SELECT SUM(bet_amount) ... 쿼리는 bet_amount 컬럼 파일만 읽습니다. 나머지 데이터는 전혀 건드리지 않습니다. 결과: 디스크에서 10-100배 적은 데이터를 읽습니다. 게다가 동질적인 데이터를 가진 컬럼은 압축이 잘 됩니다.
실제 사례: 프로덕션에서 20억 행의 이벤트 테이블이 있었습니다. PostgreSQL에서 단순한 SELECT AVG(odds) WHERE user_id IN (1,2,3)는 45초가 걸렸습니다(전체 행을 읽어야 했기 때문). ClickHouse는 같은 쿼리를 0.3초 만에 처리했습니다. odds와 user_id 컬럼만 가져왔기 때문입니다. 150배 속도 향상.
데이터 스키마: 실제 시스템에서 베팅을 저장하는 방법
베팅 분석을 위한 프로덕션 스키마에서는 이 엔진을 사용합니다:
CREATE TABLE bets_analytics
(
user_id UInt64,
event_time DateTime64(3),
bet_amount Decimal64(2),
odds Float64,
outcome Enum8('win' = 1, 'loss' = 2, 'refund' = 3),
session_id String,
device_type LowCardinality(String), -- 반복 값 최적화
ip_hash UInt32
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time) -- 월별 파티션
ORDER BY (event_time, user_id) -- 정렬 순서
SETTINGS index_granularity = 8192;
이렇게 하는 이유:
device_type에LowCardinality사용 — 장치 유형이 적음(ios, android, web), 비트맵으로 압축DateTime64(3)는 밀리초 제공 — 피크 시간대 초 단위 집계용- 월별 파티션으로
DELETE없이 오래된 데이터 삭제 가능(13개월 TTL) ORDER BY (event_time, user_id)— 가장 빈번한 쿼리는 시간 간격과 사용자 필터
PostgreSQL을 죽이고 ClickHouse는 가볍게 처리하는 쿼리
상상해보세요: 운영자를 위한 일반적인 작업 — "지난 24시간 동안 시간별 베팅과 평균 지급액 변화 추이 표시"
SELECT
toStartOfHour(event_time) AS hour,
COUNT(*) AS total_bets,
SUM(bet_amount) AS total_volume,
AVG(bet_amount) AS avg_bet,
AVG(odds) AS avg_odds,
SUM(CASE WHEN outcome = 'win' THEN bet_amount * odds ELSE 0 END) AS total_payout,
COUNTIf(outcome = 'win') / COUNT(*) AS win_rate
FROM bets_analytics
WHERE event_time >= now() - INTERVAL 24 HOUR
GROUP BY hour
ORDER BY hour DESC;
5억 행의 테이블에서 이 쿼리는 ClickHouse에서 0.8–1.2초 만에 실행됩니다. 왜? 세 가지 요인:
벡터화된 계산 — ClickHouse는 한 번에 한 행이 아닌 배치(8192행)를 처리합니다.
bet_amount * odds곱셈은 CPU SIMD 명령어(최신 Intel의 AVX2)를 통해 전체 배열에서 발생합니다.최소화된 디스크 I/O —
event_time,bet_amount,odds,outcome컬럼만 스캔됩니다. 다른 필드(user_id,session_id,ip_hash)는 전혀 건드리지 않습니다.즉석 집계 — 중간 결과를 구체화하지 않고, 읽는 동안 직접 해시 테이블을 구축합니다.
실제 벤치마크: ClickHouse vs. 클래식 데이터베이스
문서의 건조한 숫자는 드리지 않겠습니다 — 실제 하드웨어(AWS c5.4xlarge, 16 vCPU, EBS gp3, 100GB 압축되지 않은 데이터)에서 정직한 테스트를 분석해보겠습니다.
데이터: 3개월에 걸친 10억 개의 베팅 레코드.
| 쿼리 | PostgreSQL 14 (인덱스 포함) | MySQL 8 (InnoDB) | ClickHouse 23.8 | 속도 향상 배수 |
|---|---|---|---|---|
SELECT SUM(bet_amount) FROM bets |
184초 | 201초 | 0.9초 | 204배 |
SELECT user_id, SUM(bet_amount) GROUP BY user_id |
312초 (1천만 명 이상 사용자에서 OOM) | 287초 | 3.2초 | 97배 |
SELECT toHour(event_time), COUNT(*) GROUP BY hour |
97초 | 112초 | 0.4초 | 242배 |
SELECT user_id, COUNT(DISTINCT session_id) WHERE outcome='win' |
421초 | 389초 | 5.1초 | 82배 |
SELECT AVG(odds) WHERE user_id IN (SELECT user_id FROM ...) |
248초 | 203초 | 2.8초 | 88배 |
공식 ClickHouse 테스트에 게시된 유사한 벤치마크 실행 데이터(참조: clickhouse.com/benchmark/dbms/).
중요한 차이점: 컬럼 확장 cstore_fdw를 사용한 PostgreSQL은 30-50배 속도 향상에 접근하지만, 여전히 네이티브 컬럼 아키텍처를 따라잡지 못합니다.
우리가 당한 부분: 한 숟갈의 타르
ClickHouse는 만병통치약이 아닙니다. 다음은 권장하지 않는 사항입니다:
포인트 업데이트. UPDATE와 DELETE는 작동하지만, 디스크에 부하를 주는 백그라운드 뮤테이션으로 변합니다. 초당 1만 건의 트랜잭션에 대해
outcome을 업데이트하려고 시도했는데, 시스템이 2분 만에 다운되었습니다.OLTP 워크로드. 초당 1만 건의 INSERT와 즉시 일관성이 필요하다면 ClickHouse가 처리할 수 있지만, 같은 행을 기본 키로 즉시 읽어야 한다면... 잘못된 도구를 선택한 것입니다.
대규모 테이블 JOIN. 권장 패턴은 삽입 시 비정규화입니다. 우리는 120개 컬럼이 있는 하나의 넓은 테이블에 모든 것을 저장합니다. 네, 정규형에 대한 안티패턴입니다. 아니요, 신경 쓰지 않습니다.
초보자의 흔한 실수: FINAL 수정자를 사용하여 행의 최신 버전을 보장하려는 것. 이는 파티션 전체를 다시 읽게 만듭니다. 하지 마세요. 최신 버전이 필요하면 집계에서 argMax와 함께 version 컬럼을 사용하세요.
실제로 ClickHouse를 프로덕션에서 사용하고 비용을 지불하는 기업
이론이 아닌, ClickHouse가 페타바이트의 데이터를 처리하는 실제 사례:
Cloudflare — 모든 HTTP 요청 분석: 초당 2천만 요청, 하루 7조 행. 그들의 블로그 게시물 "ClickHouse @ Cloudflare"는 규모를 이해하는 데 필독입니다.
Uber — 여행 모니터링, 실시간 사기 탐지. ZooKeeper(현재 ClickHouse Keeper)를 통한 복제로 Rides Analytics 전용 클러스터를 운영합니다.
GitLab — 제품 메트릭, DevOps 대시보드. 성능 모니터링의 백엔드로 ClickHouse를 사용합니다.
온라인 카지노 (이름은 밝히지 않겠지만, 믿으세요) — 베팅 주제가 완전히 적용됩니다. 일반적인 설치: 3-5개 노드, 3000억 개의 베팅 레코드, 6개월 TTL, 가장 무거운 쿼리 — 베팅 클러스터링 분석을 통한 다중 계정 탐지.
사용 사례: 베팅 사기 방지 방법
내 경험의 실제 작업: 모든 이벤트에 같은 금액과 배당률로 베팅하는 플레이어(봇) 찾기. 실시간 분석.
-- 지난 5분간 의심스러운 베팅 패턴
SELECT
user_id,
COUNT(DISTINCT event_id) as events_count,
AVG(bet_amount) as avg_bet,
STDDEV(bet_amount) as bet_stddev,
AVG(odds) as avg_odds,
STDDEV(odds) as odds_stddev
FROM bets_analytics
WHERE event_time >= now() - INTERVAL 5 MINUTE
GROUP BY user_id
HAVING events_count > 20 AND bet_stddev < 1 AND odds_stddev < 0.1;
이 쿼리는 5억 개 레코드에서 0.7초 만에 실행됩니다. 파티션이 있는 PostgreSQL 복제본 세계에서는 같은 로직을 Flink로 스트리밍하고 별도로 계산해야 했습니다.
기타 일반적인 사용 사례:
플레이어 LTV(Lifetime Value) — 7/14/30일 윈도우와 가중 집계. ClickHouse는
arrayReduce와groupArray를 사용하여 윈도우에서 롤링 합계를 몇 초 만에 계산합니다.리텐션 분석 — "등록 후 n일에 몇 명의 플레이어가 돌아왔는가" 매트릭스. ClickHouse가
groupUniqArray와hasAny를 통해 최적화하는 셀프 조인을 사용한 클래식 SQL.코호트 분석 — 첫 이벤트별 사용자 그룹화.
min(event_time) OVER (PARTITION BY user_id)와quantile을 결합하여 백분위수를 계산합니다.
사랑하게 된 아키텍처 기능
프로젝션 — 베팅 테이블에 세 가지 프로젝션을 사용합니다: 시간별 집계용, 사용자 세션용, 머신러닝용(평균, 분산). 이는 삽입 시 업데이트되는 구체화된 뷰입니다. 쿼리 시 ClickHouse는 어떤 프로젝션을 사용할지 결정합니다.
구체화된 컬럼 — event_time 대신 DATE(event_time)를 구체화된 컬럼으로 저장합니다. 이렇게 하면 파티셔닝과 필터링이 무료가 됩니다.
비동기 INSERT — 일반적인 부하: 초당 5만 행. PostgreSQL에서는 PgBouncer와 파티션이 필요했을 것입니다. ClickHouse는 INSERT를 대기열에 넣고 100만 레코드 배치로 비동기 플러시하며, 디스크는 거의 부담을 느끼지 않습니다.
부족한 점과 해결 방법
다중 테이블 트랜잭션 — 지원되지 않음. 단일
INSERT INTO ... SELECT FROM으로 데이터 마트를 구축하고 입력에서 Kafka의 멱등성에 의존합니다.전문 검색 — 존재하지만 일반적인 형태는 아닙니다.
hasToken은 토큰 수준에서 작동하지만, 한국어 형태소 분석에는 문제가 있습니다. 로그의 경우 검색을 Lucene이 있는 별도 클러스터로 오프로드했습니다.격리 수준 — 스냅샷 격리를 통한 읽기 커밋만 지원. 읽는 동안 파티션을 업데이트하면 이전 스냅샷을 읽습니다. 우리에게는 충분합니다.
앞으로의 방향
ClickHouse는 분석 쿼리에 대한 답을 1분이 아닌 100ms 안에 원할 때 사용합니다. 베팅, 사기 탐지, 원격 측정, 인프라 모니터링과 같은 위험 비즈니스에 완벽합니다. OLTP 사고방식을 잊고 컬럼 패러다임을 받아들이기만 하면 됩니다.
다음 글에서는 Ubuntu/Debian에 ClickHouse 클러스터를 처음부터 배포하고, 복제를 설정하며, 첫 번째 벤치마크에서 실패하지 않는 방법을 보여드리겠습니다.
👉 [Ubuntu/Debian에 ClickHouse 설치: 프로덕션 준비 구성](링크는 게시 시 추가 예정)
→ 다음 글: Ubuntu/Debian에 ClickHouse 설치하기: 잘못된 권한으로 고생한 사람이 알려주는 단계별 가이드
— Editorial Team
아직 댓글이 없습니다.