DB Study

1장. ClickHouse 개요 — 왜 빠른가

같은 데이터, 같은 쿼리인데 100배 빠른 이유가 있습니다. 열 저장·벡터화 실행·immutable 파트 — ClickHouse 속도의 3대 비밀을 그림으로 파헤칩니다.

기초 ⏱ 약 25분 🎬 애니메이션 3개 선수 지식: SQL 기본
🧭
이 장에서 배우는 것

① OLAP과 OLTP의 근본적 트레이드오프 ② 행 저장 vs 열 저장 — 집계 쿼리가 읽는 데이터량의 차이 ③ 벡터화 실행이 CPU를 쥐어짜는 방법 ④ MergeTree 스토리지의 설계 철학 (immutable 파트 + 백그라운드 머지)

1.1 OLAP의 극단에 서 있는 데이터베이스

ClickHouse는 OLAP(Online Analytical Processing, 온라인 분석 처리) 워크로드에 특화된 컬럼 지향(column-oriented) DBMS입니다. 모든 데이터베이스는 OLTP와 OLAP 사이의 스펙트럼 위 어딘가에 있는데, ClickHouse는 처음부터 "가능한 한 빠른 OLAP 시스템"을 목표로 OLAP 쪽 극단에 자리 잡았습니다.

OLTP (예: PostgreSQL, MySQL)OLAP (예: ClickHouse)
대표 작업주문 1건 저장, 회원 정보 수정"지난 1년간 국가별 매출 합계"
쿼리 패턴소량의 행을 자주 읽고 씀드물지만 한 번에 수억 행을 읽음
최적화 방향행 단위 읽기/쓰기컬럼 단위 대량 읽기
저장 방식행형(row-oriented)컬럼형(columnar)
트랜잭션완전 지원미지원 (mutation은 제한적)

공식 문서는 ClickHouse를 "true column-oriented DBMS"라고 부릅니다. 단순히 데이터를 컬럼 단위로 저장하는 것을 넘어, 쿼리 실행 자체가 컬럼(벡터) 단위로 이루어진다는 뜻입니다. 이 두 가지가 이 장의 핵심 주제입니다.

1.2 행 저장 vs 열 저장 — 집계 쿼리가 읽는 데이터량

100개 컬럼을 가진 테이블에서 딱 2개 컬럼만 쓰는 분석 쿼리를 떠올려 보세요. 행 저장에서는 한 행의 모든 컬럼이 디스크에 붙어 있으므로 필요 없는 98개 컬럼도 함께 읽어야 합니다. 열 저장에서는 컬럼마다 파일이 분리되어 있어 필요한 파일만 읽습니다.

SELECT sum(amount) WHERE country='Korea' 📄 행 저장 (row-oriented) 1001 · Alice · 25 · Korea · 50000 1002 · Bob · 31 · USA · 12000 1003 · Charlie · 42 · Japan · 78000 1004 · Dave · 29 · Korea · 35000 🗂 열 저장 (ClickHouse) user_id.bin: 1001·1002·1003·1004 name.bin: Alice·Bob·Charlie·Dave age.bin: 25·31·42·29 country.bin: Korea·USA·Japan·Korea amount.bin: 50000·12000·78000·35000 모든 행의 모든 컬럼을 디스크에서 읽음 ⚠️ 필요한 건 country·amount 뿐 — I/O의 60% 낭비 country.bin과 amount.bin — 이 2개 파일만 읽음 ✅ 읽은 데이터량 비교 행 저장 100% 열 저장 40% (+압축하면 수%대)

1.3 보너스: 압축률의 극적인 차이

열 저장의 이점은 I/O 감소만이 아닙니다. 같은 타입·같은 분포의 값이 물리적으로 연속 배치되므로 압축 알고리즘이 반복 패턴을 훨씬 잘 찾아냅니다.

