テーブル初期設計のポイント:事実(Fact)を元に設計する

アプリケーションにおいてのコアはデータです。
データ設計に問題があるアプリケーションは、意図がわかりづらく、実装が複雑になり、その結果、不具合が発生しやすくなります。また、後からデータ設計を変更することはコストが大きく、簡単に変更もできません。

この記事では、データベースの初期設計において考慮しておくべきポイントと実務で使いやすい方法について解説します。

テーブル初期設計の基本ポイント

テーブル設計において、特に「状態を持つデータ」の設計は難しいものです。
ユーザー情報を例にすると、status カラムに active / inactive / banned などの状態を持つという設計はよくある設計でしょう。このように状態を持つ設計は一見シンプルに見えますが、実は後々問題になりがちです。

なぜ状態を持つ設計が問題になりやすいのか

状態を1つのカラムで管理する設計は、一見シンプルに見えますが、実際には以下のような問題が発生しやすくなります。

データの不整合が発生しやすい

状態を持つとデータの不整合が発生しやすくなります。

1
2
3
4
5
6
-- 悪い例:派生情報をカラムに保存
users
- id
- email
- is_active -- 派生情報
- deleted_at -- 事実

この設計では、deleted_at が設定されているのに is_active = true のまま、という矛盾した状態が発生する可能性があります。

データが曖昧になりやすい

初期設計では active / inactive の2つだった状態が、後から以下のような状態が必要になることがあります。

  • banned (アカウント停止)
  • suspended(一時停止)
  • pending_verification(認証待ち)
  • trial_expired(トライアル期限切れ)

このように後から状態を追加していくと、いつの間にか複数の意味を持つステータスが1つのカラムに入っているということが発生します。そうなると、カラムの意味がぼやけてしまい、明確なデータ表現が難しくなってしまいます。

テーブル設計の原則

事実(Fact)だけを保存する

テーブル設計の原則は 業務・ドメインの事実(Fact)だけを保存する です。

  • 事実:登録された、退会した、BANされた、メール認証した
  • 派生:有効ユーザーかどうか、ログイン可能かどうか

派生情報は保存せずに事実から算出します
このように、事実のみを保存するという原則に則ることにより、データの不整合や曖昧さに強い設計にすることができます。

以下は、ユーザーの情報を日付型として扱う例です。1つのカラムでは1つの意味を取り扱うことで、明確なデータ設計にしているのがポイントです。

1
2
3
4
5
6
7
8
9
-- 良い例:日時型を使う(状態が増えたらカラム追加で対応)
users
- id
- email
- deleted_at -- 退会日時
- banned_at -- BAN日時
- suspended_at -- 一時停止日時
- email_verified_at -- メール認証日時
-- 新しい状態が増えても、新しい日時カラムを追加するだけ

状態=「事実の集合」として考える

状態は事実の組み合わせです。例えば、ユーザーが「利用可能」かどうかは、これらの事実の組み合わせから判断します。

  • 退会していない
  • BANされていない
  • メール認証済み
1
2
3
4
-- 利用可能ユーザー
WHERE deleted_at IS NULL
AND banned_at IS NULL
AND email_verified_at IS NOT NULL

ユーザーが「利用可能」というような状態は事実を元に算出する設計が安定します。

状態として持つ場合

一方で 「割り切って状態を持たせた方が“良い設計”になるケース」 は、実務では普通にあります。

UI / 管理画面で即座に使いたいとき

例:決済管理画面

例えば、一覧で「未払い」「支払済み」「返金済み」を表示したい場合は、状態を持ったほうがよいことがあります。
一覧画面で毎回集計の計算をするのはパフォーマンスが重くなりがちです。そういった場合は割り切って状態を持ったほうがトータルとして安定します。

1
2
3
4
invoices
- id
- total_amount
- status -- unpaid / paid

ルール

状態を持つときは以下のルールを守るのがポイントです。

  • 状態が事実から再計算できる

ワークフローが明確に有限なとき

例:審査フロー

審査フローのようにワークフローが明確で有限な場合は状態として持って問題ありません。

1
draftsubmitted reviewing → approved / rejected

なぜOKか

  • 状態遷移が業務ルールとして固定
  • 「今どこか」が重要

一生変わらない意味の状態

状態の意味がが変わらない場合は状態として持って問題ありません。例えば、次のような場合です。

  • is_admin(管理者かどうか)
  • is_trial_user(トライアルユーザーかどうか)
  • is_archived(アーカイブされているかどうか)

補足:YES/NOで判断できるという意味?

多くの場合は YES/NO(boolean)で表現できる ということです。ただし重要なのは、単に2値で表現できることではなく、次の2点です。

  • 定義が将来もブレない:運用や要件が変わっても「何を意味するか」が変わらない
  • 派生状態ではない:他の複数の事実の組み合わせ(計算結果)になっていない

例えば is_admin は「管理者権限が付与されているか」という 明確な事実 に近いのでフラグとして持ちやすいです。一方で is_active は「退会していない」「BANされていない」「認証済み」など条件が増えやすい 派生状態 なので、フラグとして持つと破綻しやすくなります。

まとめ

「状態」を保存するのではなく、まずは「事実(Fact)」を保存するのが、拡張と運用に強い設計です。

  • 原則: DBには 事実(イベント・日時) を保存し、状態は 計算結果 として扱う
  • 避けたいこと: status のような1カラムに、意味の違う状態を押し込める(曖昧さ・不整合の原因)
  • 例外(状態を持ってもよい): UI/管理画面で即時に必要、ワークフローが有限で明確(例:請求ステータス、審査フロー)など、割り切る理由がある
  • 状態を持つときのルール: 再計算できる / 壊れても直せる(できないなら、まずFactの持ち方を見直す)

テーブルは「状態を持つ存在」ではなく、「状態を生む出来事を持つ存在」 として設計することがポイントです。