ウェブエンジニア問題集
第5章

ER図とテーブル設計 — リレーションの考え方と正規化

5
この章の目次開く

テーブル設計はシステムの土台です。 ここが崩れると、あらゆる機能の実装に歪みが伝播します。 逆に、テーブル設計がしっかりしていれば、APIもバリデーションも自然と整います。

この章では、ER図の読み書きから正規化の考え方、実務での判断基準までを整理します。

学習者学習者

テーブル設計って、とりあえず必要そうなカラムを並べればいいんじゃないの?正規化って何…?

ER図とは何か

ER図(Entity-Relationship Diagram)は、データの構造とリレーション(関連)を可視化する図です。 テーブル設計の前にER図を描くことで、「どのデータがどう関係しているか」をチーム内で合意できます。

設計を考えるエンジニア
ER図はコードを書く前に、データ構造をチームで合意するための設計図

リレーションの種類

リレーション説明
1:1一方のレコードに対して相手も1つusers ↔ user_profiles
1:N一方のレコードに対して相手が複数users → orders
N:N双方とも複数(中間テーブルが必要)products ↔ tags

中間テーブルが必要になるケース

N:N のリレーションは、RDBでは直接表現できません。中間テーブルを置いて、2つの1:Nに分解します。

-- 商品とタグの N:N → 中間テーブル product_tags で解決
CREATE TABLE product_tags (
  product_id INT NOT NULL,
  tag_id     INT NOT NULL,
  PRIMARY KEY (product_id, tag_id),
  FOREIGN KEY (product_id) REFERENCES products(id),
  FOREIGN KEY (tag_id)     REFERENCES tags(id)
);
sql

正規化 — データの重複を排除する

正規化は、データの冗長性を減らし、更新時の矛盾を防ぐための設計手法です。

第1正規形(1NF)— 繰り返しを排除する

1つのカラムに複数の値を入れない。

❌ 非正規形
| order_id | items            |
|----------|-----------------|
| 1        | りんご, みかん    |
 
✅ 第1正規形
| order_id | item    |
|----------|---------|
| 1        | りんご   |
| 1        | みかん   |

第2正規形(2NF)— 部分関数従属を排除する

複合主キーの一部にだけ依存するカラムを別テーブルに分ける。

❌ 部分関数従属あり
| order_id | product_id | product_name | quantity |
|----------|-----------|--------------|----------|
| 1        | 101       | りんご        | 3        |
 
product_name は product_id だけで決まる(order_id は不要)
 
✅ 第2正規形
orders_items: order_id, product_id, quantity
products:     product_id, product_name

第3正規形(3NF)— 推移的関数従属を排除する

主キー以外のカラムに依存するカラムを別テーブルに分ける。

❌ 推移的関数従属あり
| user_id | department_id | department_name |
 
department_name は department_id で決まる(user_id からは間接的)
 
✅ 第3正規形
users:       user_id, department_id
departments: department_id, department_name
先生先生

実務では第3正規形まで整理すれば十分なケースがほとんど。「同じデータが2箇所以上に書いてあるなら、正規化を疑う」と覚えておくと判断しやすいよ。

非正規化する判断基準

正規化は原則だが、パフォーマンスのために意図的に崩す場合があります。

場面非正規化の例理由
参照が圧倒的に多いユーザー名をordersテーブルにも持つJOINを減らして読み取り速度を上げる
集計をリアルタイムで見せたい注文合計金額をordersに持つ毎回SUM()するより速い
履歴として固定したい注文時点の商品価格をorder_itemsに持つ商品の値段が変わっても注文時の金額を保持

論理削除カラム(deleted_at)の設計

レコードを物理的にDELETEせず、deleted_at カラムで「削除済み」を表現する手法。

ALTER TABLE users ADD COLUMN deleted_at TIMESTAMP NULL DEFAULT NULL;
 
-- 論理削除
UPDATE users SET deleted_at = NOW() WHERE id = 1;
 
-- 有効なユーザーだけ取得
SELECT * FROM users WHERE deleted_at IS NULL;
sql
方式メリットデメリット
論理削除復元が容易、監査ログ代わりになる全クエリに WHERE deleted_at IS NULL が必要
物理削除クエリがシンプル復元不可、参照整合性に注意
論理削除を採用するなら、全クエリに条件を付け忘れない仕組み(ORM のデフォルトスコープなど)を入れること。

インデックス設計の基本

インデックスはテーブルの検索を高速化するが、書き込み時にはオーバーヘッドが生じます。

-- WHERE句で頻繁に使うカラムにインデックスを張る
CREATE INDEX idx_users_email ON users(email);
 
-- 複合インデックス(左端のカラムから使われる)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
sql
張るべき場所理由
WHERE句の条件カラム検索を高速化
JOIN条件のカラム結合を高速化
外部キーのカラム参照整合性チェックと結合を高速化
ORDER BY / GROUP BY のカラムソート・集計を高速化

テーブル定義書のテンプレート

実務では、ER図とあわせてテーブル定義書を残します。

テーブル名: orders
説明: 注文情報を管理する
 
| カラム名     | 型            | NULL | デフォルト | 説明               |
|-------------|---------------|------|-----------|--------------------|
| id          | BIGINT        | NO   | AUTO_INC  | 主キー              |
| user_id     | BIGINT        | NO   |           | FK: users.id       |
| total_price | DECIMAL(10,2) | NO   |           | 注文合計金額(税込) |
| status      | VARCHAR(20)   | NO   | 'pending' | pending/paid/shipped/cancelled |
| created_at  | TIMESTAMP     | NO   | NOW()     | 作成日時            |
| updated_at  | TIMESTAMP     | NO   | NOW()     | 更新日時            |
| deleted_at  | TIMESTAMP     | YES  | NULL      | 論理削除日時         |
 
インデックス:
- idx_orders_user_id (user_id)
- idx_orders_status (status)
 
非正規化の意図:
- total_price: 注文時点の合計金額を保持。商品価格変更の影響を受けない

ちゃんと使うためのポイント

  • テーブル設計はシステムの土台。後から変更するコストが最も高い
  • まずER図で全体のリレーションを俯瞰し、その後カラムレベルに落とす
  • 正規化は原則だが、パフォーマンス要件によっては意図的に崩す判断もある
  • N:N は中間テーブルで 1:N × 2 に分解する
  • 論理削除を使うなら、全クエリに条件を忘れない仕組みを入れる
  • インデックスはクエリパターンに応じて効果の高いものに絞る
  • 「なぜこの構造にしたか」をテーブル定義書に残しておく

次の章では、テーブルの上に乗るAPI設計の基本を見ていきます。