目次を表示する

DB 設計の軸 2026 ─ ドメイン駆動と特性駆動の二つの流派を行き来する 19 章

OLAP の本質 ─ 列指向・Bulk read という別世界

OLAP の本質 ─ 列指向・Bulk read という別世界

OLAP(Online Analytical Processing)は OLTP の双子のような対立軸として育ってきた。同じ「DB」と呼ばれるが、内部で起きていることは別世界。OLTP のセオリーを OLAP に持ち込むと破綻する

この章では ClickHouse / DuckDB を主役に、OLAP の本質を見る。

OLAP の定義

Kleppmann『DDIA』第 3 章 より:

OLAP: アナリスト向け、各クエリは大量のレコードをスキャンし、少数の列を読み、aggregation する。

第 3 章の 6 軸で言うと:

  • read/write: read 圧倒
  • レイテンシ: 秒〜分
  • 整合性: Eventual(スナップショット同期で十分)
  • データ形状: 列
  • 寿命: Warm + Cold
  • スケール: Sharding / 分散

OLTP が「多くの小さなトランザクション」だったのに対し、OLAP は「少数の大きなクエリ」。1 クエリで億単位の行をスキャンし、SUM / AVG / COUNT(DISTINCT) を返す。

なぜ列指向が速いのか

OLAP の典型クエリ:

SELECT
  toStartOfMonth(created_at) AS month,
  category,
  SUM(amount) AS revenue
FROM orders
WHERE created_at > now() - INTERVAL 1 YEAR
GROUP BY month, category;

このクエリで触る列は created_at / category / amount の 3 つだけ。customer_nameshipping_address は読まない。

行指向ストレージだと:

ページ 1: [id=1, customer=Alice, address=..., created_at=2026-01-01, category=A, amount=100, ...]
ページ 2: [id=2, customer=Bob, address=..., created_at=2026-01-02, category=B, amount=200, ...]

各行を全カラム読み込まないと created_at を取れない。10 億行なら、不要なカラムも 10 億回読む。

列指向だと:

created_at 列: [2026-01-01, 2026-01-02, 2026-01-03, ...]
category 列:   [A, B, A, B, A, ...]
amount 列:     [100, 200, 150, 300, 80, ...]
customer 列:   [Alice, Bob, Carol, ...]  ← このクエリでは読まない

必要な列だけを連続的に読める。I/O が圧倒的に少ない

graph TB
  subgraph "行指向 OLTP(PostgreSQL)"
    R1[Row 1: 全列]
    R2[Row 2: 全列]
    R3[Row 3: 全列]
  end

  subgraph "列指向 OLAP(ClickHouse / DuckDB)"
    C1[id 列: 連続]
    C2[created_at 列: 連続]
    C3[amount 列: 連続]
  end

  R1 -.集計クエリは不利.-> X[全行・全列スキャン]
  C1 -.集計クエリ.-> Y[必要列のみスキャン]

  style X fill:#ffe1e1
  style Y fill:#e1ffe1

圧縮効率の違い

列指向のもう一つの強みは 圧縮。同じ列の値は性質が似ているため、圧縮率が極めて高い。

category 列: [A, A, A, B, B, B, A, A, ...]
→ Run-Length Encoding: [(A, 3), (B, 3), (A, 2), ...]

created_at 列(タイムスタンプ): [t, t+1, t+5, t+8, ...]
→ Delta encoding: [t, +1, +4, +3, ...]

ClickHouse / Parquet / DuckDB は数十種類の圧縮アルゴリズムを使い分け、同じデータが行指向の 1/10 のサイズになることも珍しくない。これがそのまま I/O 削減 → クエリ速度に効く。

Vectorized execution

OLAP DB は vectorized execution(ベクトル化実行)を採用する。

OLTP の典型的な実行:行ごとに処理する(tuple-at-a-time)。

// 概念コード(OLTP 流)
for (row in rows) {
  if (row.created_at > threshold) {
    sum += row.amount;
  }
}

OLAP は batch-at-a-time

// 概念コード(OLAP 流、vectorized)
auto batch = read_column_batch("amount", 1024);   // 1024 行を一気に
auto mask = batch > threshold;                     // SIMD で並列比較
sum += sum_with_mask(batch, mask);                 // SIMD で並列加算

