🦁

nullが多いテーブルには異なる情報がまとめられている可能性がある

2022/05/24に公開

どうも、株式会社プラハCEOの松原です

先日プラハチャレンジの課題の一環でデータベース設計をレビューしていた際に「テーブルにnullが多い時は少し立ち止まってみると良いかもしれない」という話をしたので、記事にまとめてみました

nullが多いテーブルの例

例えば携帯電話ショップに来店した顧客を管理するためのシステムにこんなテーブルがあったとして:

Table Customer {
  id int [pk]
  name varchar /* 氏名。普通は1つのカラムにまとめないと思うけど簡単のため */
  address varchar /* 住所。普通は1つの(以下略) */
  campaign_id int /* 当店を知ったキッカケ */
  staff_id int /* 来店時に応対した従業員のid */
  sales_note varchar /* 応対時のメモ */
  contract_plan_id int /* 契約した携帯プランのid */
  phone_number varchar /* 契約した携帯番号 */
  first_came_at datetime /* 初回来店日時 */
  registered_at datetime /* 契約日時 */
  cancelled_at datetime /* 解約日時 */
}

id以外のカラムが全てnull許容だったとします(流石にここまで酷いケースは稀だと思いますが...)

「nullableなカラムが多いテーブルは100%怪しい」と言いたい訳では無いのですが、本来は異なるモノなのに同じモノとして一つのテーブルにまとめてしまった時、nullableなカラムが増える傾向がある気がするんですよね。

本来異なるモノを一つのテーブルにまとめるとnullが増えやすい

極端な話「Person」と「House」という概念を「Something」テーブルにまとめたらnullableカラムだらけになりますよね。

Personには「築年数」なんて情報は存在しないし、Houseにも「血液型」なんて情報は存在しないわけですから、SomethingテーブルにPersonに関するデータを入れたら築年数はnullに、Houseに関するデータを入れたら血液型がnullになるイメージです。

上記のテーブルも「Customer」と十把一絡げに顧客をまとめていますが、よくよく見てみると...

  • registered_at は契約したcustomerにしか値が存在しない(契約済Customer)
  • cancelled_at は解約したcustomerにしか値が存在しない(解約済Customer)
  • staff_id は実際に来店したcustomerにしか値が存在しない(来店済Customer)

といった具合に、Customerテーブルには特定の状態のCustomerにしか存在しないカラムが多数存在することが分かります。

では異なる状態のCustomerを1つのテーブルにまとめてしまうと何が起きるのでしょうか?

例えば現在契約中の顧客に利用料を毎月請求するアプリケーションを開発するとしたら、Customerテーブルに対して実行するクエリには必ず「WHERE registered_at IS NOT NULL AND cancelled_at IS NULL」を書かなければいけません。もし一箇所でも書き忘れたら、既に解約している過去の顧客や、一度来店して住所を記入しただけの顧客に利用料を請求してしまいます。

Customerテーブルは、解約済みの顧客も一度来店しただけの見込み顧客も現在契約中の顧客も全て「Customer」にまとめているわけですから、情報を区別する責務がDBからアプリケーションコード側に委ねられるわけです。

(そもそも 「顧客」というリソースと「契約する」「解約する」というイベントが一つのテーブルにまとめられているのも問題 だと思うのですが、これはまた別のトピックに脱線してしまうのでこの記事では言及しません)

情報を区別する責務がアプリケーションに委ねられてしまう弊害

情報を区別する責務がアプリケーションコードに委ねられると様々な不都合が生じます。例えば:

  • 管理者画面など、同じDBに触れる他のアプリケーションにも同じ処理を書かなければいけない
  • 緊急対応時などのミスを誘発しやすい(現在契約中の顧客を取得するにはregistered_atとcancelled_atを組み合わせる必要がある、という暗黙的な知識を持っていないと正しいクエリが書けない)
  • データベースに格納されたレコードを見ただけでは情報が正しく理解できない(アプリケーション側のロジックも見ないとCustomerテーブルに含まれるCustomerが契約中か否か判断できない)

なので、よほど性能上の問題が起きていて、かつその問題がマテリアライズドビューやキャッシュ等で本当に解決できない場合を除いて、本来異なるモノは異なるモノとして別のテーブルに格納した方が良いのではないか?nullが多すぎるテーブルは本来異なるモノがまとめられていることを示唆している可能性があるのではないか?と考えた次第です

じゃあnullは全部ダメなのか

とはいえ「単純に任意項目だからカラムがnull許容になっているだけで、値がnullだったとしてもレコードの意味は変わらず、アプリケーション上の振る舞いも変わらない」というケースもあるので、一概にnullが悪いとは言えないと個人的には考えています。

例えばマッチングアプリにはとんでもない数の任意選択項目がありますが(趣味とか性格特性とか家族構成とか)、それらをいくつ選択したとしてもアプリケーション上の振る舞いは変わりません。自分の情報が異性からの検索結果に表示されづらくなるだけで、いいねも送れるし、メッセージの送受信も可能で、機能的な差はありませんから、先述の不都合は特に生じません。

なのでnullだから無条件に悪いのではなく、特定のカラムがnullか否かによってレコードの意味が変わる(例えばcancelled_atがnot nullだと契約済Customerではなく解約済Customerになる)ようなテーブルは正しく現実の事象をモデル化できていない可能性があるから気をつけた方が良い、ぐらいに個人的に解釈しています。

一方でnullは無条件に悪いとする意見もあったり、nullの許容度に関しては色んな意見があるのでよければ他の意見もどうぞ:

null撲滅委員会
データベース設計におけるNULL
Database Design Follies: NULL vs. NOT NULL
nulls nullified

PrAha

Discussion