こんにちは、かつコーチです。
実務のデータベースは、2つのテーブルだけで完結することはほとんどありません。
「会員」「注文」「商品」のように、3つ、4つとテーブルが連鎖している構成が一般的です。
今回は、複数テーブルを連結して結合する方法と、多対多の関係を扱う際に欠かせない中間テーブルの考え方を解説します。
複数テーブルJOINとは?
JOINは何個でもつなげられる
JOINは、2つのテーブルを結合する構文ですが、その結果に対してさらにJOINを重ねることができます。
例えば、次のようなテーブル構成を考えます。
-- users(会員): id, name
-- orders(注文): id, user_id, product_id
-- products(商品): id, name, price
「どの会員が」「どの商品を」注文したかを一覧にするには、ordersを軸にusersとproductsの両方を結合する必要があります。
多対多の関係と中間テーブル
「1人の会員が複数の商品タグに興味を持つ」「1つの商品が複数のタグを持つ」のような多対多(お互いに複数対複数で対応する関係)を表現する場合、直接結びつけるカラムを用意できません。
そこで登場するのが中間テーブル(2つのテーブルの対応関係だけを記録する橋渡し役のテーブル)です。ordersテーブルも、実は「会員」と「商品」をつなぐ中間テーブル的な役割を果たしています。
基本の書き方
3テーブルを結合する
ordersを軸に、usersとproductsを結合します。
SELECT u.name, p.name AS product_name, p.price
FROM orders o
INNER JOIN users u ON o.user_id = u.id
INNER JOIN products p ON o.product_id = p.id;
JOINを上から順に追加していくイメージで書けます。ordersとusersを結合した「一時的な結果」に対して、さらにproductsを結合する、という流れで処理されます。
中間テーブルを使った多対多の結合例
「商品」と「タグ」が多対多の関係にある場合を考えます。
-- products(商品): id, name
-- tags(タグ): id, name
-- product_tags(中間テーブル): product_id, tag_id
「“セール”タグが付いている商品を一覧にする」には、product_tagsを経由してproductsとtagsを結合します。
SELECT p.name AS product_name, t.name AS tag_name
FROM products p
INNER JOIN product_tags pt ON p.id = pt.product_id
INNER JOIN tags t ON pt.tag_id = t.id
WHERE t.name = 'セール';
中間テーブルを1つ挟むことで、商品とタグをそれぞれ何個でも自由に対応させられる柔軟な構造になります。
つまずきやすい設定・注意点
JOINの順番でパフォーマンスが変わることがある
JOINを重ねる際、どのテーブルから結合するかによって、途中経過の行数が大きく変わることがあります。
MySQLのオプティマイザ(実行計画を自動で最適化する仕組み)がある程度調整してくれますが、非常に大きなテーブル同士を結合する際は、絞り込み条件(WHERE)を先に効かせられる構成を意識すると安定します。
私が担当したレポート機能で、orders(数百万行)・users・products・campaignsの4テーブルを結合するクエリが、想定より大幅に遅くなったことがありました。
調査の結果、キャンペーン期間の絞り込み条件がWHERE句の末尾に書かれていて、絞り込み前の巨大な中間結果に対してJOINが行われていたことが原因でした。
❌ Before(絞り込み条件が効くタイミングが遅い)
SELECT o.id, u.name, p.name, c.name AS campaign_name
FROM orders o
INNER JOIN users u ON o.user_id = u.id
INNER JOIN products p ON o.product_id = p.id
INNER JOIN campaigns c ON o.campaign_id = c.id
WHERE c.start_date >= '2026-08-01';
✅ After(対象を絞ってから他テーブルと結合する)
SELECT o.id, u.name, p.name, c.name AS campaign_name
FROM campaigns c
INNER JOIN orders o ON o.campaign_id = c.id
INNER JOIN users u ON o.user_id = u.id
INNER JOIN products p ON o.product_id = p.id
WHERE c.start_date >= '2026-08-01';
実際にはオプティマイザが賢く処理してくれるケースも多いですが、行数が多いテーブルほど、絞り込みが効くタイミングを意識した書き方や、EXPLAINでの実行計画確認が有効です。
EXPLAINの読み方は、このシリーズの後続記事で扱います。
結合条件を1つでも間違えると結果全体が崩れる
複数JOINでは、途中の1つの結合条件が間違っているだけで、最終結果全体がおかしくなります。
デバッグの際は、JOINを1つずつ追加しながら、都度件数や内容を確認していく方法がおすすめです。
-- ステップ1: orders単体
SELECT COUNT(*) FROM orders;
-- ステップ2: usersを結合した時点
SELECT COUNT(*) FROM orders o INNER JOIN users u ON o.user_id = u.id;
-- ステップ3: productsも結合した時点
SELECT COUNT(*) FROM orders o
INNER JOIN users u ON o.user_id = u.id
INNER JOIN products p ON o.product_id = p.id;
各ステップで件数が想定通りに変化しているかを確認すれば、どのJOINで問題が起きたか特定しやすくなります。
応用・一歩先の使い方
LEFT JOINとINNER JOINを使い分けて混在させる
複数テーブルを結合する際、すべてをINNER JOINにする必要はありません。
「注文情報は必須(INNER JOIN)」「レビューは任意(LEFT JOIN)」のように、テーブルごとの必須・任意の関係に応じて使い分けます。
SELECT u.name, p.name AS product_name, r.rating
FROM orders o
INNER JOIN users u ON o.user_id = u.id
INNER JOIN products p ON o.product_id = p.id
LEFT JOIN reviews r ON o.id = r.order_id;
「注文と会員・商品の情報は必ずセットで存在する」「レビューはまだ書かれていないかもしれない」という業務ルールを、そのままSQLの構文で表現できます。
中間テーブルに追加情報を持たせる
中間テーブルは、単なる対応関係だけでなく「いつ付与されたか」「誰が設定したか」といった付加情報を持たせることもできます。
-- product_tags: product_id, tag_id, created_at, created_by
こうした設計は、テーブル設計・正規化の記事で詳しく扱う内容ですが、JOINの基礎を理解していると、こうした設計判断も理解しやすくなります。
まとめ
この記事のポイント
JOINは2つ以上のテーブルに対して連続して重ねられる- 多対多の関係は、中間テーブルを経由してJOINすることで表現する
- JOINを重ねる際は、どのテーブルから絞り込むかでパフォーマンスが変わることがある
- デバッグ時はJOINを1つずつ追加し、都度件数を確認すると原因を特定しやすい
次に読むべき記事
- SQL INNER JOINで複数テーブルを結合する
- SQL LEFT JOIN RIGHT JOIN 違い
- SQL 正規化の考え方(第1〜第3正規形)
タグ: SQL, 中級者向け, JOIN