データベース設計

英語名 Database Design
読み方 データベース デザイン
難易度
所要時間 設計に1〜5日
提唱者 E.F.コッド(関係モデル)
目次

ひとことで言うと
#

データの構造を論理的に整理し、整合性・パフォーマンス・拡張性のバランスが取れたデータベースを設計する手法。正規化でデータの重複を排除し、インデックスやパーティションでパフォーマンスを確保する。

押さえておきたい用語
#

押さえておきたい用語
正規化(Normalization)
データの重複を排除し論理的に整理された構造にするプロセスのこと。第1〜第3正規形まで段階的に適用し、更新異常や挿入異常を防ぐ。
非正規化(Denormalization)
パフォーマンス向上のために意図的にデータの重複を許容する設計判断のこと。読み取りの高速化と引き換えに、書き込みの整合性管理が複雑になる。
インデックス(Index)
検索を高速化するための**データ構造(B-tree等)**のこと。WHERE句やJOINで使う列に作成するが、INSERT/UPDATEのコストが増える。
ER図(Entity-Relationship Diagram)
エンティティ(実体)とその関係を視覚的に表現した図のこと。DB設計の初期段階でビジネス側と対話しながら描く。

データベース設計の全体像
#

データベース設計:正規化→物理設計→進化のサイクル
論理設計ER図を描くエンティティと関係を洗い出す正規化する第3正規形を基本とする1対1 / 1対多 / 多対多物理設計インデックス設計WHERE/JOINで使う列に作成パーティション大量データの分割管理意図的な非正規化進化・運用マイグレーションスキーマ変更をコード管理実行計画分析EXPLAINで遅いクエリを特定ロールバック手順整備設計のバランス原則正規化で整合性を守り、非正規化でパフォーマンスを得るまず正規化してから、ボトルネックが判明した箇所だけ非正規化する非正規化は「意図的な設計判断」として必ず理由を記録する
データベース設計の進め方
1
ER図作成
ビジネス側と対話しながらエンティティと関係を洗い出す
2
正規化
第3正規形を基本にデータの重複を排除
3
物理設計
インデックス・パーティション・非正規化を適用
進化に備える
マイグレーションとロールバック手順を整備

こんな悩みに効く
#

  • 同じデータが複数テーブルに存在し、更新漏れが発生する
  • クエリが遅く、テーブルの構造を変えたいが影響範囲がわからない
  • 新しい要件が出るたびにテーブルを追加して、スキーマがカオスになっている

基本の使い方
#

ステップ1: エンティティと関係を洗い出す

ビジネスで扱うデータの実体と、その関係性を整理する

  • ER図(Entity-Relationship Diagram)を描く
  • エンティティ: ユーザー、注文、商品、カテゴリ etc.
  • 関係: 1対1、1対多、多対多を明確にする

ポイント: ビジネス側と対話しながらエンティティを決める。技術者だけで決めない。

ステップ2: 正規化でデータの重複を排除する

第3正規形を基本として、テーブルを分割する

  • 第1正規形: 繰り返しグループを排除(1つのセルに1つの値)
  • 第2正規形: 部分関数従属を排除(主キーの一部だけに依存する列を別テーブルに)
  • 第3正規形: 推移的関数従属を排除(非キー列同士の依存を排除)

ポイント: まずは第3正規形まで正規化し、必要に応じて非正規化する。順番が大事。

ステップ3: パフォーマンスを考慮した物理設計をする

インデックス、パーティション、非正規化でクエリ性能を確保する

  • WHERE句やJOINで使う列にインデックスを作成する
  • 大量データのテーブルはパーティションを検討する
  • 読み取り性能が重要な場合は、計算済みの値を持つ(非正規化)

ポイント: 非正規化は「意図的な設計判断」として記録する。理由がわからない非正規化は負債になる。

ステップ4: マイグレーションと進化に備える

スキーマの変更を安全に行える仕組みを整備する

  • マイグレーションツール(Flyway、Alembic、Rails Migrations等)を導入する
  • 破壊的変更は段階的に行う(新カラム追加 → データ移行 → 旧カラム削除)
  • スキーマの変更履歴をバージョン管理する

ポイント: 本番DBのスキーマ変更は最もリスクが高い操作。必ずロールバック手順を用意する。

具体例
#

例1:ECサイトの注文データベースを正規化と非正規化のバランスで設計する

エンティティの洗い出し: ユーザー、商品、注文、注文明細、カテゴリ、配送先

正規化の適用: 注文テーブルに商品名と単価を直接持つ(非正規形)→ 注文明細テーブルに分離し、商品テーブルを参照(第3正規形)。ただし、注文時点の価格を保持する必要がある → 注文明細にunit_price_at_orderを持つ(意図的な非正規化)

パフォーマンス設計:

  • ordersテーブルのuser_idcreated_atにインデックス
  • ordersテーブルを月単位でパーティション(月間200万件の注文データ)
  • productsテーブルにreview_countaverage_ratingを非正規化して持つ(集計クエリの削減)

結果: 商品一覧の表示速度が320ms→45msに改善。注文時点の価格保持により、価格変更後の請求トラブルがゼロに。

例2:EXPLAINで遅いクエリの原因を特定し、インデックスで解決する

問題: ユーザーの注文履歴ページのレスポンスが3.2秒。SQLはSELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT 20

EXPLAIN分析: Full Table Scan(全件走査)が発生。ordersテーブルには500万行。user_idにインデックスなし。

対策: CREATE INDEX idx_orders_user_created ON orders(user_id, created_at DESC) を追加。複合インデックスによりWHEREとORDER BYの両方をカバー。

結果: レスポンスタイムが3.2秒→12msに改善(99.6%短縮)。EXPLAINでIndex Scanに変わったことを確認。

例3:SaaSの多テナントDBをテナント分離とパーティションで設計する

要件: 1つのDBに500テナントのデータを格納。テナント間のデータ分離が必須。大規模テナントのデータが他テナントのパフォーマンスに影響してはならない。

設計:

  • 全テーブルにtenant_idカラムを追加し、Row Level Security(RLS)で分離
  • tenant_idをパーティションキーとしてリストパーティションを設定
  • 大規模テナント上位10社は専用パーティション、残りは共有パーティション

結果: テナント間のデータ漏洩リスクがRLSにより構造的にゼロ。大規模テナントのバッチ処理が他テナントに影響するケースが月5件→0件に。

やりがちな失敗パターン
#

  1. 最初から非正規化する — パフォーマンスが問題になる前から非正規化し、データ不整合を生む。まず正規化してから、ボトルネックが判明した箇所だけ非正規化する
  2. インデックスを貼りすぎる — すべての列にインデックスを作成し、INSERT/UPDATEが遅くなる。クエリの実行計画(EXPLAIN)を見て、必要なインデックスだけ作る
  3. NULL許容を安易に使う — すべての列をNULL許容にして、アプリ側でNullチェックが大量に必要になる。業務上必須のデータはNOT NULLにする。デフォルト値も検討する
  4. テーブル設計をアプリのクラス構造そのままにする — ORM任せでテーブルを作ると、リレーショナルDBの利点が活かせない。ER図ベースでビジネスの実体を反映した設計を行うこと

まとめ
#

データベース設計は「正規化で整合性を守り、非正規化でパフォーマンスを得る」バランスの技術。ER図でエンティティと関係を可視化し、第3正規形を基本に設計したうえで、実測に基づいて最適化する。スキーマは必ず変わるものなので、マイグレーションの仕組みも最初から整備しておこう。