設計者の発言

業務システム開発とデータモデリングに関する語り

「永続null」と「時限null」を区別しよう

 テーブル設計において、null値を取り得る項目の扱いは慎重でなければいけない。nullが設定される可能性のある項目を使ってSelect文の選択条件や並び順の指定をすると、予期しない結果が返る怖れがあるからだ。

 具体的には、ある文字項目がnullを取り得る場合、設計者がnullとブランク(数字項目ではnullとゼロ)とで意味の違いがないとみなしても、SQLはそれらを区別して処理してしまう。非nullとして定義して0やブランクを初期値に設定しておけば大抵はしのげるのだが、日付項目のようにnullか非nullしか取らない項目向けにはnullであった場合のふるまいを仕様上明確にする必要がある。

 nullの扱いに関わるこうしたややこしさを避けたい。そのために提唱される設計原則が「テーブル上の項目はすべて非nullとする」である。レコード上で条件次第でnullが設定されるとしたら、その項目をそのテーブルから除外して、条件に従ってオプショナル対応する(同一主キーの)別テーブル上に置く、と考える。この原則を「null分離主義」と呼んでおくが、この原則に従った場合、注意しないとDB構造が必要以上に複雑になる。どういうことか。

 まず、常に非nullが設定される項目が2個あるとして、条件が2種類、それぞれに従属する項目が2個ずつあるとしよう。なお、ここでいう条件とは「これが成立すればnullでない値が設定される条件」くらいの意味である。これらの条件を含むデータ要件を「null分離主義」にしたがってモデリングすると、図1のようになる。

f:id:dbconcept:20211002155945p:plain

図1.null分離主義によって切り出されたテーブル構造

 変形される前のテーブル「A」におけるd,eは、非null条件1が成立していなければ「永続的にnull」である。いっぽうf、gは、非null条件2が成立していなければ「永続的にnull」である。「永続的にnull」となる項目を、非null条件毎にグルーピングしてテーブルを切り出すべし、と考えるのがnull分離主義である。この例でのA0は(サブタイプから見て)スーパータイプ、A1,A2は(スーパータイプから見て)サブタイプと呼ばれる。

 この設計方針は一見すると何も問題はなさそうだ。じっさい、図2のようなデータ要件向けには、自然かつ効果的なデータモデルが得られる。

f:id:dbconcept:20211002160210p:plain

図2.サブタイプ化が合理的な例

 図2の「品目」は、非null条件同士が「直交」している例で、ある品目が購入品であるかどうかは、それが販売品や製造品であるかどうかに影響を与えない。3つのサブタイプをすべて伴う品目も存在し得るし、サブタイプを一切持たない品目も存在し得る。いっぽう「動物種」は「相互排他」の例だ。すなわち「動物種」から見て「爬虫類属性」、「鳥類属性」、「哺乳類属性」のいずれかひとつだけが対応する(3つの門以外は省略されている)。

 では、これらの非null条件が「直交的」でも「排他的」でもなく、「時限的」すなわち「時系列で順次に成立する関係」であったらどうだろう。こういったnullのあり方を「時限null」と呼んでおくが、時限nullであるような項目については、サブタイプに分離するのではなく、単一テーブル上にまとめたほうがよい。テーブルを無駄に増やさずに済むからだ。

 具体例を見ればその意味がよくわかる。図3の「発注」には時限nullとなる複数の項目が載っている(*1)。発注時、承認時、といったタイミングでそれらには非null値が設定される。nullを取り得るという理由で、それらをいちいち独立したテーブルに載せることを想像してみてほしい。サブタイプが際限なく増えて、同じ主キーを持つ似たようなテーブルがいくつも存在するようになる。私が「おそ松くんDB」と呼ぶアンチパターンにまっしぐらだ。

f:id:dbconcept:20211002160602p:plain

図3.時系列で非nullが設定されてゆく項目の例

 これらの「時限null項目」のまとまりが、「ステータス」の基礎となることに注意してほしい。「どの項目が非nullに設定されているか」によって導出される論理フィールドが、ここでの(レコードの)ステータスである。図3上でのステータスは、「未承認」「承認済/未発注」「発注済/未入荷」、「入荷済/未検収」「検収済」「取消済」のいずれかの値が更新経路にしたがって設定される(*2)。言い換えると、時限null項目には「更新経路に関する仕様」がセットになっているということでもある。

 同じnullでも永続nullと時限nullとが異なる扱いを受ける――この考え方は、DOA(データ指向)とPOA(プロセス指向)の違いを想起させる。じっさいPOAでは、「発注登録する」と「発注承認する」とが異なるイベントであるゆえ、それぞれに異なるテーブルをあてがおうとする。いっぽうDOAにおいて、そういった業務要件は局所的な「ステータスの遷移様式」の問題とみなされる。結果的に、業務フローの変化に伴うDB構造の影響は最小限に抑えられる。

 端的に言えば、POAは「アプリ構成や業務構成にしたがってDB構造が定まる」と考えるが、DOAでは「データ項目間の論理的関係にしたがってDB構造が決まる」と考える。アプリ構成や業務構成は変化しやすいので、そのたびにテーブル構成まで変化してしまうようでは困るのだ。また、POAではDB構造を得るために現状のアプリ構成や業務構成が緻密に調査分析されるので、現状が温存されやすいという弊害もある。いっぽうDOAでは、関数従属性にもとづいて再構成されたDB構造から、抜本的なアプリ構成や業務構成を導けばよいと考える。DXの観点からもDOAの優位性は明らかだ。

 まとめ。項目に設定され得るnullを「時限null」と「永続null」に区別することで、DB構造の無駄な複雑化を避けられる。「null分離主義」は順守されるべき設計方針のひとつではあるが、その基礎となる「テーブルを切り出すための非null条件」から「時限null条件」は除外して考えよう。そのうえで、時限null項目に関してそれらの更新経路とステータスを丁寧に仕様化してほしい。


*1.わかりやすさのために「分納」は考慮していない。

*2.実務的には「承認日」、「発注日」等の日付(日時でも可)項目の値がnullか非nullかで更新の度に自動設定されるステータスを実装することで、ステータス違いのレコードを扱いやすくなる。