目次を表示する

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

OLAP 設計の意思決定 ─ Sort key・Dimensional Modeling・MV

OLAP 設計の意思決定

OLAP DB の設計は、OLTP のセオリーが通用しない別世界。列の並び順集計の前計算 が設計の主役になる。

判断 1:Sort key(ORDER BY)の選び方

ClickHouse の ORDER BY は最重要設計。これがストレージの物理配置 + sparse index の構造を決める。

ベストプラクティスのテンプレート

-- 典型的な時系列分析テーブル
CREATE TABLE events (
  organization_id UInt32,
  user_id UInt64,
  event_type LowCardinality(String),
  timestamp DateTime,
  ...
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (organization_id, event_type, user_id, timestamp);

Altinity Knowledge Base の指針:

順位カラム理由
1organization_id低カーディナリティ、ほぼ全クエリで filter
2event_type低カーディナリティ、よく filter
3user_id中カーディナリティ
4timestamp高カーディナリティ、最後

失敗パターン

-- ❌ 高カーディナリティを先頭に
ORDER BY (timestamp, organization_id, ...)
-- → ほとんどのクエリで先頭 column の filter が効かず、フルスキャンに近づく

-- ❌ 全部のカラムを ORDER BY に
ORDER BY (col1, col2, col3, col4, col5, col6, col7)
-- → 後半は実質効かない、メモリオーバーヘッドだけ増える

Partition key は別物

PARTITION BYPart を物理的に分ける単位PARTITION BY toYYYYMM(timestamp) だと月ごとに別ディレクトリに保存される。古い月をまるごと DROP できる利点があるが、小さなテーブル(数 GB 以下)では設定しない方が良い ── Part が細切れになると merge コストが嵩む。

判断 2:Dimensional Modeling か Wide Table か

OLAP の schema 設計には大きく 2 つの流派がある。

流派 A:Kimball の Dimensional Modeling(Star Schema)

yuzutas0(風音屋)の “30 分でわかるディメンショナルモデリング” スライド が詳しい。古典的アプローチ:

graph TB
  F[Fact: orders<br/>order_id, customer_key, product_key, time_key, amount]

  D1[Dim: customers<br/>customer_key, name, segment]
  D2[Dim: products<br/>product_key, name, category]
  D3[Dim: time<br/>time_key, year, month, day]

  F --> D1
  F --> D2
  F --> D3

  style F fill:#e1f5ff
  style D1 fill:#fff4e1
  style D2 fill:#fff4e1
  style D3 fill:#fff4e1
  • Fact テーブル:実測値(売上、訪問数等)
  • Dimension テーブル:分析の切り口(顧客、商品、時間)
  • 集計は Fact を中心に Dimensions を JOIN

利点:分析者が直感的、変化に強い、再利用性高。

流派 B:Wide Table(One Big Table)

近年のクラウド DWH(BigQuery / Snowflake)と vectorized engine では、JOIN コストが下がったため、全部 1 テーブルに非正規化して入れる流派も力を持っている。

CREATE TABLE orders_wide (
  order_id UInt64,
  customer_id UInt32, customer_name String, customer_segment String,
  product_id UInt32, product_name String, product_category String,
  timestamp DateTime,
  year UInt16, month UInt8, day UInt8,
  amount Decimal(18, 2)
);
  • JOIN がないためシンプル速い
  • ただし dimension の更新が反映しづらい(dimension 列を全行 UPDATE する重さ)
  • 履歴の固定化としては自然(請求時の顧客名は変わらない)

判断軸

状況推奨
分析の柔軟性が高く、dimension が頻繁に変わるStar Schema(Kimball)
dimension は安定、JOIN コストを避けたいWide Table
dimension の履歴を残したい(SCD type 2)Star Schema + SCD
クラウド DWH(BigQuery / Snowflake)Wide Table 寄り
ClickHouse 等の自前運用Star Schema 寄り

yuzutas0 の主張は「ビジネス変化に追随できる構造を選ぶ」── つまり分析者の思考と一致する schema を選べ、という方向。これが Kimball 寄りに引かれる理由。

判断 3:Materialized View / Pre-aggregation

OLAP で「よく走るクエリは事前に計算しておく」のは強力。

ClickHouse の MV パターン

-- 元テーブル
CREATE TABLE events (
  org_id UInt32,
  event_type String,
  timestamp DateTime,
  amount Decimal(18, 2)
) ENGINE = MergeTree() ORDER BY (org_id, timestamp);

-- 日次集計の MV
CREATE MATERIALIZED VIEW events_daily
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(day)
ORDER BY (org_id, event_type, day)
AS
SELECT
  org_id,
  event_type,
  toDate(timestamp) AS day,
  sum(amount) AS total
FROM events
GROUP BY org_id, event_type, day;

INSERT が走るたびに MV が自動的に更新される。集計クエリは MV を読むだけで終わる。

判断軸

場面戦略
同じ集計を頻繁に繰り返すMV で前計算
集計軸が動的に変わるMV ではなく実 query
ストリーミングで来る集計MV + AggregatingMergeTree
バッチで完全再計算可能dbt 等で MV ではなくテーブル化

判断 4:データ進化と分析の独立性

OLAP の重要な原則:OLTP の schema 変更が OLAP に直結しないようにする

❌ 直接結合

[OLTP: orders] -- 直接 JOIN --> [OLAP: ダッシュボード]

OLTP の orders テーブルに列追加する → ダッシュボードの SQL が壊れる、または古い値を返す。

✅ ETL レイヤーを挟む

[OLTP: orders] -> [ETL] -> [OLAP: orders_fact] -> [OLAP: orders_daily MV] -> [ダッシュボード]

ETL レイヤーで「どう投影するか」を決める。OLTP の進化が下流にすぐ影響しない。dbt や Dataform のようなツールが、この投影層を SQL で記述させる。

yuzutas0 が訳した『アジャイルデータモデリング』 の主題は、まさにこの進化的なデータモデリング ── OLAP schema を一度決めて固定するのではなく、ビジネス変化に合わせて段階的に進化させる作法。

判断 5:Read 専用とは限らない ─ INSERT バッチング

OLAP は read 圧倒的だが、書き込みは大量にバッチで来る。これも設計判断。

❌ アンチパターン:1 行ずつ INSERT

-- ClickHouse で 1 行ずつ INSERT
INSERT INTO events VALUES (...);  -- 1 件
INSERT INTO events VALUES (...);  -- 1 件
-- → 1 INSERT ごとに 1 Part が作られ、Merge が追いつかなくなる

✅ バッチ INSERT

-- 1 INSERT = 数千〜数万行
INSERT INTO events VALUES (..., ..., ..., ..., ...);
-- → 1 Part に大量の行、merge が効率化

公式推奨:1 INSERT は数千行以上、頻度は秒間 1 回程度。これに合わせるために、アプリ側で バッファリング するか、Kafka 経由で集約してから INSERT する設計が一般的。

これが第 12 章の Stream への接続。OLTP → Stream → バッチ INSERT → OLAP の流れが現代の標準パイプライン。

OLAP 設計の意思決定マトリクス

graph TB
  Q1[Sort key を選ぶ] --> A1[低→高カーディナリティ<br/>3-5 列、時系列は最後]
  Q2[Schema スタイル] --> A2[Kimball Star or Wide<br/>Dimension 変化頻度で決定]
  Q3[MV 戦略] --> A3[頻出集計のみ前計算<br/>動的な集計は実 query]
  Q4[OLTP との分離] --> A4[ETL レイヤー必須<br/>dbt / Dataform 等]
  Q5[INSERT 戦略] --> A5[バッチ化必須<br/>Stream 経由が標準]

  style Q1 fill:#e1f5ff
  style Q2 fill:#e1f5ff
  style Q3 fill:#e1f5ff
  style Q4 fill:#e1f5ff
  style Q5 fill:#e1f5ff

ドメインとの関係

OLAP のテーブルは、ドメインの Aggregate と一致しないorders_fact は商品・顧客・時間の “切り口” の交点で、それ自体は Aggregate ではない。

これは DDD の純粋主義からは “詳細” だが、分析業務は分析業務として独自のドメインを持つ ── と考える方が綺麗。yuzutas0 の主張:分析もドメインモデリングである

つまり OLAP 設計は 「分析者という別のドメイン」のためのモデリング。OLTP 側の Aggregate がそのまま OLAP の schema にならないのは当然。

この章の要点

  • ORDER BY = ストレージ物理配置の核心。低→高カーディナリティ、3-5 列、時系列は最後
  • Schema は Kimball Star Schema か Wide Table の二択。Dimension 変化頻度で決める
  • Materialized View は頻出集計の前計算。動的集計には不向き
  • OLTP との接続は ETL レイヤーで疎結合に
  • INSERT はバッチ化必須。Stream 経由が標準パイプライン
  • OLAP は「分析者ドメイン」のためのモデリング ── OLTP の schema をそのまま持ち込まない

次章への問いかけ

OLAP は Outside data を扱う場所だった。だがもう一つ、外側にあるデータの住処がある。

次章は Cache ── データ構造を選ぶことが設計の出発点になる、Redis の流儀の世界。