緩やかに変化するディメンション(SCD)
顧客の引っ越しや商品分類の変更で、過去の分析集計が今の属性に塗り替わってしまう問題を根絶したい人へ。SCDのType1/2/3、サロゲートキーと有効期間、ファクトとの結合を原理から整理します。
- 1.ディメンションの属性は時間とともに変わる(顧客の地域、商品の分類)。SCDはこの変更履歴をどう保持するかの設計で、上書きするType1、変更のたびに行を足すType2、直前値だけ列で持つType3が基本。分析の正しさはファクトの数値だけでなくディメンションの時間的正しさに依存する。
- 2.Type2の核はサロゲートキー(意味を持たない代理キー)と有効期間(valid_from/valid_to)。ファクトには自然キーではなく、事実発生時点で有効だったディメンション行のサロゲートキーを結合時に確定して持たせる。これで過去の集計が当時の属性に固定され、後の変更で塗り替わらない。
- 3.SCD更新はCDCが運ぶ変更ストリームを、キー一致で既存の現在行を閉じて新しい現在行を挿入するMERGE(upsert)で実装する。冪等に書けば再実行しても履歴が二重化しない。レイクハウスのタイムトラベルはテーブル全体の物理版で、SCDはディメンション個別の論理的な履歴管理という点で役割が異なる。
ディメンションは時間とともに変わる
スタースキーマでは、数値の事実を持つ巨大なファクト表を、属性を持つ小さなディメンション表が囲みます(/data-engineering/olap-vs-oltp/ の非正規化ディメンションが前提です)。ここで見落とされがちなのが、ディメンションの属性は時間とともに変わるという事実です。顧客が引っ越して地域が変わる、商品のカテゴリが再編される、営業担当が交代する——こうした変化を分析基盤がどう扱うかで、集計の意味が根本から変わります。
問題を具体化します。ある顧客が2025年は東京、2026年に大阪へ引っ越したとする。「2025年の売上を地域別に見たい」とき、その売上は当時の東京で数えたいのか、それとも今の大阪で数えたいのか。これは実装ミスの話ではなく業務上の設計判断です。緩やかに変化するディメンション(SCD, Slowly Changing Dimension)は、この「属性の変更をどう保持するか」を型として整理したものです。
ファクトが毎秒何万行と増えるのに対し、ディメンション(顧客・商品・店舗)の属性変更はまれで、不定期です。年に数回引っ越す顧客、たまに再編される商品分類。この「めったに変わらないが、変わると過去の解釈に影響する」性質が『緩やかに変化する』という名前の由来で、だからこそ変更を検知して履歴に刻む専用の仕組みが要ります。高頻度の更新を捌く話ではありません。
Type1・Type2・Type3:履歴の残し方
SCDの中心は三つの型です。どれを選ぶかは「過去のファクトを、変更前・変更後のどちらの属性で集計したいか」で決まります。
| 型 | 変更時の挙動 | 履歴 | 過去ファクトの見え方 |
|---|---|---|---|
| Type1(上書き) | 属性を新値で上書きする | 残らない | 常に最新値で再集計される |
| Type2(行追加) | 旧行を閉じ、新値の行を追加 | 全世代を保持 | 当時の属性に固定される |
| Type3(列追加) | 現在列と直前列に値を持つ | 直前の1世代のみ | 現在/直前を列で選べる |
Type1は単純に属性を上書きします。履歴は残らず、過去のファクトも自動的に最新値で集計される。誤記の訂正や、履歴に意味がない属性(表示名の綴り直しなど)に向きます。実装は軽い代わり、「当時の値」は永久に失われます。
Type2が履歴管理の本命です。属性が変わったら既存の行を上書きせず、その行を「もう有効でない」と閉じ、新しい属性値を持つ新しい行を追加する。こうして同じ自然キー(例:顧客ID)に対し、時期の異なる複数の行が並びます。過去のファクトは当時有効だった行に結び付くので、後から属性が変わっても過去の集計は塗り替わりません。
Type3は「現在値」と「直前値」を別々の列で持ちます(例:current_region と previous_region)。保持できる履歴は直前の1世代だけですが、「組織再編の前後を並べて比べたい」といった、限られた履歴を軸として比較する用途に噛み合います。行を増やさないのでファクトとの結合構造も変わりません。
一つのディメンション表で全列を同じ型にする必要はありません。「地域は履歴を残したい(Type2)が、電話番号は最新だけでよい(Type1)」は普通に起こります。実務では列ごとに保持方針を決めるのが現実的で、Type2列が変わったときだけ新しい行世代を作り、Type1列は現在行を上書きする、という併用が定石です。まず「どの属性の変化が、過去の分析の解釈を変えるか」を洗い出すのが設計の出発点です。
サロゲートキーと有効期間:Type2の骨格
Type2を正しく回すには二つの道具が要ります。サロゲートキーと有効期間です。
サロゲートキー(surrogate key)は、業務的な意味を一切持たない代理キーです。顧客IDのような自然キーは、同じ顧客の複数世代を区別できません(東京時代の顧客Aも大阪時代の顧客Aも顧客ID=A)。そこでディメンションの各行世代に、単調増加の連番などで一意なサロゲートキーを別途振る。自然キーは「どの実体か」を、サロゲートキーは「その実体のどの時点の姿か」を指す、と役割を分けるのが要点です。
有効期間は、その行世代がいつからいつまで有効だったかを表す列の組(例:valid_from と valid_to)と、現在行を高速に引くための真偽フラグ(例:is_current)です。属性が変わった瞬間に、旧世代の valid_to を変更時刻で閉じ、新世代を valid_from = 変更時刻、valid_to = 無限大、is_current = true で開きます。
顧客A の Type2 ディメンション(surrogate_key が行世代を一意に識別)
surrogate_key | customer_id | region | valid_from | valid_to | is_current
--------------+-------------+--------+-------------+-------------+-----------
501 | A | 東京 | 2024-01-01 | 2026-03-15 | false
777 | A | 大阪 | 2026-03-15 | 9999-12-31 | true
期間は半開区間 [valid_from, valid_to) にすると境界の重複が起きない。
ある時点 t で有効な行は valid_from <= t かつ t < valid_to でちょうど1行。
valid_from 以上かつ valid_to 未満(半開区間)で有効と定義してください。両端を含める閉区間にすると、旧世代の valid_to と新世代の valid_from が同じ時刻になった瞬間、その時刻でちょうど2行が有効になり、ファクト結合が世代を一意に決められず行が二重にカウントされます。半開区間なら、任意の時点で有効な世代は必ず1行に定まります。境界の扱いは履歴管理の正しさを左右する核心です。
ファクトとの結合:時点の属性を焼き込む
SCDの価値が現れるのはファクトとの結合です。ここが初心者と実務者を分ける最重要点です。
素朴には、ファクトに顧客IDのような自然キーを持たせ、実行時にディメンションと結合すればよさそうに見えます。しかしType2では同じ自然キーに複数世代があるため、単純結合すると1つのファクトが複数のディメンション世代にマッチして行が増えます。正しい設計は、ファクトを書き込む時点で、その事実が発生した時刻に有効だったディメンション世代を引き当て、そのサロゲートキーをファクト行に確定して持たせることです。これを俗にディメンションの**焼き込み(surrogate key lookup)**と呼びます。
売上ファクトを取り込むとき、イベント時刻でディメンション世代を確定する
fact_sales(event_date=2025-06-10, customer_id=A, amount=1000)
→ event_date が [2024-01-01, 2026-03-15) に入る世代を引く
→ surrogate_key = 501(東京時代)を確定して書き込む
fact_sales(customer_surrogate_key=501, amount=1000)
以後この売上は「東京」で固定。顧客Aが大阪へ引っ越しても
501 は東京世代を指したままなので、2025年の集計は塗り替わらない。
これで、ファクトはサロゲートキーでディメンションの特定世代に直結します。結合はサロゲートキー同士の等値結合となり、有効期間の範囲比較を実行時にやる必要がなくなる。過去の集計はイベント時刻の属性に永久に固定され、後の変更に影響されません。「地域は変更前で見たいがカテゴリは最新で見たい」といった要求も、Type2列はサロゲートキー経由で当時の値、Type1列は現在値の上書きを追う、と両立できます。
ファクトが数十億行でも、結合相手のディメンションは小さいので、サロゲートキー等値結合は分散処理でブロードキャスト結合(小さい表を各ノードへ配り、シャッフルを回避)に落ちて軽く済みます。ファクトをイベント日付でパーティショニングしておけば、対象期間だけを読んで結合できる(/data-engineering/partitioning-bucketing/ のプルーニングとブロードキャストがそのまま効きます)。SCD設計は物理レイアウト設計と地続きです。
更新の実装:CDC・MERGE・冪等性
ディメンションの変更をどう検知し、どう履歴に刻むか。源泉は業務DBの変更であり、これを低遅延で運ぶのが/data-engineering/cdc-log-based/ です。CDCが「顧客Aの地域が大阪に変わった」という変更イベントを届け、それをSCD Type2の世代更新へ変換します。
実装の中心は MERGE(upsert) です。到着した変更を自然キーで現在行と突き合わせ、追跡対象のType2列が変わっていれば、現在行を閉じて(valid_to と is_current を更新)、新世代を挿入する。この「閉じる+挿す」を1つのMERGE文で原子的に行うのが定石です。
変更イベント(customer_id=A, region=大阪, changed_at=2026-03-15) を適用
MERGE 相当の論理:
1. is_current=true の顧客A の行を引く(現行 = 東京, sk=501)
2. region が変化 → 501 を valid_to=2026-03-15, is_current=false に更新
3. 新世代 (sk=777, region=大阪, valid_from=2026-03-15,
valid_to=9999-12-31, is_current=true) を挿入
変化が無ければ何もしない(no-op)
ここで死活的なのが冪等性です。CDCの配信は少なくとも一度(at-least-once)なので、同じ変更イベントが二度届きうる。二度目に無条件で新世代を挿すと、同じ属性の重複世代ができて履歴が壊れます。防ぐ鍵は「現在行の追跡列と、到着値が実際に異なるときだけ世代を進める」条件をMERGEに入れること。同じ値の再送は差分ゼロで no-op に落ち、履歴は一意に保たれます。これは分析基盤全体を貫く「同じ入力なら結果が一意に収束する」設計の、ディメンション版です。
変更イベントは発生時刻の順に届くとは限りません。3月の変更より先に5月の変更が着くと、素朴に「今の現在行を閉じて新世代を開く」実装では、有効期間が時系列と食い違い、区間が重なったり逆転したりします。対策は、世代を進める前に changed_at を既存世代の期間と突き合わせ、正しい位置へ挿入・分割する(バイテンポラルに近い扱い)こと。到着順ではなくイベント時刻で世代を並べるのが、履歴の正しさの前提です。
レイクハウスのタイムトラベルとの違い
Type2を「テーブルの版管理でよいのでは」と考える人がいます。実際、/data-engineering/lakehouse-iceberg-delta/ のタイムトラベルは、テーブルを過去スナップショット時点で丸ごと読み直せます。しかし両者は目的も粒度も異なります。
タイムトラベルはテーブル全体の物理的な版で、「テーブルが3月10日時点でどうだったか」を復元する運用・監査・巻き戻しの機能です。粒度はスナップショット単位で、個別の顧客の属性遍歴を分析クエリの結合軸として自然に扱う設計ではありません。一方SCD Type2は、ディメンション個々の実体について、属性変更を論理的な行世代として明示的にモデル化し、ファクトからサロゲートキーで特定世代を狙って結合できるようにします。「2025年の売上を当時の地域で」がSQLの通常の結合で表せる。
「SCDの型は?」にはType1=上書き(履歴なし)/Type2=行追加(全世代保持)/Type3=列追加(直前1世代)を、選択基準は「過去ファクトを変更前・変更後どちらで集計するか」で答えます。「Type2の要素は?」はサロゲートキー(行世代の一意識別)と有効期間(半開区間の valid_from/valid_to、is_current)。「ファクトとどう繋ぐ?」は書き込み時にイベント時刻で有効世代を引き、そのサロゲートキーを焼き込む(実行時の範囲結合ではない)。「更新は?」はCDC+MERGEで現在行を閉じ新世代を挿す、値が変わったときだけ進めて冪等に、が定番の勘所です。
まとめ
- ディメンションの属性は時間とともに変わる。SCDは変更履歴の保持方法の設計で、分析の正しさはファクトの数値だけでなくディメンションの時間的正しさにも依存する。
- Type1は上書き(履歴なし・常に最新で集計)、Type2は行追加(全世代保持・過去は当時の属性に固定)、Type3は列追加(直前1世代)。列ごとに型を混在させてよい。
- Type2の骨格はサロゲートキー(各行世代を一意識別する代理キー)と有効期間(半開区間の
valid_from/valid_toとis_current)。半開区間にしないと境界時刻で世代が二重になる。 - ファクトには自然キーではなく、イベント時刻に有効だった世代のサロゲートキーを書き込み時に焼き込む。これで過去集計が当時の属性に固定され、実行時の範囲結合も不要になる。
- 更新はCDCの変更ストリームをMERGEで適用し、現在行を閉じて新世代を挿す。追跡列が実際に変わったときだけ進めて冪等に。遅延到着はイベント時刻で世代を並べて吸収する。
- レイクハウスのタイムトラベルはテーブル全体の物理版、SCDはディメンション個別の論理的な履歴管理で、役割が異なる。
データ工学 Article
緩やかに変化するディメンション(SCD)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
SCD
比較で見る軸
難易度: advanced / カテゴリ: データ工学 / タグ数: 6
導入後に効く点
Type2の核はサロゲートキー(意味を持たない代理キー)と有効期間(valid_from/valid_to)。ファクトには自然キーではなく、事実発生時点で有効だったディメンション行のサロゲートキーを結合時に確定して持たせる。これで過去の集計が当時の属性に固定され、後の変更で塗り替わらない。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データ工学
- タグ数
- 6
判断チェックリスト
- 自社の用途が「SCD / ディメンションモデリング」に近いか確認する。
- 強みである「ディメンションの属性は時間とともに変わる(顧客の地域、商品の分類)。SCDはこの変更履歴をどう保持するかの設計で、上書きするType1、変更のたびに行を足すType2、直前値だけ列で持つType3が基本。分析の正しさはファクトの数値だけでなくディメンションの時間的正しさに依存する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。