MySQL の中間テーブル設計

多対多リレーションを実現するために使うのが中間テーブル(結合テーブル、ピボットテーブルとも呼ばれます)です。リレーショナルデータベースでは多対多を直接表現する方法がないため、2つの1対多リレーションに分解して中間テーブルで橋渡しします。

基本的な構造

ユーザーとロールの多対多関係を例にとります。1人のユーザーは複数のロールを持ち、1つのロールには複数のユーザーが所属できます。

-- 親テーブル
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL
);

CREATE TABLE roles (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(50) NOT NULL UNIQUE
);

-- 中間テーブル
CREATE TABLE user_roles (
  user_id INT NOT NULL,
  role_id INT NOT NULL,
  PRIMARY KEY (user_id, role_id),
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);

中間テーブルの主キーは user_id と role_id の複合主キーにするのが定石です。これにより、同じユーザーとロールの組み合わせが重複して登録されるのを防げます。

サロゲートキーを持たせるか

中間テーブルに独立した id カラム(サロゲートキー)を持たせるかどうかは設計の判断ポイントです。

複合主キーのみ(サロゲートキーなし)

シンプルで無駄がない。純粋に関連だけを管理する中間テーブルにはこれで十分。ORM によっては複合主キーの扱いが面倒になる場合がある。

サロゲートキーあり

ORM との相性がよい。中間テーブル自体を API で操作する場合に識別子として使える。中間テーブルに追加の属性を持たせる場合に管理しやすい。

中間テーブルに属性を持たせる

関連に付随する情報がある場合は、中間テーブルにカラムを追加します。

CREATE TABLE enrollments (
  id INT AUTO_INCREMENT PRIMARY KEY,
  student_id INT NOT NULL,
  course_id INT NOT NULL,
  enrolled_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  grade CHAR(2) DEFAULT NULL,
  UNIQUE KEY uk_student_course (student_id, course_id),
  FOREIGN KEY (student_id) REFERENCES students(id),
  FOREIGN KEY (course_id) REFERENCES courses(id)
);

この例では enrolled_at(登録日時)と grade(成績)が関連に付随する属性です。こうした属性がある場合は、サロゲートキーを主キーにして複合ユニーク制約を別途設定するパターンが管理しやすくなります。

インデックス設計

中間テーブルのクエリパフォーマンスは、インデックスの設計に大きく左右されます。

複合主キーのインデックス

PRIMARY KEY (user_id, role_id) を定義すると、user_id での検索は高速ですが、role_id 単体での検索はインデックスが効きません。

逆方向のインデックス

role_id から検索するクエリ(特定ロールに所属するユーザー一覧)が必要な場合は、INDEX (role_id) を追加します。

CREATE TABLE user_roles (
  user_id INT NOT NULL,
  role_id INT NOT NULL,
  PRIMARY KEY (user_id, role_id),
  INDEX idx_role_id (role_id),  -- 逆引き用
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);

自己結合の中間テーブル

同じテーブル同士の多対多関係も中間テーブルで表現できます。SNS のフォロー関係が典型例です。

CREATE TABLE follows (
  follower_id INT NOT NULL,
  followee_id INT NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (follower_id, followee_id),
  FOREIGN KEY (follower_id) REFERENCES users(id) ON DELETE CASCADE,
  FOREIGN KEY (followee_id) REFERENCES users(id) ON DELETE CASCADE
);

この場合、follower_id と followee_id はどちらも users テーブルを参照しますが、意味が異なります。カラム名で方向性を明確にすることで、クエリの可読性が格段に向上します。中間テーブルは多対多リレーションの要であり、正しく設計することでデータの整合性とクエリの効率を両立できます。