-- country 컬럼만 놓고 비교해 보면:
-- 행 저장: Korea, (다른 컬럼들...), USA, (다른 컬럼들...), Japan, ...
--   → 패턴이 끊겨서 LZ4 압축률 ~50%
-- 열 저장: Korea, Korea, Korea, USA, USA, Japan, Japan, ...
--   → 반복 패턴이 명확 → LowCardinality + LZ4로 ~95%까지 압축

CREATE TABLE user_events
(
    user_id  UInt64,
    country  LowCardinality(String),  -- 고유값이 적은 문자열에 특효
    amount   UInt32
)
ENGINE = MergeTree
ORDER BY (country, user_id);
💡
압축 = I/O 감소 = 속도

압축은 디스크 절약 이상의 의미가 있습니다. 디스크에서 읽는 바이트 수 자체가 줄어들므로 쿼리 성능에 직결됩니다. ClickHouse는 범용 압축(LZ4, ZSTD) 외에도 Delta, Gorilla 같은 타입 특화 코덱을 제공합니다.

1.4 벡터화 실행 — CPU를 쥐어짜는 방법

저장만 열 단위인 것이 아니라, 연산도 열(배열) 단위로 합니다. 행 하나씩 처리(row-at-a-time)하는 대신 수천 개 값의 배열(vector/chunk)에 연산을 한 번에 적용하는 것을 벡터화 쿼리 실행(vectorized query execution)이라고 합니다. 이 아이디어는 1957년 APL 언어까지 거슬러 올라가는, 과학 계산 분야에서 수십 년간 검증된 접근입니다.

🐢 일반 DB (row-at-a-time) 50000 12000 78000 35000 sum() 호출 행마다 1번 N행 = 함수 호출 N번 + 분기 예측 실패 ⚡ ClickHouse (vectorized) [50000, 12000, 78000, 35000, …] SIMD 호출 1번 CPU 명령 1개로 4~8개 값 동시 처리 벡터화가 빠른 3가지 이유 ① 연속 메모리 → L1/L2 캐시 히트율↑ ② SIMD 동시 처리 ③ 함수 호출 오버헤드↓

쿼리 처리 속도를 높이는 또 다른 접근으로 런타임 코드 생성(runtime code generation)이 있습니다. 쿼리마다 기계어 코드를 즉석에서 만들어 여러 연산을 하나로 융합(fuse)하는 방식입니다. 공식 문서는 CMU 연구를 인용하며 두 접근의 결합이 최선이라고 밝히고, 실제로 ClickHouse는 벡터화를 주축으로 삼되 제한적인 코드 생성을 보조적으로 사용합니다.

접근원리장점단점
Vectorized Execution데이터를 배열 단위로 처리SIMD 활용 용이, 구현 단순중간 벡터가 L2 캐시를 넘으면 성능 저하
Runtime Code Generation실행 시 기계어를 동적 생성연산 융합, 간접 호출 제거생성 오버헤드, 구현 복잡

1.5 스토리지의 설계 철학 — MergeTree는 LSM Tree가 아니다

ClickHouse의 대표 스토리지 엔진 MergeTree는 겉보기에 RocksDB류의 LSM Tree와 닮았지만, 결정적 차이가 있습니다. MEMTABLE(인메모리 쓰기 버퍼)도 WAL(Write-Ahead Log)도 없습니다. INSERT된 데이터는 프라이머리 키 순서로 정렬되어 즉시 디스크에 새 파트(part)로 기록됩니다.

🪨 LSM Tree (RocksDB 등) 🌲 MergeTree (ClickHouse) INSERT 도착 INSERT 도착 (배치) MEMTABLE (메모리 버퍼) 정렬된 맵 WAL 메모리에 쓰고 + 복구용 로그도 씀 Level 0 SSTable Level 1 SSTable (compaction) 가득 차면 그제야 디스크로 flush Part all_1_1_0 즉시 디스크에! 💾 MEMTABLE 없음 · WAL 없음 part 2 part 3 🔧 백그라운드 머지 무거운 변환은 전부 머지 시점으로 (merge-time computation)

