Skip to content
Published on

ClickHouse Internals Deep Dive — MergeTree、Vectorized Execution、分散クエリ、Keeper 完全攻略 (2025)

Authors

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_countryrevenue の 2 カラムだけ、結果は小さな集計。Row ストレージでは不要カラムまで読まざるを得ず、行単位処理で関数呼び出しオーバーヘッドが蓄積する。

1.3 3 つの武器

  1. Columnar ストレージ: 必要なカラムだけをディスクから読む。
  2. Vectorized Execution: 一度に数万行を SIMD で処理。
  3. 高圧縮: ディスク 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 の流れ

  1. 入力を batch として受ける。
  2. メモリ上にカラム別ベクトル構築。
  3. ORDER BY でソート。
  4. 各カラムを別ファイルに圧縮書き込み。
  5. 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 クエリの段階

  1. パースとプラン生成。
  2. Partition pruning。
  3. Primary index で Granule 選択。
  4. 必要カラムだけ読み込み。
  5. Vectorized Execution。
  6. GROUP BY をハッシュテーブルで集計。
  7. ソートして返却。

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 ViewProjection
保存場所別テーブル同じテーブル
使用明示名自動選択
削除/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_usagemax_bytes_before_external_group_by でディスク spill。
  • 観測: system.partssystem.query_logsystem.metricssystem.eventssystem.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. 学習ロードマップ

  1. 公式ドキュメントと NYC Taxi チュートリアルから始める。
  2. ORDER BY とパーティショニング設計を練習。Altinity Knowledge Base が実務向け。
  3. system.* テーブルを探索し、Alexey Milovidov の「ClickHouse under the hood」講演を視聴。
  4. 分散クエリ設計、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" — 分散分析エンジンの代替。