データベース設計におけるNULL
NULL絶対ダメ論や現実的には無理だから上手く付き合っていくしかないんだよ論など見られるが、せっかくCodd博士が上図の分類を提示しておられるので、これを元にもっと詳細化して考えてみよう。
ここで考えた全体のデシジョンツリーは以下のとおり。
https://gyazo.com/75b1163e20283bd414d0dac121cff75a
タプルまるごと
「成立したけどDBMSはそのことを知らない事態」「成立しえない事態」はDB上も考慮されることは無いかと思うが、「不成立の事態」はタプルとしてではなく、「属性の値」として、実際にNULLとして現れることがある。
決裁ワークフローがどこまで進んだかを管理するテーブルを以下のように設計すると、「1次決裁日時」「2次決裁日時」「最終決裁日時」がいずれもNULLだったら、その決裁は「未決裁」状態である。のように属性の値がNULLかどうかで決裁のステータスを判断できる。
https://gyazo.com/f2d2fb410155db7fcaa85405d5a34fac
だが、これはそもそも決裁イベントをそれぞれエンティティとして抽出し、そのイベントレコードの有無、すなわち上図の「不成立の事態」で現在の決裁ステータスがわかるようにした方が決裁イベントのAtomic性が担保される。(→ イミュータブルデータモデル参照) https://gyazo.com/596d5c953ae8467e1a0dce4fee84ab12
よく悪い例として挙げられる「削除フラグ」「削除日時」もこの一種と言える。
https://gyazo.com/100db7c7b1db3b9522197b256c49cec1/thumb/160#.png
これもリソースにイベントが紛れ込んだ形なので、「退職」イベントが必要ならエンティティを作り、リソースエンティティである「社員」からは日時属性を排除する。その上で、退職社員もデータとして残しておく、消せない理由があるのであれば、属性として区分を追加して、これを利用して現役社員だけクエリするなどすれば良い。
https://gyazo.com/5562cb1ff81de904d0b229c4b67606ac/thumb/500#.png
いずれにせよ、「タプルまるごと」タイプのNULLの発生は設計によって排除できる。(し、そうすべきであると考える)
属性の値
次に属性の値としてのNULLだが、主には未知(Unknown)と適用不能(Not Applicable)に区別される。
Unknown
まずUnknownの分解から。
本当に不明 (欠損値)
任意
エンティティ生成時点ではまだ値が入らない/後で入る。
本当に不明
この場合、欠損値として何か定めた値を登録するしか無いので、利用のRDBMS的に何か問題がなければ「NULL」を使うし、そうでなければ、欠損値を表す値を決めておいてそれを使う。
NULLの代わりに「−1」と「99999」とかを入れるんじゃねぇ、という批判はたいていこの欠損値のケースに向けられているように思う。NULL撲滅委員会では、NULLじゃなく「0」を使うで大した問題になったことはない、と言及されている。ルールが周知されている現場ではそれで良いのかもしれない。 任意
「任意」の扱いには悩みどころがある。
まず「任意」にも以下の2通りが考えられる。
現実世界では属性として持っているが、システムには登録しない
現実世界でもその属性が存在しない
だが、これによってエンティティの設計が変わるわけでは無いので、この議論はおいておく。
悩みどころは「属性」なのか「リレーションシップ」なのか? で、これは境界は曖昧だ。(詳細は「Data and Reality」を参照)
「リレーションシップ」で2つのエンティティが非依存の場合は「NULL」を使わずに交差テーブルを作った方が、運用上取り回しが良い。
https://gyazo.com/c07cc252827196b7dc1c04b969c2f78e
そうではなく「属性」ととらえる場合はNULLを使うことになるだろう。
https://gyazo.com/c548418fa009e60ef92adf8671a80226/thumb/300#.png
上記例で、性別について考えると、これは「リレーションシップ」と捉えることもできるだろう。「属性」なのか「リレーションシップ」なのか、これが境界が曖昧なところ。
https://gyazo.com/6cd98c9259614959f63c8b3908a36174
エンティティ生成時点ではまだ値が入らない/後で入る
リソースエンティティに「不明」でもないし「任意」でもないが、とあるイベントが発生すれば必ず値は入るけれども、それまでは入らない類のライフサイクルが異なる属性を持つことがある。
ライフサイクルごとにサブタイプとして識別できる場合
https://gyazo.com/108c9ac74f93716c4d6f00b10fcaee2d/thumb/300#.png
例えば起業支援の法人向けサービスで、法人番号が取得される前から使えなきゃいけないとする。
そうすると、取得できた時点で「国税局法人番号」を更新したい。それまではNULLだ。このケースでは、サービス的には法人番号取得前後でサービスとしてできることが変わるはずなので、サブタイプとして識別することができるだろう。そうなると「Not Applicable」のケースとして考えられる。(サブタイプをどうテーブル実装するかによって分岐する)
https://gyazo.com/91bee6b45e5158e47dbed486945f2d9d
非依存リレーションシップとして関連で結べる場合
サイト内での金銭のやり取りをする機能を使う場合だけは、銀行口座の登録が必要なサービスを考える。会員登録時には銀行口座番号は不要だ。
https://gyazo.com/797d8e64c4e775f70ac88252f0a46b8d/thumb/150#.png
この場合、銀行口座は会員のライフサイクルとは別で存在して、銀行口座の登録はそのリレーションシップが作られることと考えうる。
https://gyazo.com/ed7cc8032aa94ac4eefe5d7379099084/thumb/400#.png
そうすると、会員が銀行口座登録しているかどうかは、「銀行口座」とのリレーションシップで表現されるため、あらかじめどこかの属性にNULLを入れておく必要はない。
どちらでも無い場合
うまい例が思いつかなかったが、どちらにも分類できないケースもあるだろう。だが業務上重要な意味をもつ属性にはならないことが想像できる。したがって「NULLを入れておいて、あるイベントによって値が入る」という設計を許容しても良いだろう。
Not Applicable
論理モデル上はサブタイプとしてNULLが無いようにモデル書けるが、テーブルに実装する際にシングルテーブル継承を選択すると発生してしまうのが、Not ApplicableとしてのNULLである。
代表的なのは、範囲の上限・下限である。例えば消費税率は、過去の3%や8%の時代のデータには「適用開始日」「適用終了日」が属性として存在するが、現在の10%については、(今のところ)「適用終了日」は定まっていないので、属性としては存在しないと考えうる。ので、論理モデルは以下のようになる。
https://gyazo.com/fcaa9370d984eb3fd797822121c0dee8
普通は、これを物理設計するときに、このまんま3テーブルで実装することはせず、1つのテーブルで実装するだろう。これはシングルテーブル継承パターンと呼ばれる。 https://gyazo.com/7d46d4411a556cbfa9abbe479495d820/thumb/300#.png
こうして複数のサブタイプを1つのテーブルで実装すると、あるサブタイプには存在しない属性に相当するカラムは、Not Applicableになり、NULLを格納せざるを得ないだろう。
なお、ここで挙げた例の「範囲」の属性に関しては、それに相当する型を持つRDBMSの製品もあるので、使えるなら積極的に使うとよい。(ORMが対応していないかもしれないが…)
サブタイプはシングルテーブル継承以外にも、具象テーブル継承、クラステーブル継承の実装がありうる。その場合は、Not Applicableは発生しない。