JOIN(テーブルの結合)
正規化でバラバラに分けたテーブルを、共通のキーでつなぎ合わせて1つの結果として取り出す操作。INNER / OUTER の違いが肝。
- 1.JOIN は、別々のテーブルを“結合キー”でつなぎ、1つの表として取り出す操作。
- 2.INNER は両方に一致した行だけ、LEFT/RIGHT/FULL OUTER は片側(or 両側)に無くても残す。
- 3.ループ内で1件ずつ引くと N+1 問題。JOIN か IN でまとめて取るのが定石。
なぜ JOIN が要るのか
RDB では、同じ情報を何度も書かない(正規化する)のが基本です。例えば注文ごとにユーザー名を毎回コピーすると、改名時に全行を直す羽目になり、矛盾も生まれます。そこで——
usersテーブル … ユーザーの情報(id,name)ordersテーブル … 注文の情報(id,user_id,amount)
と分け、orders.user_id で「どのユーザーか」を参照(外部キー)だけ持たせます。分けた代償として、「注文と、その注文をしたユーザー名」を一緒に見たいときにつなぎ直す必要が出てきます。それが JOIN です。
-- 注文に、注文者の名前をくっつけて取り出す
SELECT orders.id, users.name, orders.amount
FROM orders
JOIN users ON orders.user_id = users.id;
ON orders.user_id = users.id が結合条件で、ここで使う列を結合キーと呼びます。多くは「片方の主キー(users.id)」と「もう片方の外部キー(orders.user_id)」を突き合わせます。
図的な対応イメージ
JOIN は、2つの表を結合キーで横に並べてくっつけるイメージです。
users orders 結合後(user_id = id で対応)
+----+-------+ +----+---------+--------+ +----------+-------+--------+
| id | name | | id | user_id | amount | | order_id | name | amount |
+----+-------+ +----+---------+--------+ +----------+-------+--------+
| 1 | Alice | ←── | 1 | 1 | 500 | → | 1 | Alice | 500 |
| 2 | Bob | ←── | 2 | 1 | 300 | → | 2 | Alice | 300 |
| 3 | Carol | ←── | 3 | 2 | 800 | → | 3 | Bob | 800 |
+----+-------+ +----+---------+--------+ +----------+-------+--------+
(Carol は注文なし → INNER では消える / LEFT では残る)
ポイントは、1対多だと「多」の側の行数だけ結果が増えること。Alice は注文2件なので、結合後に Alice が2行に展開されます。
4種類の JOIN
「どちらのテーブルに行が無くても残すか」で種類が分かれます。これが JOIN 最大の論点です。
| 種類 | 残す行 | ひとことで |
|---|---|---|
| INNER JOIN | 両方に一致する行だけ | マッチしたペアのみ。注文の無いユーザーは消える |
| LEFT (OUTER) JOIN | 左の全行+右の一致 | 左を主役に。右が無ければ NULL で埋める |
| RIGHT (OUTER) JOIN | 右の全行+左の一致 | LEFT の左右逆。実務では LEFT に書き換えがち |
| FULL OUTER JOIN | 両方の全行 | どちらかに有れば残す。無い側は NULL |
LEFT JOIN を使うと、「注文が1件も無いユーザー」も結果に残り、注文側の列が NULL になります。
-- 全ユーザーを、注文が無い人も含めて一覧(注文数が 0 の人も出る)
SELECT users.name, COUNT(orders.id) AS order_count
FROM users
LEFT JOIN orders ON users.id = orders.user_id
GROUP BY users.name;
-- Carol → order_count = 0 で出る(INNER JOIN だと Carol は消える)
LEFT OUTER JOIN と LEFT JOIN は完全に同じ。OUTER は付けても付けなくても挙動は変わりません。逆に何も付けない JOIN は INNER JOIN と同義です。
つまずきポイント
LEFT JOIN で残したはずの「右が NULL の行」も、WHERE orders.amount > 100 のように右テーブルの列を WHERE で条件にすると消えます(NULL は比較で真にならないため)。OUTER 側の条件は WHERE ではなく ON 句に書くのが鉄則。「LEFT のつもりが結果が減る」典型ワナです。
ON を書き忘れたり、条件が緩すぎると、全行 × 全行の総当たり(クロス結合)になります。1万行 × 1万行 = 1億行が一瞬で生まれ、DB が固まります(意図せず CROSS JOIN を書いた状態)。JOIN したら結合キーが正しく1対1で効いているかを必ず確認しましょう。
N+1 問題
JOIN を避けて「アプリ側のループで1件ずつ引く」と、N+1 問題に陥ります。
1回目: ユーザー一覧を取得 → SELECT * FROM users (1回)
2回目以降: ユーザーごとに注文を取得 → SELECT * FROM orders WHERE... (N回)
合計 = 1 + N 回のクエリ
ユーザーが1,000人なら1,001回もDBに往復します。1回あたりの通信が速くても、回数が積み重なると致命的に遅くなります。ORM(O/Rマッパー)が裏で自動発行しがちで、気づきにくいのが厄介です。
| 観点 | N+1(1件ずつループ) | JOIN / IN でまとめ取り |
|---|---|---|
| クエリ回数 | 1 + N 回 | 1 回(または数回) |
| DBとの往復 | 件数ぶん発生し遅い | 1往復で完結 |
| 典型の原因 | ORM の遅延ロード | JOIN / IN / eager loading |
| 対処 | — | 結合してまとめて取得する |
対処は、JOIN で一度に取るか、WHERE user_id IN (...) でまとめて引くこと。ORM なら「eager loading(先読み)」の機能を使います。
結合キー(特に外部キー側 orders.user_id)にインデックスが無いと、JOIN のたびに全行スキャンになり遅くなります。「結合キーには索引を張る」がパフォーマンスの基本です。
まとめ
- JOIN は、正規化で分けたテーブルを結合キーでつなぎ直す操作。
- INNER=一致のみ、LEFT/RIGHT/FULL OUTER=片側(両側)に無くても残す。違いは「残す行」で覚える。
- OUTER 側の条件は
ONに書く(WHERE に書くと INNER 化)。結合キー漏れはクロス結合の事故。 - ループで1件ずつ引く N+1 問題は、JOIN か
INでまとめて取り、結合キーにインデックスを張って解消する。
データベース Article
JOIN(テーブルの結合)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
SQL
比較で見る軸
難易度: intermediate / カテゴリ: データベース / タグ数: 3
導入後に効く点
INNER は両方に一致した行だけ、LEFT/RIGHT/FULL OUTER は片側(or 両側)に無くても残す。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- intermediate
- カテゴリ
- データベース
- タグ数
- 3
判断チェックリスト
- 自社の用途が「SQL / RDB」に近いか確認する。
- 強みである「JOIN は、別々のテーブルを“結合キー”でつなぎ、1つの表として取り出す操作。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。