CPU の SIMD 命令や cache line を最大限使えるため、行ごと処理に比べて 10-100 倍速い。DuckDB / ClickHouse / Snowflake / BigQuery などモダン OLAP は全部この路線。

ClickHouse の MergeTree

OLAP の代表的な実装として ClickHouse の MergeTree ストレージエンジンを見る。

基本構造

graph LR
  A[INSERT バッチ] --> P1[Part 1<br/>sorted by ORDER BY]
  A --> P2[Part 2<br/>sorted by ORDER BY]
  A --> P3[Part 3<br/>sorted by ORDER BY]

  P1 --> M[Merge<br/>バックグラウンド]
  P2 --> M
  P3 --> M
  M --> P4[統合された Part]

  style M fill:#fff4e1
  • INSERT 単位で immutable な Part を作る
  • バックグラウンドで Part を merge して大きくする
  • 各 Part 内は ORDER BY で sort されている
  • ORDER BY が primary sort key = sparse index の元

ORDER BY = primary key(PostgreSQL とは別物)

ClickHouse の PRIMARY KEY は uniqueness 制約ではないデータの並び順を決める。

CREATE TABLE events (
  user_id UInt64,
  event_type String,
  timestamp DateTime,
  data String
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (user_id, event_type, timestamp);

ここで ORDER BY (user_id, event_type, timestamp)設計の核心。データはこの順で物理的にソートされ、sparse index でクエリの skip scan が決まる。

Altinity の ClickHouse Knowledge Base のベストプラクティス:

  • 3-5 カラム が適切
  • 低カーディナリティを先頭に、高カーディナリティを後ろに
  • 時系列データなら timestamp は最後toStartOfDay(timestamp) を間に挟む

OLTP の B-tree インデックスとは設計の流儀が真逆。これを次章の「設計編」で深掘りする。

OLAP は Outside Data の世界

ここまで読んで気づくのは、OLAP DB は Pat Helland の “Outside Data” の住処だということ。

  • Immutable: 一度書いたデータは原則変更しない(INSERT 中心、UPDATE/DELETE は重い)
  • Identifier で参照: 「2026-05-09 の取引データ」と時間軸で識別される
  • Stable: 一度確定したら結果が変わらない

OLTP(Inside Data)から ETL / CDC / Stream 経由で OLAP(Outside Data)へ流れる。これは第 12 章の Stream で再登場するテーマ。

graph LR
  subgraph "Inside data: OLTP"
    A[PostgreSQL]
  end

  subgraph "Outside data: OLAP"
    B[ClickHouse]
    C[DuckDB]
    D[BigQuery]
  end

  A -- ETL / CDC / Stream --> B
  A -- ETL / CDC / Stream --> C
  A -- ETL / CDC / Stream --> D

  style A fill:#e1f5ff
  style B fill:#ffe1e1
  style C fill:#ffe1e1
  style D fill:#ffe1e1

OLAP DB を OLTP として使うとどうなるか

逆もまた真:

-- ClickHouse で 1 件 UPDATE
ALTER TABLE events UPDATE data = 'fixed' WHERE user_id = 123;
-- → 該当行を含む全 Part を書き換える非同期処理
-- → 数秒〜数分かかる、I/O も重い

OLAP は 更新を捨てている。代わりに大量スキャンと aggregation を最適化する。これは「ワークロードに合った DB を選ぶ」の典型例。

この章の要点

  • OLAP = 大量レコードスキャン、少数列、aggregation 中心
  • 列指向で I/O 削減、圧縮効率向上、vectorized execution
  • ClickHouse の MergeTree は immutable Part + バックグラウンド merge + ORDER BY ソート
  • ORDER BY が設計の核心。低 → 高 cardinality、3-5 列、時系列は最後
  • OLAP は Outside data の世界。Immutable、ID 参照、Stable

次章への問いかけ

列指向の世界では「何の順で並べるか」が設計の起点になる。OLTP の正規化的な発想は通用しない。

次章で OLAP の設計編。Sort key の選び方・Dimensional Modeling・Materialized View、そして yuzutas0(風音屋)が説くアジャイルなデータモデリング。