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);
| 順位 | カラム | 理由 |
|---|---|---|
| 1 | organization_id | 低カーディナリティ、ほぼ全クエリで filter |
| 2 | event_type | 低カーディナリティ、よく filter |
| 3 | user_id | 中カーディナリティ |
| 4 | timestamp | 高カーディナリティ、最後 |
失敗パターン
-- ❌ 高カーディナリティを先頭に
ORDER BY (timestamp, organization_id, ...)
-- → ほとんどのクエリで先頭 column の filter が効かず、フルスキャンに近づく
-- ❌ 全部のカラムを ORDER BY に
ORDER BY (col1, col2, col3, col4, col5, col6, col7)
-- → 後半は実質効かない、メモリオーバーヘッドだけ増える
Partition key は別物
PARTITION BY は Part を物理的に分ける単位。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 の流儀の世界。