ER 図

N : M は中間テーブルに分解する

素の N:M 線は設計未完了のサイン。必ず中間テーブル(交差表)に分解する。

N : M は中間テーブルに分解する diagram

素の N : M 線は「設計の未完了マーク」

「学生 ── 講義」を N : M のまま線 1 本で結んだ ER 図は、概念を捉えている途中の下書きでしかありません。リレーショナルモデルには 2 つのテーブルを直接 N : M で結ぶ仕組みが無く、必ず第 3 のテーブル(中間テーブル/交差表/junction table)を介します。

  • 片方に FK 列を持たせると多値になり、第 1 正規形(1NF)を満たせない
  • 「誰が何を取っているか」を問い合わせる SQL が書けない(CSV 文字列で持つ、JSON 配列で持つ等は別の闇)
  • 後から「履修日」や「成績」といった属性が出てくると、置き場所が無く詰む

ER 図のレビューで N : M を見かけたら、そこで必ず中間テーブルに分解するまでがレビューの仕事です。

実例 — タグ付け・ロール・履修登録

「N:M を中間テーブルに分解する」はセオリーですが、中間テーブルに属性を乗せるかどうかで設計が二段階に分かれます。実例でパターンを覚えると判断が速くなります。

業務例中間テーブル属性の有無扱い
記事にタグを付けるarticle_tags (article_id, tag_id)なし(純粋な N:M)複合 PK の中間テーブルで十分。関連実体に昇格させない
ユーザーにロールを割り当てuser_roles (user_id, role_id, assigned_at, assigned_by)あり(誰がいつ割当)関連実体に昇格。role_assignments に改名して履歴管理
学生が講義を履修enrollments (student_id, course_id, enrolled_at, grade)あり(履修日・成績)最初から enrollments として命名。代理 id + UNIQUE(student_id, course_id) が定石
商品を注文に入れるorder_items (order_id, product_id, quantity, unit_price)あり(数量・単価)事実上の「明細」。代理 id を必ず持つ(他テーブルから参照される)

記事-タグのような「純粋な紐付け」は複合 PK のままで、ユーザー-ロールや注文-商品のように紐付いた瞬間の情報(いつ・誰が・いくらで)を残したいものは関連実体に昇格 ― という判断軸です。迷ったら「レコード単位で参照・更新・削除したいか」を問うと決まります。

中間テーブルの基本形

中間テーブルは 両側のエンティティの PK を FK として 2 列並べる だけが基本形です。

複合 PK (student_id, course_id) にすることで、「同じ学生が同じ講義に 2 回登録される」を DB レベルで防げます。片方だけ UNIQUE を付けたり、何も付けないで運用すると、必ず重複レコードが紛れ込みます。

sql
CREATE TABLE student_courses (
  student_id BIGINT NOT NULL REFERENCES students(id),
  course_id  BIGINT NOT NULL REFERENCES courses(id),
  PRIMARY KEY (student_id, course_id)
);

関連実体への昇格 — 属性が乗るケース

中間テーブルに属性(列)が乗り始めたら、それはもはや単なる交差表ではなく、独立した「関連実体(association entity)」に昇格しています。

  • enrolled_at(いつ履修登録したか)
  • grade(成績)
  • assigned_by(誰が割り当てたか)

こうなると、テーブル名も enrollments(履修)のように業務用語に沿った名前に改名するのが自然です。ER 図の書き方も「学生 ── 講義」の線ではなく「学生 ─ 履修 ─ 講義」の 2 段関係に変わります。

※ 属性が乗る = 関連そのものが業務上の「モノ」として扱われている、というシグナル。改名・整理のタイミングを逃すと、後から SQL 側で「最新の履修」「成績付き履修」などを書くときにコードが散らかります。

複合 PK vs 代理 id + UNIQUE

中間テーブルの PK には 2 つの流派があります。

設計PK向く場面弱点
複合 PK(古典派)PRIMARY KEY (a_id, b_id)純粋な交差表。属性が乗らない・乗ってもせいぜい 1〜2 列MySQL InnoDB では全セカンダリ索引に PK 2 列が埋め込まれて太る
代理 id + UNIQUE(現代派)PRIMARY KEY (id) + UNIQUE (a_id, b_id)関連実体に昇格したテーブル。他テーブルから参照される/ORM で扱うPK が業務的意味を持たない列になるため、列が 1 本増える

迷ったら「他のテーブルから FK で参照されるか」で決めます。されるなら代理 id、されないなら複合 PK で十分。詳しい根拠は 代理キー vs 自然キー を参照。

両方向インデックスも忘れずに

中間テーブルは両方向から検索されるのが普通です。PK (student_id, course_id) だけだと「この講義を取っている学生一覧」のクエリで左端欠けとなり、索引が使われません。

CREATE INDEX ON student_courses (course_id, student_id) のような逆方向の索引を最初から貼るのが定石です。詳細は 外部キーとリレーション列の索引設計 を参照してください。