[[ディメンションテーブル]] の [[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 で受けるという前提