📚

テーブル設計をするときに考えていること

2022/01/17に公開

はじめに

技術は時代と共に移り変わっていくものですが、時代が変わっても昔の知識がそのまま使えるものもあります。その筆頭がリレーショナルデータベース(RDBMS)でしょう。

この記事では、RDBMSでのテーブル設計において、自分が特に気をつけていることについて書きます。

前提条件、あるいはActive Recordパターンについて

この記事では、Djangoを使ったときの経験を前提としています。そのため、Active Recordパターンを念頭に置いています。Active Recordパターンの是非についてはここでは議論しません[1]

Active Recordパターンはシンプルかつ強力です。ただしその一方で、設計に歪みがあると、その歪みがダイレクトにコードを侵食していく弱点があります。そのため、テーブル設計の是非が、アプリケーションの是非を左右します。

正規化を行う

RDBMSにおいてもっとも大事なのは、テーブル間の関係性の定義です。適切に設計できていないテーブルがあると、同じデータが別のテーブルにバラバラに格納され、不整合が生じていきます。その不整合を解消するために機能を追加すると、保守性が悪くなっていきます。

そのために正規化を行う必要があります。経験上、データベース周りの問題は正規化不足に起因することが多いです。

少し先を見越して設計する

プログラミングではYAGNI原則と呼ばれる、「機能は実際に必要となるまでは追加しないのがよい」考え方があります。ですが、データベースはソースコードよりも変更が難しいため、少し先を見据えて設計する必要があります。

例えば「GitHubでログイン」機能をつけるときに、「Twitterでログイン」「Facebookでログイン」といった機能が必要になりそうだと容易に想像つくでしょう。今は1件だけでいいとしても、近い将来n件になる可能性があるなら、複数入ることを前提に設計した方が良いと考えています。ただし拡張性を考えるが必要なのはそこまでで、列の追加や、コード側の対応まで行う必要はありません。あくまで「近い将来機能が増えても設計をやり直さなくて済む」程度です。

また、想像が必要なのはせいぜい現在の延長線上までで考えられる範囲です。例えばオンラインで完結するサービスを作る際に、住所を複数設定することはまずないでしょう。もし事業が拡大して、リアルの商品を扱うようになったら必要になるかも知れませんが、遠い将来を仮定して設計を行っても冗長になり、かえって分かりにくくなるだけです。

「予備」「汎用」は入れない

列を定義するときに禁忌としているのは、「予備」「汎用」項目です。予備項目は何が入っているか分からず、バリデーションがかけられません。アンチパターンである、EAV(エンティティ・アトリビュート・バリュー)パターンと同様に、保守性の問題があります。

もし「予備項目を入れて欲しい」と言われたら、理由を聞きましょう。利用目的があるはずです。プログラム上は意味がない、単なるテキスト入力かもしれませんが、列名とラベルを適切に設定することに意味があります。

「将来的に必要になるかもしれない」でしたら、容赦なく削ってください。必要になったときに追加するので十分間に合います。もし列の追加が簡単にできない事情があるなら、そちらを改善してください[2]

フラグが出てきたら注意

ダメという訳ではないですが、出てきたら注意が必要なのが、フラグです。例えば is_active というフラグが出てきたときにはステータスとして扱えないか考えます。あるいは「有効期間」のように、タイムスタンプを使うのが適切な場合もあります。

NULLに注意、ただしNOT NULLにこだわりすぎない

RDBMSでは常識ですが、NULLには注意が必要です。NULLが出てそうなときは「デフォルト値を使う」「外部キーの場合、逆のテーブルにつける(参照を逆にする)」など、NULLを排除できないか、可能性を検討します。特に文字列型のときは、空文字があれば十分で、NULLが必要なケースはほとんどありません[3]

ただ、例えば「未選択=0」と「未選択=NULL」では、後者の方がテーブルを直接見たときに分かりやすいという利点があります。また、ORMではNULLを扱いやすくしているため、NULLによるトラブルは起きにくいです[4]。そのためNULLの方が自然な場合はNOT NULLにこだわっていません。

設計見直しから逃げない

ここまでいろいろ書いてきました。ソースコードに比べると慎重に設計し、念入りにレビューしていますが、それでもミスは避けられません。また、プロダクトが成長すると、当初想定していなかった要求が出てきて、現在のテーブル設計では対応できないこともあります。

しかし、そこが踏ん張りどころです。場当たりの対応をするのではなく、大幅な設計変更をして、ときにはデータ移行を行い、設計を綺麗に保つのが大事です。

それが必要な状況は2つあります。1つ目は、欲しいデータを取得するために複雑なSQLを書かないといけなくなったときです。この場合、保守性はもちろん、パフォーマンスに問題が出ることもあります。2つ目は、頭の中の概念モデルと、テーブル設計にズレが出てきたときです。言い換えると、テーブル間の関係が変わったときです。

このときに無理にソースコードだけで対応しようとすると、ソースコードが汚くなり、パフォーマンスが落ち、保守性が下がり、開発速度を落とします。そしてバグを生み、壊れたデータが生まれてしまいます。

この問題を簡単に解決できる方法はありません。大幅な設計変更をして、ときにはデータ移行を行い、設計を綺麗に保ち続けるしかありません。そして、ソースコード以上に、データベースの設計は速く腐っていきます。なぜなら、使われれば使われるほどデータは増えていき、データ移行が難しくなるからです。

おわりに

最初に書きましたが、Active Recordパターンは強力な一方で、設計の歪みがコードを侵食していきやすい弱点があります。

これは弱点ですが、逆に考えると、コードに歪みが出ることで、設計の歪みに気づきやすい構造になっています。その利点を使って、迅速に改善していくこそが大事だと考えています。

脚注
  1. 是非はともかく、Active Recordパターンを使っている現実と向き合う必要があるため ↩︎

  2. Djangoでは列の追加は簡単にできるため、開発プロセスや、デプロイに問題がなければ簡単にできるはずです。 ↩︎

  3. flake8-djangoを使うと、CharField, TextFieldにnull=Trueがついている場合指摘してくれます。 ↩︎

  4. 自分は起きた記憶がない・・・ ↩︎

Discussion