이 구조에서 세 가지 중요한 성질이 나옵니다.

⚠️
이 설계의 대가: 배치 INSERT 필수

INSERT마다 파트(디렉터리+파일들)가 생성되므로, 초당 수천 번의 소량 INSERT는 파트 폭증으로 이어집니다("Too many parts" 에러). 배치로 모아 넣거나 async insert를 사용해야 합니다. 4장과 9장에서 자세히 다룹니다.

1.6 Data Pruning — 읽지 않는 것이 가장 빠르다

공식 문서의 명언: "The fastest way to read data is to not read it at all." ClickHouse는 불필요한 데이터를 건너뛰기 위한 세 가지 장치를 제공합니다.

🔑 Sparse Primary Index

테이블의 정렬 순서를 정의하는 희소 인덱스. WHERE 필터를 풀 스캔 대신 이진 탐색으로 — O(n)이 O(log n)으로. → 7장

🪞 Table Projections

같은 데이터를 다른 프라이머리 키로 정렬한 내부 복제본. 자주 쓰는 필터 조건이 여러 개일 때 유용하지만, 데이터가 중복됩니다.

⏭ Skipping Indexes

컬럼에 min/max, 고유값 집합 같은 통계를 내장해 블록을 건너뜁니다. 프라이머리 키와 직교하는 보조 수단입니다.

-- 잘 설계된 프라이머리 키는 스캔량을 극적으로 줄인다
EXPLAIN indexes = 1
SELECT count() FROM user_events WHERE country = 'Korea';

-- PrimaryKey  Parts: 1/4  Granules: 12/3257   ← 3257개 중 12개만 읽음!

1.7 확장 모델 — 수직과 수평

수직 확장: ClickHouse는 쿼리 플랜을 여러 lane으로 펼쳐 코어당 하나씩 병렬 실행합니다. 각 lane이 테이블의 서로 다른 범위를 처리하므로 코어 수에 비례해 빨라집니다(3장에서 스레드 모델을 다룹니다).

수평 확장: 단일 노드로 부족하면 테이블을 샤드(shard)로 분할해 여러 노드에 분산합니다(8장). 단, 공식 문서가 인정하는 제약이 있습니다 — 클러스터는 elastic하지 않습니다. 새 샤드를 추가해도 데이터가 자동으로 리밸런싱되지 않으므로, 초기 샤딩 전략이 중요합니다.

🚨
1장에서 바로 도출되는 운영 원칙 4가지

① 배치 INSERT 필수 (파트 폭증 방지) ② Mutation(UPDATE/DELETE) 최소화 — immutable 파트 기반이라 파트 전체 재작성 ③ 프라이머리 키 설계가 곧 성능 — 생성 후 ORDER BY 변경 불가 ④ 샤드 자동 리밸런싱 없음을 전제로 설계

✍️ 이해도 체크

100개 컬럼 테이블에서 5개 컬럼만 사용하는 집계 쿼리를 실행할 때, 컬럼 지향 저장이 빠른 가장 직접적인 이유는?
✅ 컬럼 지향 저장에서는 컬럼마다 파일이 분리되어 있어, 쿼리에 필요한 컬럼 파일만 읽습니다. 행 지향이라면 95개 컬럼 분량의 I/O가 낭비됩니다. 여기에 연속 배치로 인한 높은 압축률이 I/O를 한 번 더 줄여 줍니다.
MergeTree가 LSM Tree와 구조적으로 다른 점은?
✅ 공식 문서 원문: "MergeTree is not an LSM tree because it does not contain MEMTABLE and LOG: inserted data is written directly to the filesystem." 그래서 배치 INSERT는 초고속이지만, 소량 빈번 INSERT는 파트 폭증을 부릅니다.
📚
원문으로 더 깊이

이 장의 원문 문서: chapters/ch01_clickhouse_overview.md — 벡터화 실행의 역사(APL 계보), MergeTree vs LSM 상세 비교 다이어그램이 담겨 있습니다.