✍️ 필사 모드: ClickHouse Internals Deep Dive — MergeTree、Vectorized Execution、分散クエリ、Keeper 完全攻略 (2025)
日本語TL;DR
- ClickHouse は Yandex が 2009 年に内部分析用に作り、2016 年にオープンソース化。C++ で書かれた Columnar OLAP エンジンの代表格。
- Columnar ストレージ: 同じカラムの値を連続格納。
SELECT sum(x) WHERE y > 0のようなクエリは該当カラムだけを読めばよい → 10-100 倍速い。 - MergeTree: ClickHouse の中核エンジン。LSM のように「Part」を書き、バックグラウンドでマージ。ただし Row ではなく カラム単位 で保存。
- Sparse Primary Index: 全行ではなく Granule (デフォルト 8192 行) ごとに 1 エントリ。インデックスが小さく完全にメモリ常駐。
- Vectorized Execution: 一度に数万行をループ処理。SIMD フレンドリで、コンパイラが AVX2/AVX-512 を自動活用。
- 圧縮: カラム別に LZ4/ZSTD がデフォルト。時系列は Delta + Gorilla でさらに攻撃的に圧縮。
- Distributed Engine: シャーディング + レプリケーション。クエリを Shard にばらまいて集約。
- Keeper: ZooKeeper の置き換え。Raft ベース、C++ で再実装。ClickHouse 向け最適化。
- Materialized View: Insert 時点で事前計算された結果を保存。リアルタイム集計に必須。
- 2025 の競合: Druid、Pinot (リアルタイム集計)、DuckDB (組み込み)、Snowflake/BigQuery (マネージド)。
1. なぜ ClickHouse は速いのか
1.1 OLAP vs OLTP
OLTP は少数の行を読み書きし、インデックスで O(log n)。Postgres、MySQL、Oracle。
OLAP は数百万から数十億行をスキャンし、少数のカラムだけを使い集計する。ClickHouse、Snowflake、BigQuery、Druid。
1.2 典型的な OLAP クエリ
SELECT user_country, SUM(revenue)
FROM events
WHERE event_date BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY user_country
ORDER BY SUM(revenue) DESC
LIMIT 10;
日付範囲で数十億行をスキャンし、必要なのは user_country と revenue の 2 カラムだけ、結果は小さな集計。Row ストレージでは不要カラムまで読まざるを得ず、行単位処理で関数呼び出しオーバーヘッドが蓄積する。
1.3 3 つの武器
- Columnar ストレージ: 必要なカラムだけをディスクから読む。
- Vectorized Execution: 一度に数万行を SIMD で処理。
- 高圧縮: ディスク I/O 帯域を節約。
この 3 つで Postgres 比 100-1000 倍速い OLAP クエリを実現。
2. 歴史
Yandex は Google Analytics 対抗の Metrica を運営していたが、MySQL では毎日数十億イベントを処理できなかった。Alexey Milovidov 率いるチームが C++ で新エンジンを書き下ろし、2009 年プロトタイプ、2012 年プロダクション。2016 年 GitHub に公開すると爆発的に普及 — CloudFlare、Uber、Bloomberg、GitLab、Deutsche Bank など。2021 年 ClickHouse Inc. として Yandex から spin-off、2.5 億ドル以上のシリーズ資金調達、ClickHouse Cloud を提供。2025 年時点で GitHub 35,000+ スター、月次リリース、世界 1,000+ コントリビュータ。
3. Columnar ストレージの基礎
3.1 Row vs Column
Row ベース (Postgres, MySQL):
Row 1: [id=1, name="Alice", age=30, country="US"]
Row 2: [id=2, name="Bob", age=25, country="UK"]
ディスク上: 1|Alice|30|US|2|Bob|25|UK|...
Column ベース (ClickHouse):
Column "id": [1, 2, 3, ...]
Column "name": ["Alice", "Bob", ...]
Column "age": [30, 25, ...]
Column "country": ["US", "UK", ...]
各カラムが別ファイル。
3.2 なぜ Columnar が分析に速いのか
I/O 削減: SELECT AVG(age) は age.bin だけ読めばよい。100 カラムのテーブルで 1 カラムクエリなら I/O は 1/100。
圧縮効率: 同じカラムの値は型と分布が似ており、country カラムは少数値の繰り返しで LZ4 10 倍以上圧縮可能。Row は型がバラバラで圧縮しにくい。
キャッシュフレンドリ: 連続メモリのシーケンシャルスキャンは L1 キャッシュ効率が良い。
SIMD: 同じ型の値が連続するので AVX 256-bit レジスタに int32 を 8 個ロードし並列処理できる。
3.3 Row ベースが有利な場合
- Point lookup (
WHERE id = 42)。 - Update 中心のワークロード。
- 少数行 × 全カラム返却。
ClickHouse は OLTP には使わないこと。
4. MergeTree — ClickHouse の心臓
4.1 基本アイデア
LSM-tree からインスピレーションを受けつつ Columnar 向けに再設計。
Insert -> メモリバッファ -> ディスクに "Part" 生成 (カラム別ファイル)
|
バックグラウンドでマージ
|
より大きなソート済み Part
4.2 Part 構造
Part はディレクトリ:
/var/lib/clickhouse/data/mydb/mytable/
20240101_1_1_0/ # partition_minBlock_maxBlock_level
checksums.txt
columns.txt
count.txt
primary.idx # sparse primary index
user_id.bin # column file
user_id.mrk2 # marks (offsets)
event_time.bin
event_time.mrk2
revenue.bin
revenue.mrk2
各カラムに .bin と .mrk2 のペア。Marks は各 Granule の .bin オフセット。
4.3 Granule と Sparse Primary Index
MergeTree の核心は「全行にインデックスを置かない」こと。
- デフォルト 8192 行 が 1 Granule。
- Primary index は各 Granule の 先頭行 のみを指す。
WHERE user_id = 12345 では、メモリ上の Primary index を binary search して対象 Granule を特定し、.mrk2 でオフセットを引き、8192 行を読み込んで中から検索する。インデックスは従来型の 1/8192 サイズ。10 億行テーブルでもインデックスは数 MB で完全にメモリ常駐。
4.4 なぜ Sparse で良いのか
OLAP は多数行の集計が主で、Granule 単位 (8192 行) の読み込みはディスク I/O のページ単位 (4-32 KB) と相性が良い。Point lookup はサポートされるが ClickHouse の強みではない。
4.5 ORDER BY が Primary Key
CREATE TABLE events (
event_time DateTime,
user_id UInt64,
event_type String,
revenue Decimal(10, 2)
) ENGINE = MergeTree
ORDER BY (event_time, user_id)
PARTITION BY toYYYYMM(event_time);
データは (event_time, user_id) 順に物理配置され、Primary index はそのソート順の sparse サンプル。
4.6 Partitioning
PARTITION BY toYYYYMM(event_time) で月別に別ディレクトリ。Partition pruning、DROP PARTITION による高速削除、パーティション単位のバックアップ/レプリケーションが可能。パーティション数が多すぎると Part 数が膨張するので月次または週次が推奨。
5. Write Path
5.1 INSERT の流れ
- 入力を batch として受ける。
- メモリ上にカラム別ベクトル構築。
ORDER BYでソート。- 各カラムを別ファイルに圧縮書き込み。
- Part ディレクトリ作成、manifest 更新。
新しい Part が 1 つ作られ、既存 Part と並行して存在する。
5.2 バックグラウンドマージ
バックグラウンドスレッドが小さな Part を大きな Part に合併する。同一パーティション内のみ、オンラインでマージ、完了後に旧 Part を削除。
5.3 なぜマージが必須か
Part 数がデフォルトでパーティションあたり 300 を超えるとクエリが遅くなり「Too many parts」エラー。UPDATE/DELETE の mutation もマージ時に実際に適用される。
5.4 Insert 最適化
1 行ずつの INSERT は毎回新 Part を作るので Part 数が爆発する。数千から数万行を一括 INSERT するか、Kafka table engine や外部 ingester で batch する。
6. Read Path と Vectorized Execution
6.1 クエリの段階
- パースとプラン生成。
- Partition pruning。
- Primary index で Granule 選択。
- 必要カラムだけ読み込み。
- Vectorized Execution。
- GROUP BY をハッシュテーブルで集計。
- ソートして返却。
6.2 Vectorized Execution
従来 DB は Volcano モデル (1 行ずつ next()) で関数呼び出しオーバーヘッドが大きく、分岐予測失敗や pipeline stall が多い。
ClickHouse は ブロック単位 (~65536 行) で処理:
while (block = scan.next_block()) { // 65536 rows
auto mask = filter_block(block); // SIMD
aggregate_block(block, mask); // SIMD
}
コンパイラがループを AVX2 に自動ベクトル化。
6.3 SIMD の実例
// Naive
int64_t sum = 0;
for (int i = 0; i < n; i++) sum += revenue[i];
// AVX2 intrinsics
__m256i sum_vec = _mm256_setzero_si256();
for (int i = 0; i < n; i += 4) {
__m256i v = _mm256_loadu_si256((__m256i*)&revenue[i]);
sum_vec = _mm256_add_epi64(sum_vec, v);
}
4-8 倍の高速化。ClickHouse は多くをコンパイラに任せ、ベクトル化しやすいコードスタイルを採る。
6.4 ランタイム CPU Dispatch
SSE2、SSE4.2、AVX2、AVX-512 を CPUID で検出し、最適実装を選択する。
7. Data Skipping Index
Primary index は ORDER BY カラムにしか効かない。他のカラム向け:
7.1 Bloom Filter
ALTER TABLE events
ADD INDEX idx_user_email user_email TYPE bloom_filter GRANULARITY 4;
Bloom で「ここには無い」と分かった Granule をスキップ。
7.2 MinMax
ADD INDEX idx_revenue revenue TYPE minmax GRANULARITY 1;
非常に効果的かつ安価。デフォルト推奨。
7.3 Set
ADD INDEX idx_country user_country TYPE set(100) GRANULARITY 4;
低カーディナリティのカラム向け。
7.4 N-gram
ADD INDEX idx_log_msg log_message TYPE ngrambf_v1(4, 1024, 3, 0) GRANULARITY 1;
ログ文字列の LIKE '%error%' を加速。
7.5 落とし穴
Granule 粒度なので個別行は除外不可。Part メタデータが肥大化し書き込みコストも増える。慎重に選ぶこと。
8. 圧縮
カラム別に独立コーデック。
8.1 基本コーデック
- LZ4 (デフォルト): 高速、中程度の圧縮率。
- ZSTD: 高圧縮率、やや遅い。レベル 1-22。コールドデータ向け。
CREATE TABLE events (
event_time DateTime CODEC(DoubleDelta, ZSTD(3)),
user_id UInt64 CODEC(T64, LZ4)
)
8.2 Delta / DoubleDelta
Delta は [1000, 1, 2, 2, 5, 2] のように差分を格納。DoubleDelta は差分の差分で、定期タイムスタンプではほぼ 0 連続に。10 倍以上の圧縮もざら。
8.3 Gorilla
Facebook 由来の時系列向け。浮動小数を XOR で圧縮し、0 が多いほど圧縮率が上がる。メトリクスカラムで 5-10 倍。
8.4 T64
整数を最小ビット幅にビットパッキング。
8.5 コーデックチェーン
event_time DateTime CODEC(DoubleDelta, LZ4)
先に変換、後に汎用圧縮でさらに比率向上。
8.6 実際の圧縮率
Raw CSV 100 GB -> Row DB 80 GB (LZ4) -> ClickHouse 10-15 GB。Columnar と専用コーデックで Raw 比 7-10 倍。
9. MergeTree Family
9.1 ReplacingMergeTree
同一キーの重複をマージ時に除去し、最新 updated_at のみ残す。
CREATE TABLE users (
user_id UInt64,
name String,
updated_at DateTime
) ENGINE = ReplacingMergeTree(updated_at)
ORDER BY user_id;
クエリ時に重複が見える場合は FINAL で強制マージ (遅い)。
9.2 SummingMergeTree
同一キーの数値カラムをマージ時に合算。
CREATE TABLE daily_stats (
date Date,
user_id UInt64,
pageviews UInt64,
clicks UInt64
) ENGINE = SummingMergeTree()
ORDER BY (date, user_id);
マージ未完の可能性があるのでクエリ時は GROUP BY + sum() を明示。
9.3 AggregatingMergeTree
任意の集約関数の 中間状態 を保存。
CREATE TABLE hourly_stats (
hour DateTime,
country String,
unique_users AggregateFunction(uniq, UInt64),
avg_revenue AggregateFunction(avg, Decimal(10, 2))
) ENGINE = AggregatingMergeTree()
ORDER BY (hour, country);
Materialized View と組み合わせて使う。
9.4 CollapsingMergeTree
sign Int8 (+1/-1) で旧状態を打ち消す。MySQL binlog のレプリケーションに便利。
9.5 VersionedCollapsingMergeTree
Version 明示版の Collapsing。マージ順を保証。
9.6 GraphiteMergeTree
Graphite 互換の時系列メトリクス。TTL による解像度低減。
10. Materialized View
MV は Insert trigger のように振る舞う。
10.1 基本
CREATE MATERIALIZED VIEW hourly_pageviews_mv
ENGINE = SummingMergeTree()
ORDER BY (hour, page)
AS
SELECT
toStartOfHour(event_time) AS hour,
page,
count() AS views
FROM events
GROUP BY hour, page;
events への Insert ごとに対象行で SELECT が走り結果が MV に挿入される。クエリは原本 10 億行ではなく MV の数百万行に当てるので 100 倍以上速い。
10.2 AggregateFunction MV
CREATE MATERIALIZED VIEW user_metrics_mv
ENGINE = AggregatingMergeTree()
ORDER BY (date, user_id)
AS
SELECT
toDate(event_time) AS date,
user_id,
uniqState(session_id) AS unique_sessions,
sumState(revenue) AS total_revenue
FROM events
GROUP BY date, user_id;
クエリ側では uniqMerge / sumMerge で状態をマージ。DISTINCT 集計も事前計算できる。
10.3 Fan-out
1 回の Insert が複数の MV (hourly、daily、country、user) を同時トリガ。クエリに応じて最適な MV を選ぶ。
10.4 注意点
原本 Insert が失敗すれば MV も失敗 (atomic)。MV は Insert 時点のデータのみを見る。大きな JOIN は遅い。TTL は原本から MV に伝播しない。
11. Projections
MV の進化版 (ClickHouse 21+)。同じテーブル内に別ソート順のデータを保持する。
ALTER TABLE events
ADD PROJECTION p_country
(
SELECT event_time, user_country, revenue
ORDER BY (user_country, event_time)
);
ALTER TABLE events MATERIALIZE PROJECTION p_country;
クエリが WHERE user_country = 'US' を使うと ClickHouse が 自動で Projection を選択。
| 項目 | Materialized View | Projection |
|---|---|---|
| 保存場所 | 別テーブル | 同じテーブル |
| 使用 | 明示名 | 自動選択 |
| 削除/TTL | 独立 | 原本に追従 |
| 複雑クエリ | 可能 | 限定的 |
12. Distributed Engine とシャーディング
12.1 コンセプト
- Local table: 各 Shard が自前データを保持。
- Distributed table: クエリを複数 Shard にルーティング。
12.2 Cluster 設定
<clickhouse>
<remote_servers>
<my_cluster>
<shard>
<replica>
<host>shard1-r1</host>
<port>9000</port>
</replica>
<replica>
<host>shard1-r2</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>shard2-r1</host>
</replica>
</shard>
</my_cluster>
</remote_servers>
</clickhouse>
12.3 Distributed Table
CREATE TABLE events_distributed ON CLUSTER my_cluster AS events
ENGINE = Distributed(my_cluster, default, events, rand());
12.4 クエリ実行
コーディネータが全 Shard にクエリを送り、各 Shard がローカル集計、部分結果をコーディネータがマージして最終結果を返す。MapReduce 類似でネットワーク以外は高効率。
12.5 シャーディングキー
良い選択: 高カーディナリティで均等分布、クエリに頻出 (ローカルクエリ可能)。悪い選択: country (偏り)、timestamp (ホットシャード)。
12.6 レプリケーション
ReplicatedMergeTree で Shard 内レプリケーション:
CREATE TABLE events (...) ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/events',
'{replica}'
)
ORDER BY ...;
Keeper/ZooKeeper を介して同期、自動フェイルオーバー。
13. Keeper — ZooKeeper の置き換え
ZooKeeper は Java 実装でメモリを食い GC pause があり、ClickHouse のメタデータ高頻度更新パターンに最適ではなかった。2021 年に登場した ClickHouse Keeper は C++ 再実装、Raft 自前実装、ZK と同一 API、ZK 比 1.5-2 倍高速、メモリは半分、単一バイナリで ClickHouse に組み込み可能。2023 年以降の新デプロイはほぼ Keeper がデフォルトで、ZK は legacy 扱い。
<clickhouse>
<keeper_server>
<tcp_port>9181</tcp_port>
<server_id>1</server_id>
<raft_configuration>
<server><id>1</id><hostname>keeper1</hostname><port>9234</port></server>
<server><id>2</id><hostname>keeper2</hostname><port>9234</port></server>
<server><id>3</id><hostname>keeper3</hostname><port>9234</port></server>
</raft_configuration>
</keeper_server>
</clickhouse>
14. 実務チューニング
- Insert batch: 1 万から 10 万行。Buffer table、Kafka table engine、
async_insertを活用。 - ORDER BY 設計: 低カーディナリティから高カーディナリティ順。通常 3-5 カラム。
- Part 管理:
system.partsで監視。デフォルト上限はパーティションあたり 300。必要に応じてbackground_pool_sizeを増やす。 - メモリ制限:
max_memory_usage。max_bytes_before_external_group_byでディスク spill。 - 観測:
system.parts、system.query_log、system.metrics、system.events、system.asynchronous_metrics。
SELECT query, elapsed, memory_usage
FROM system.query_log
WHERE event_date = today()
ORDER BY elapsed DESC
LIMIT 10;
15. ClickHouse vs 代替
15.1 DuckDB
組み込み型 Columnar DB (SQLite ライク)。単一プロセス、分散なし。GB から TB スケールのローカル分析に最適。同じ Vectorized 哲学だがデプロイ形態が違う。
15.2 Druid / Pinot
リアルタイム取り込みとダッシュボード特化。Druid の segment は Part に似る。Druid はロールアップポリシーを事前定義する必要があり柔軟性が低い。ClickHouse は SQL が豊富。
15.3 Snowflake / BigQuery
フルマネージド。コストは高いが運用ゼロ。ClickHouse は自己ホスト可能で安価、一部クエリは高速だが運用チームが必要。
15.4 StarRocks / Doris
中国主導の代替。MySQL 互換フロントエンド、Join 性能が優れる。エコシステムは小規模。
16. 学習ロードマップ
- 公式ドキュメントと NYC Taxi チュートリアルから始める。
- ORDER BY とパーティショニング設計を練習。Altinity Knowledge Base が実務向け。
system.*テーブルを探索し、Alexey Milovidov の「ClickHouse under the hood」講演を視聴。- 分散クエリ設計、Keeper 運用、Materialized View パターンへ。
17. チートシート
Columnar ストレージ:
- カラム別ファイル
- 必要カラムのみ読む
- 圧縮効率が高い
- SIMD フレンドリ
MergeTree:
- Part = カラム別 .bin + .mrk2 のディレクトリ
- Granule = 8192 行
- Sparse primary index
- バックグラウンドマージ
- Partition pruning
Family:
MergeTree (基本), Replacing (重複除去), Summing (合算),
Aggregating (一般集約), Collapsing (状態取り消し), Replicated
Vectorization:
- ブロック単位 (~65536 行)
- 自動 SIMD (AVX2/AVX-512)
- ランタイム CPU dispatch
インデックス:
Primary: sparse, Granule 先頭
Skipping: minmax / bloom / set / ngrambf
圧縮:
LZ4 (default), ZSTD (cold)
Delta, DoubleDelta (timestamp)
Gorilla (float), T64 (bit packing)
Materialized View:
- Insert trigger
- 事前集約
- AggregateFunction state
Distributed:
- Shard x Replica
- Distributed engine = query router
- ReplicatedMergeTree + Keeper
Projection:
同一テーブル内に別ソート順、自動選択
vs 競合:
DuckDB (embedded), Druid/Pinot (real-time),
Snowflake/BQ (managed)
18. クイズ
Q1. Columnar ストレージが OLAP クエリを速くする 3 つの理由は?
A. (1) I/O 削減 — SELECT AVG(age) は age カラムだけ読めばよく 100 カラムテーブルで 100 分の 1 の I/O。(2) 圧縮率 — 同じカラムは型と分布が似るため country のような少数値反復は LZ4 で 10 倍以上。(3) SIMD — 同型連続値で AVX2 レジスタに int32 を 8 個ロードし並列処理。さらにキャッシュライン効率も向上。これらが合わさり Postgres 比 100-1000 倍高速。
Q2. Sparse Primary Index とは何で、なぜそれで十分なのか?
A. 全行ではなく Granule (デフォルト 8192 行) ごとに 1 エントリ だけ保存。10 億行でもインデックスは数 MB で完全メモリ常駐。Point lookup は Granule 単位のスキャンになるが、OLAP は多数行集計が主流でディスク I/O のページ単位と相性が良い。OLTP の dense index 哲学とは正反対の設計。
Q3. Vectorized Execution が Volcano モデルより速い理由は?
A. 関数呼び出しオーバーヘッド排除と SIMC 活用。Volcano は行ごとの next() で数十 ns × 数億行が致命的、分岐予測失敗も多い。Vectorized は ブロック単位 (~65536 行) で処理するので関数呼び出しは 1 ブロック 1 回、連続同型データをコンパイラが自動で AVX2/AVX-512 化。実測 10-100 倍。
Q4. ORDER BY で低カーディナリティから高カーディナリティ順に配置する理由は?
A. Prefix ベースのデータスキップを最大化。物理配置が ORDER BY 順なので、低カーディナリティの country を先頭に置くと同じ国の値が長い連続範囲を占め、WHERE country = 'US' が大きな Granule 範囲をまとめてスキップできる。event_time を先にすると国が散らばり全スキャンになる。event_time のような範囲クエリ用カラムは後ろに置けば両方で効率的。実務原則: フィルタに頻出する低カーディナリティを先に。
Q5. SummingMergeTree と AggregatingMergeTree の違いは?
A. 対応する集約の範囲。SummingMergeTree は同一キーの数値カラムを 合算のみ — 単純で高速だが sum 以外は不可。AggregatingMergeTree は任意の集約関数の 中間状態 (AggregateFunction(uniq, ...) 等) を保存しマージ時に state 同士を結合。-State / -Merge suffix で distinct count や quantile も事前計算可能。実務: 単純カウンタは Summing、高度な集計は Aggregating + Materialized View。
Q6. ClickHouse Keeper が ZooKeeper を置き換えた理由は?
A. 運用複雑度と ClickHouse ワークロード特化。ZooKeeper は Java でメモリ消費が大きく GC pause があり、ClickHouse の高頻度メタデータ更新パターンに最適ではなかった。Keeper は C++ 再実装 + Raft 自前実装で ZK 比 1.5-2 倍高速、メモリ半分。ZK API 互換でクライアント変更不要、単一バイナリで ClickHouse と同プロセスに組み込めてスタック単純化。2023 年以降の新規デプロイはほぼ Keeper デフォルト、ZK は legacy。
Q7. ClickHouse と DuckDB は共に Columnar + Vectorized だが用途が異なる理由は?
A. デプロイモデルとスケーラビリティ。ClickHouse は サーバアーキテクチャ — ネットワークサービス、分散、レプリケーション、TB から PB 規模、万単位の QPS。DuckDB は 組み込みアーキテクチャ — プロセスにライブラリとして内蔵 (SQLite 類似)、単一ノード、ファイルベース (.duckdb)。ローカル開発やノートブック分析、ETL スクリプトに最適。どちらも優れた Vectorized エンジンだが「分散が必要か」で分かれる。Jupyter で 10GB Parquet なら DuckDB、Kafka から毎秒 100 万イベント + リアルタイムダッシュボードなら ClickHouse。良し悪しではなく解く問題が違う。
この記事が役に立ったら、次の記事もチェックしてください:
- "RocksDB & LSM-Tree Deep Dive" — Row ベース LSM との比較。
- "Columnar Storage Parquet/ORC/Arrow/Dremel" — カラムフォーマットの基礎。
- "Snowflake Architecture Deep Dive" — 別の Columnar OLAP アプローチ。
- "Apache Spark Catalyst & Tungsten" — 分散分析エンジンの代替。
현재 단락 (1/340)
- **ClickHouse** は Yandex が 2009 年に内部分析用に作り、2016 年にオープンソース化。C++ で書かれた Columnar OLAP エンジンの代表格。