🧚 お知らせ!Beekeeper Studio は高速でモダンなオープンソースのデータベース GUI です ダウンロード
2026年5月2日 著者: Matthew Rathbone

多対多リレーションシップは、どちら側の行も相手側の複数の行と対応できるように 2 つのテーブルを結び付けます。たとえば、1 人の学生は複数の講座を受講し、1 つの講座には複数の学生がいます。これは、リンクごとに 1 行を保存する中間テーブルでモデル化します。サンプルは PostgreSQL、MySQL、SQLite、SQL Server で動作します。

多対多リレーションシップのモデル化方法

3 つ目のテーブルとして中間テーブル(ブリッジテーブル、関連テーブル、結合テーブルとも呼ばれます)を作成し、両側への外部キーを持たせます。各行が 1 つのリンクを表します。つまり「この学生がこの講座を受講している」ということです。

多対多データベースリレーションシップの図

CREATE TABLE students (
    id INTEGER PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE courses (
    id INTEGER PRIMARY KEY,
    course_name VARCHAR(100) NOT NULL
);

-- The junction table: one row per student per course
CREATE TABLE enrollments (
    student_id INTEGER NOT NULL,
    course_id INTEGER NOT NULL,
    PRIMARY KEY (student_id, course_id),
    FOREIGN KEY (student_id) REFERENCES students(id),
    FOREIGN KEY (course_id) REFERENCES courses(id)
);

一方の側からもう一方へたどるには、中間テーブルを経由して結合します。

-- Input: students
-- id | name
-- ---+------
-- 1  | Ana
-- 2  | Ben
-- 3  | Chloe

-- Input: courses
-- id | course_name
-- ---+------------
-- 1  | Biology
-- 2  | Chemistry
-- 3  | Physics

-- Input: enrollments
-- student_id | course_id
-- -----------+----------
-- 1          | 1
-- 1          | 2
-- 2          | 1
-- 3          | 1

SELECT s.name, c.course_name
FROM students s
JOIN enrollments e ON s.id = e.student_id
JOIN courses c ON e.course_id = c.id
ORDER BY s.name, c.course_name;

-- Output:
-- name  | course_name
-- ------+------------
-- Ana   | Biology
-- Ana   | Chemistry
-- Ben   | Biology
-- Chloe | Biology

Ana は 2 つの講座を受講し、Biology には 3 人の学生がいるため、リレーションシップは両方向で「多」になります。

押さえておきたいルール

  • ペアを主キーにします。 PRIMARY KEY (student_id, course_id) により、同じリンクが二重に保存されるのを防げます。
-- Input: the enrollments table above

INSERT INTO enrollments VALUES (1, 1);
-- ERROR: duplicate key value violates unique constraint "enrollments_pkey"
  • 2 番目の列にインデックスを作成します。 主キーのインデックスは student_id から始まるため、学生の講座はすでに高速に検索できます。講座の学生を高速に検索するには、course_id から始まるインデックスが必要です(MySQL では外部キーと一緒に自動で作成されます)。
CREATE INDEX idx_enrollments_course_id ON enrollments(course_id);
  • リストではなく、リンクごとに 1 行を保存します。 '1,2' を格納した course_ids 列は、結合もインデックス作成も外部キーによるチェックもできません。
  • SQLite は有効にするまで外部キーを無視します。 接続ごとに PRAGMA foreign_keys = ON; を実行してください。そうしないと、SQLite は存在しない学生の受講登録も受け付けてしまいます。

別の書き方

フレームワークによっては、中間テーブルも含めてすべてのテーブルに独自の id 列を持たせます。id だけでは重複リンクを防げないため、ペアに UNIQUE 制約を追加すれば問題ありません。データベース間で異なるのは id 列だけです。

-- PostgreSQL
CREATE TABLE enrollments (
    id SERIAL PRIMARY KEY,
    student_id INTEGER NOT NULL,
    course_id INTEGER NOT NULL,
    UNIQUE (student_id, course_id),
    FOREIGN KEY (student_id) REFERENCES students(id),
    FOREIGN KEY (course_id) REFERENCES courses(id)
);

-- MySQL:      id INT AUTO_INCREMENT PRIMARY KEY
-- SQLite:     id INTEGER PRIMARY KEY
-- SQL Server: id INT IDENTITY PRIMARY KEY

外部キーはインラインでも宣言できます: student_id INTEGER NOT NULL REFERENCES students(id)。MySQL 8.0 はこの書き方を受け付けますが黙って無視するため、MySQL では別途 FOREIGN KEY 句を記述してください。

多対多リレーションシップはいつ使うべきか?

両側の行がそれぞれ複数の相手を持てる場合に使います。学生と講座、投稿とタグ、ユーザーとロール、商品とカテゴリなどです。一方の側の各行が常に相手を 1 つしか持たないなら、一対多リレーションシップで十分です。中間テーブルを用意すれば、次のような課題を解決できます。

行のリンク先を調べる

学生はどの講座を受講しているか?投稿にはどのタグが付いているか?行から中間テーブルへ、さらに反対側へと結合します。同じ中間テーブルで両方向の問いに答えられます。

最も受講者の多い講座、最もよく使われるタグ、最も多くのクラブに所属するメンバー。一方の側でグループ化し、中間テーブル内の行数を数えます。リンクのない行を消さずに 0 として数えたい場合は LEFT JOIN を使います。

成績は学生単独や講座単独ではなく「ある講座における学生」に属し、印税の配分は特定の書籍における著者に属します。こうした列を中間テーブルに置けば、他の列と同じようにクエリできます。

フォロワー、友人、メンターは人と人をつなぎます。中間テーブルの外部キーはどちらも同じテーブルを指すため、そのテーブルを毎回異なるエイリアスで 2 回結合します。

多対多リレーションシップの実例

リスト内のすべてのタグを持つ投稿を探す

タグフィルターは通常「これらのタグをすべて持つ投稿」を意味します。いずれかのタグに一致させてから、すべてに一致した投稿だけを残します。

-- Input: posts
-- id | title
-- ---+----------------
-- 1  | Indexing basics
-- 2  | Weekend hiking
-- 3  | Joins explained

-- Input: tags
-- id | name
-- ---+---------
-- 1  | sql
-- 2  | outdoors
-- 3  | beginner

-- Input: post_tags
-- post_id | tag_id
-- --------+-------
-- 1       | 1
-- 1       | 3
-- 2       | 2
-- 2       | 3
-- 3       | 1

SELECT p.title
FROM posts p
JOIN post_tags pt ON p.id = pt.post_id
JOIN tags t ON pt.tag_id = t.id
WHERE t.name IN ('sql', 'beginner')
GROUP BY p.id, p.title
HAVING COUNT(*) = 2;

-- Output:
-- title
-- ---------------
-- Indexing basics

「Joins explained」は 2 つのタグのうち 1 つしか持たないため、HAVING で除外されます。2 はリスト内のタグ数に合わせて変更してください。

各講座の受講登録数を数えます。LEFT JOIN により、誰も登録していない Physics も残ります。

-- Input: courses
-- id | course_name
-- ---+------------
-- 1  | Biology
-- 2  | Chemistry
-- 3  | Physics

-- Input: enrollments
-- student_id | course_id
-- -----------+----------
-- 1          | 1
-- 1          | 2
-- 2          | 1
-- 3          | 1

SELECT c.course_name, COUNT(e.student_id) AS student_count
FROM courses c
LEFT JOIN enrollments e ON c.id = e.course_id
GROUP BY c.id, c.course_name
ORDER BY student_count DESC;

-- Output:
-- course_name | student_count
-- ------------+--------------
-- Biology     | 3
-- Chemistry   | 1
-- Physics     | 0

誰がどこでいつ働いたかを記録する

各リンクに開始日と終了日を持たせると、中間テーブルが履歴を保持します。一度離れた部署に戻ることもあるため、ペアは一意ではなくなり、開始日も主キーに加えます。現在の部署は終了日のない行です。

CREATE TABLE employee_departments (
    employee_id INTEGER NOT NULL,
    department_id INTEGER NOT NULL,
    start_date DATE NOT NULL,
    end_date DATE,  -- NULL while it's their current department
    PRIMARY KEY (employee_id, department_id, start_date),
    FOREIGN KEY (employee_id) REFERENCES employees(id),
    FOREIGN KEY (department_id) REFERENCES departments(id)
);

-- Input: employees
-- id | name
-- ---+------
-- 1  | Grace
-- 2  | Omar
-- 3  | Wei

-- Input: departments
-- id | name
-- ---+------------
-- 1  | Sales
-- 2  | Support
-- 3  | Engineering

-- Input: employee_departments
-- employee_id | department_id | start_date | end_date
-- ------------+---------------+------------+-----------
-- 1           | 1             | 2022-03-01 | 2024-06-30
-- 1           | 3             | 2024-07-01 | NULL
-- 2           | 2             | 2023-01-09 | NULL
-- 3           | 1             | 2021-05-17 | NULL

SELECT e.name, d.name AS department, ed.start_date
FROM employees e
JOIN employee_departments ed ON e.id = ed.employee_id
JOIN departments d ON ed.department_id = d.id
WHERE ed.end_date IS NULL
ORDER BY e.name;

-- Output:
-- name  | department  | start_date
-- ------+-------------+-----------
-- Grace | Engineering | 2024-07-01
-- Omar  | Support     | 2023-01-09
-- Wei   | Sales       | 2021-05-17

現在の部署が 2 つにならないようにするには、部分ユニークインデックスを追加します: CREATE UNIQUE INDEX idx_current_dept ON employee_departments(employee_id) WHERE end_date IS NULL;。MySQL には部分インデックスがないため、MySQL ではこれは使えません。

相互フォローしているユーザーを探す

follows テーブルはユーザー同士を結び付けます。CHECK 制約で自分自身のフォローを防ぎ、テーブルを自己結合することで相互フォローのペアを見つけます。

CREATE TABLE follows (
    follower_id INTEGER NOT NULL,
    followed_id INTEGER NOT NULL,
    PRIMARY KEY (follower_id, followed_id),
    FOREIGN KEY (follower_id) REFERENCES users(id),
    FOREIGN KEY (followed_id) REFERENCES users(id),
    CONSTRAINT no_self_follow CHECK (follower_id <> followed_id)
);

-- INSERT INTO follows VALUES (2, 2);
-- ERROR: new row for relation "follows" violates check constraint "no_self_follow"

-- Input: users
-- id | name
-- ---+-----
-- 1  | Ava
-- 2  | Leo
-- 3  | Mia
-- 4  | Noah

-- Input: follows
-- follower_id | followed_id
-- ------------+------------
-- 1           | 2
-- 2           | 1
-- 1           | 3
-- 3           | 4
-- 4           | 3

SELECT u1.name AS user_a, u2.name AS user_b
FROM follows f
JOIN follows back ON back.follower_id = f.followed_id
                 AND back.followed_id = f.follower_id
JOIN users u1 ON u1.id = f.follower_id
JOIN users u2 ON u2.id = f.followed_id
WHERE f.follower_id < f.followed_id
ORDER BY user_a;

-- Output:
-- user_a | user_b
-- -------+-------
-- Ava    | Leo
-- Mia    | Noah

Ava は Mia をフォローしていますが、Mia はフォローし返していないため、このペアは表示されません。f.follower_id < f.followed_id の条件により、相互ペアを 2 回ではなく 1 回だけ表示します。

多対多リレーションシップについてさらに学ぶ

このブログの記事:

公式ドキュメント:

Beekeeper Studioは無料のオープンソースのデータベースGUIです

これまで使った中で最高のSQLクエリ&エディタツールです。データベースの管理に必要なものがすべて揃っています。 - ⭐⭐⭐⭐⭐ Mit

Beekeeper Studioは高速で直感的、そして使いやすいツールです。多数のデータベースに対応し、Windows、Mac、Linuxで快適に動作します。

BeekeeperのLinux版は100%フル機能。手抜きも機能の妥協も一切ありません。

Beekeeper Studio 利用者の声

★★★★★
「Beekeeper Studio は以前の SQL ワークフローを完全に置き換えました。高速で直感的で、データベース作業がまた楽しくなりました。」
— Alex K.、データベース開発者
★★★★★
「多くのデータベース GUI を試してきましたが、Beekeeper は機能とシンプルさのバランスが完璧です。とにかくちゃんと動きます。」
— Sarah M.、フルスタックエンジニア