Article

Conversation

テーブルに状態を持たせてはいけない
リレーショナルデータベースの設計でよく見かけるものに、テーブルへ「状態」を持たせる設計があります。退会フラグ、公開ステータス、論理削除フラグなどがその代表です。これらは一見すると素直で分かりやすい設計ですが、運用が進むにつれて不整合や複雑なクエリの温床になりがちです。 今回は、なぜテーブルに状態を持たせるべきでないのか、そして代わりに何を保存すべきなのかを整理します。

状態を持たせるとは何か

たとえば会員を管理するテーブルに status というカラムを設け、active、suspended、withdrawn といった値を入れて会員の状態を表すとします。退会した会員は withdrawn に更新し、検索時には WHERE status = 'active' で絞り込む、という具合です。
この設計の問題は、状態を上書き(UPDATE)してしまうと、それまでの事実が失われる点にあります。いつ退会したのか、過去に一度停止されてから復帰したのか、といった経緯は status を更新した瞬間に消えてしまいます。トラブル対応や監査のときに必要になるのは、こうした失われた経緯であることが少なくありません。

状態は「事実」ではなく「加工された情報」である

ここで重要なのは、状態が事実そのものではないという点です。「会員が退会した」という出来事は事実ですが、「現在この会員は退会済みである」という状態は、その事実から導き出された情報にすぎません。
この区別は新しいものではありません。Clojure の作者である Rich Hickey 氏は講演「The Value of Values」で、事実とは起きた出来事であり、知識はそうした事実を比較・結合して導かれるものだと述べています。そして事実は過去を変えられないのと同じく更新できない、とも指摘しています。 同じ考え方は、年齢を保存せず生年月日を保存するという古典的な設計原則にも表れています。年齢は生年月日と現在日付から導出できる情報であり、両方を保存すると導出可能な値を冗長に持つことになって(教科書的には第三正規形違反として説明されます)、値が食い違う余地が生まれます。
退会フラグも構造はまったく同じです。保存すべきは「いつ退会という出来事が起きたか」であって、「今は退会済みである」という導出結果ではありません。

ドメインの言葉に「論理削除」はない

状態管理が話題になるとき、しばしば論理削除フラグが引き合いに出されます。これについて、ソフトウェアエンジニアの和田卓人氏は、論理削除という概念は現実の業務には存在せず、顧客が「論理削除」という言葉を使うことはない、と指摘しています。現場で使われるのは「退会」「公開停止」「キャンセル」といった具体的な言葉です。
つまり、業務で起きているのは状態の削除ではなく、新しい出来事の発生です。退会は会員データの削除ではなく、「退会した」という事実の追加として捉えるほうが自然です。発生した事実に忠実にモデリングすれば、情報を消したり書き換えたりする必要はなくなります。

履歴と集合のミスマッチ

理論的な観点からも、状態を 1 つのカラムで持つことには無理があります。リレーショナルモデルにおけるリレーションは集合であり、要素間に順序関係はありません。一方で状態の変化には「古い」「新しい」という順序が必然的に伴います。順序を持つ履歴を、順序のない集合の中の 1 カラムへ無理に押し込もうとするところに、本質的なひずみが生じます。

どう設計すればよいか

解決の方向は、状態を更新するのではなく、状態を変える出来事をそのつどレコードとして追加することです。会員の例であれば、会員そのものを表すテーブルと、会員に起きた出来事を記録するテーブルを分けます。
sql
CREATE TABLE member (
  id INT PRIMARY KEY,
  name VARCHAR(255) NOT NULL
);

CREATE TABLE membership_event (
  id INT PRIMARY KEY,
  member_id INT NOT NULL,
  event_type VARCHAR(50) NOT NULL,
  occurred_at DATETIME NOT NULL,
  FOREIGN KEY (member_id) REFERENCES member(id)
);
入会、停止、復帰、退会はすべて membership_event への INSERT として記録します。現在の状態が知りたいときは、その会員の最新の出来事を取得すれば求められます。この考え方は、更新を避けて出来事だけを記録するイミュータブルデータモデルや、状態を出来事の積み重ねとして表現するイベントソーシングと共通しています。
この設計には次のような利点があります。
  • 過去の経緯が失われないため、監査やトラブル対応に対応できる
  • 新しい状態の種類が増えても event_type の値を増やすだけで済み、テーブル構造を変えずに拡張できる
  • 上書きが発生しないため、データの食い違いが起こらない
一方で、最新状態を求めるたびに並べ替えや絞り込みが必要になり、データ量が増えるとパフォーマンスが課題になることもあります。そのため、すべてのテーブルを機械的にこの形にする必要はありません。状態の遷移や履歴が業務上意味を持つかどうかを見極め、意味を持つ場合にこの設計を選ぶのが現実的です。

まとめ

テーブルに状態を持たせる設計は手軽ですが、事実を上書きして経緯を失い、状態が増えるたびに複雑さが積み重なります。保存すべきは加工された状態ではなく、それを生み出した出来事という事実です。状態を更新するのではなく出来事を追加するという発想に切り替えることで、変更に強く、事実に忠実なテーブル設計に近づけることができます。
Want to publish your own Article?
Upgrade to Premium