ひとことで言うと#
データの構造を論理的に整理し、整合性・パフォーマンス・拡張性のバランスが取れたデータベースを設計する手法。正規化でデータの重複を排除し、インデックスやパーティションでパフォーマンスを確保する。
押さえておきたい用語#
- 正規化(Normalization)
- データの重複を排除し論理的に整理された構造にするプロセスのこと。第1〜第3正規形まで段階的に適用し、更新異常や挿入異常を防ぐ。
- 非正規化(Denormalization)
- パフォーマンス向上のために意図的にデータの重複を許容する設計判断のこと。読み取りの高速化と引き換えに、書き込みの整合性管理が複雑になる。
- インデックス(Index)
- 検索を高速化するための**データ構造(B-tree等)**のこと。WHERE句やJOINで使う列に作成するが、INSERT/UPDATEのコストが増える。
- ER図(Entity-Relationship Diagram)
- エンティティ(実体)とその関係を視覚的に表現した図のこと。DB設計の初期段階でビジネス側と対話しながら描く。
データベース設計の全体像#
こんな悩みに効く#
- 同じデータが複数テーブルに存在し、更新漏れが発生する
- クエリが遅く、テーブルの構造を変えたいが影響範囲がわからない
- 新しい要件が出るたびにテーブルを追加して、スキーマがカオスになっている
基本の使い方#
ビジネスで扱うデータの実体と、その関係性を整理する。
- ER図(Entity-Relationship Diagram)を描く
- エンティティ: ユーザー、注文、商品、カテゴリ etc.
- 関係: 1対1、1対多、多対多を明確にする
ポイント: ビジネス側と対話しながらエンティティを決める。技術者だけで決めない。
第3正規形を基本として、テーブルを分割する。
- 第1正規形: 繰り返しグループを排除(1つのセルに1つの値)
- 第2正規形: 部分関数従属を排除(主キーの一部だけに依存する列を別テーブルに)
- 第3正規形: 推移的関数従属を排除(非キー列同士の依存を排除)
ポイント: まずは第3正規形まで正規化し、必要に応じて非正規化する。順番が大事。
インデックス、パーティション、非正規化でクエリ性能を確保する。
- WHERE句やJOINで使う列にインデックスを作成する
- 大量データのテーブルはパーティションを検討する
- 読み取り性能が重要な場合は、計算済みの値を持つ(非正規化)
ポイント: 非正規化は「意図的な設計判断」として記録する。理由がわからない非正規化は負債になる。
スキーマの変更を安全に行える仕組みを整備する。
- マイグレーションツール(Flyway、Alembic、Rails Migrations等)を導入する
- 破壊的変更は段階的に行う(新カラム追加 → データ移行 → 旧カラム削除)
- スキーマの変更履歴をバージョン管理する
ポイント: 本番DBのスキーマ変更は最もリスクが高い操作。必ずロールバック手順を用意する。
具体例#
エンティティの洗い出し: ユーザー、商品、注文、注文明細、カテゴリ、配送先
正規化の適用: 注文テーブルに商品名と単価を直接持つ(非正規形)→ 注文明細テーブルに分離し、商品テーブルを参照(第3正規形)。ただし、注文時点の価格を保持する必要がある → 注文明細にunit_price_at_orderを持つ(意図的な非正規化)
パフォーマンス設計:
ordersテーブルのuser_idとcreated_atにインデックスordersテーブルを月単位でパーティション(月間200万件の注文データ)productsテーブルにreview_countとaverage_ratingを非正規化して持つ(集計クエリの削減)
結果: 商品一覧の表示速度が320ms→45msに改善。注文時点の価格保持により、価格変更後の請求トラブルがゼロに。
問題: ユーザーの注文履歴ページのレスポンスが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に変わったことを確認。
要件: 1つのDBに500テナントのデータを格納。テナント間のデータ分離が必須。大規模テナントのデータが他テナントのパフォーマンスに影響してはならない。
設計:
- 全テーブルに
tenant_idカラムを追加し、Row Level Security(RLS)で分離 tenant_idをパーティションキーとしてリストパーティションを設定- 大規模テナント上位10社は専用パーティション、残りは共有パーティション
結果: テナント間のデータ漏洩リスクがRLSにより構造的にゼロ。大規模テナントのバッチ処理が他テナントに影響するケースが月5件→0件に。
やりがちな失敗パターン#
- 最初から非正規化する — パフォーマンスが問題になる前から非正規化し、データ不整合を生む。まず正規化してから、ボトルネックが判明した箇所だけ非正規化する
- インデックスを貼りすぎる — すべての列にインデックスを作成し、INSERT/UPDATEが遅くなる。クエリの実行計画(EXPLAIN)を見て、必要なインデックスだけ作る
- NULL許容を安易に使う — すべての列をNULL許容にして、アプリ側でNullチェックが大量に必要になる。業務上必須のデータはNOT NULLにする。デフォルト値も検討する
- テーブル設計をアプリのクラス構造そのままにする — ORM任せでテーブルを作ると、リレーショナルDBの利点が活かせない。ER図ベースでビジネスの実体を反映した設計を行うこと
まとめ#
データベース設計は「正規化で整合性を守り、非正規化でパフォーマンスを得る」バランスの技術。ER図でエンティティと関係を可視化し、第3正規形を基本に設計したうえで、実測に基づいて最適化する。スキーマは必ず変わるものなので、マイグレーションの仕組みも最初から整備しておこう。