[[ディメンションテーブル]] の [[Default Row]] は 1 行とは限らず、[[ファクトテーブル]] の外部キーが NULL になる原因ごとに行を分けることができる。何行用意するかは、分析時に原因をどこまで区別したいかで決める。 行数を誤ってもエラーは出ない。しかしまとめすぎると、品質問題による欠損と業務上の対象外が「Unknown」に混ざって区別できない。逆に細かく分けすぎると、判定条件のないまま恣意的に振り分けてしまう。問題は両方向に起きるため、どこまで区別するかの見極めが要る。 > [!info] > 原典は『The Kimball Group Reader』(2nd Edition)。Kimball Group の [Selecting Default Values for Nulls](https://www.kimballgroup.com/2010/10/design-tip-128-selecting-default-values-for-nulls/) テクニックとしても解説されている。 > > 『[[The Data Warehouse Toolkit]]』(3rd Edition) に、ここまで細かい分類論はない。原因を木構造に組み、畳む深さで行数を決める整理は本ノート独自の再構成である。 > [!tip] > 複数行に分けるのは応用テクニックであり、無理に適用する必要はない。NULL の原因を分けて分析するニーズがなければ、単一の Unknown 行だけで十分である。 > > 迷ったら 1 行にまとめておけばよい。raw データを DWH 内に保持する ELT なら、判別ロジックを後から足して全履歴を再構築し、行を増やして分け直せる。 ## NULL の原因は 4 つに分類できる 原典は、単一の Unknown 行に全条件を押し込まず、「なぜ値がないのか」で行を分ける方式を示している。キー値 (0, -1, -2…) の選び方は自由だが、割り当ては全ディメンションテーブルで統一する。 | 条件 | 訳語 | 例 | | --- | --- | --- | | Not Applicable | 該当なし (そのファクト行に無関係) | プロモーション対象外の売上 | | Not Happened Yet | 未確定 (後で埋まる予定) | 未出荷注文の出荷日 | | Missing | 未設定 (ソースが値を提供しなかった) | 会員カード未提示の売上 | | Bad Value | 不正値 (壊れた・照合不能な値) | ディメンションに存在しない商品コード | この 4 分類は対等な並びではない。「値は存在するはずか」「まだ発生していないだけか」と質問を重ねて振り分けていくと、次のような木構造に整理できる。 ``` Unknown ├─ Not Applicable └─ 値は存在するはず ├─ Not Happened Yet └─ 発生済みだが取得できない ├─ Missing └─ Bad Value ``` 根の「Unknown (不明)」は 5 つ目の分類ではなく、4 分類をまとめて代表する上位概念である。原因を区別しないと決めた範囲を 1 行でまとめて扱うとき、その行のラベルとして現れる。同じテーブルに「Unknown」と「Missing」が並んだら、親と子を同列に置いた設計ミスである。 ## 行数は区別する深さで決まる Default Row の行数は、この木をどの深さまで区別するかで決まる。根だけなら 1 行、第 1 分岐までなら 2 行、第 2 分岐までなら 3 行、葉まで区別すれば 4 行になる。以下の木で「(Unknown)」と添えたノードは、そこから下の原因をまとめて「Unknown」行で扱うことを示す。 ### 1 行 ``` Unknown ``` ``` | user_key (SK) | user_id (NK) | user_name | | ------------- | ------------ | --------- | | 0 | NULL | (Unknown) | ``` ### 2 行 ``` Unknown ├─ Not Applicable └─ 値は存在するはず (Unknown) ``` ``` | user_key (SK) | user_id (NK) | user_name | | ------------- | ------------ | ---------------- | | 0 | NULL | (Unknown) | | -1 | NULL | (Not Applicable) | ``` ### 3 行 ``` Unknown ├─ Not Applicable └─ 値は存在するはず ├─ Not Happened Yet └─ 発生済みだが取得できない (Unknown) ``` ``` | user_key (SK) | user_id (NK) | user_name | | ------------- | ------------ | ------------------ | | 0 | NULL | (Unknown) | | -1 | NULL | (Not Applicable) | | -2 | NULL | (Not Happened Yet) | ``` ### 4 行 ``` Unknown ├─ Not Applicable └─ 値は存在するはず ├─ Not Happened Yet └─ 発生済みだが取得できない ├─ Missing └─ Bad Value ``` ``` | user_key (SK) | user_id (NK) | user_name | | ------------- | ------------ | ------------------ | | -1 | NULL | (Not Applicable) | | -2 | NULL | (Not Happened Yet) | | -3 | NULL | (Missing) | | -4 | NULL | (Bad Value) | ``` 「Unknown」行がまとめて扱う原因の範囲は深さで変わる。どの原因を含めているかを規約に書き残しておくと、後から確認できる。 キー値は、0 を「Unknown」行の専用とし、4 つの分類には -1 から -4 を固定で割り当てる。4 行方式に 0 が現れないのは、すべて区別して「Unknown」行が不要になるためである。この割り当てなら、どのディメンションテーブルでも同じキー値が同じ意味を指す。 ## 区別する深さは 2 軸で判断する どの深さまで区別するかは、分岐ごとに 2 つの問いを立てて決める。 ### 判別可能性: ETL が分岐を判定できるか NULL の値そのものからは、4 分類のどれに当たるか分からない。判別材料は、同じ行の別の列やディメンションとの照合結果にある。不正値だけは、ディメンションへの lookup の失敗として機械的に判定できる。 該当なしは行の種別 (イベント種別・商品タイプ) から、未確定はプロセスの状態列から導く。未設定は、どの条件にも当てはまらなかった残りとして消去法で決める。判定条件をソースのデータから書けない分岐は、行を分けずにまとめる。 `fct_order` の `user_key` を例にすると、判定材料は次のようになる。 | ソースの注文行の状態 | 判定材料 | 分類 | | --- | --- | --- | | `user_id` がディメンションに存在しない | lookup の失敗 | Bad Value | | `user_id` が空で、注文種別がゲスト購入 | 行の種別 | Not Applicable | | `user_id` が空で、会員連携ステータスが連携待ち | 状態列 | Not Happened Yet | | `user_id` が空で、上のいずれにも当てはまらない | 消去法 | Missing | ### 分析価値: 利用者が使い分けるか 判別できても、レポートで使い分ける価値がなければ行を分けない。逆に価値が高いのに判別できない分岐 (ゲスト注文を分けて見たいのに種別列がソースに無い、など) は、諦めて 1 行にまとめる前に、判別できるようソースの改修や ETL への投資を検討する。 2 軸で絞っていくと、実務ではほとんどのディメンションが 1 行方式に落ち着く。不正値の検知は dbt のデータテストが担うようになり、行として持つ動機が薄いためである。 例外は、日付ディメンションへの外部キーである。出荷前の注文の出荷日キーを Unknown 行にまとめると、未出荷分を数える集計やリードタイム計算が壊れる。未確定の日付キーだけは、遠い未来の日付を表す行に分ける実利が残る。 ## 業務上の意味を持つ NULL は専用の行で区別する ここまでの 4 分類は、どの業務システムでも通用する基本形である。ただし例外的に、さらに区別を深めたい場合がある。例えば返品時にソースが値を消す業務では、その NULL を「該当なし」行にまとめると、もともとの対象外と返品による対象外を区別できなくなる。 このような状態は、木の上では該当なしの下に葉を足す拡張として表せる。 ``` Unknown ├─ Not Applicable │ ├─ 返品につき対象外: Returned │ └─ 上記以外: Not Applicable └─ 値は存在するはず ... ``` ``` | user_key (SK) | user_id (NK) | user_name | | ------------- | ------------ | ---------------- | | 0 | NULL | (Unknown) | | -1 | NULL | (Not Applicable) | | -5 | NULL | (Returned) | ``` キー値は 4 分類の葉の続き (-5 以降) を割り当てる。行を分けるかどうかの判断には、4 分類と同じ 2 軸がそのまま使える。 ## 関連ノート - **[前提]** [[スタースキーマの NULL 可否は列の役割で決める]]: 外部キーの NULL を禁止し、Default Row で受けるという前提