SQL JOINの種類と使い分け
JOINは「2つの表をどう重ねるか」の指定です。種類はINNER/LEFT/RIGHT/FULL/CROSSの5つですが、 実務で迷うポイントは「残したい行はどちらの表か」と「条件をONとWHEREのどちらに書くか」の2点に集約されます。
This guide explains SQL JOIN types with concrete result sets, and the classic pitfall where a WHERE condition silently turns a LEFT JOIN into an INNER JOIN.
TL;DR
- INNER: 両方にある行だけ。LEFT: 左は全部残す(右が無ければNULL)
- LEFT JOINの右表条件はONに書く。WHEREに書くとNULL行が消えてINNERと同じ結果になる
- 1対多の結合は行が増える。集約してから結合するか、結合後にGROUP BYする
1. サンプルデータ / Sample data
以下の2表で全JOINの結果を比較します。
| users(左表) | orders(右表) | ||
|---|---|---|---|
| id | name | user_id | item |
| 1 | 佐藤 | 1 | 本 |
| 2 | 鈴木 | 1 | ペン |
| 3 | 高橋 | 4 | 机 |
2. 種類別の結果 / Result by JOIN type
| JOIN | 結果(name, item)/ Result | 使いどころ / When to use |
|---|---|---|
INNER JOIN | 佐藤-本, 佐藤-ペン | 両方に存在するデータだけ欲しい |
LEFT JOIN | 佐藤-本, 佐藤-ペン, 鈴木-NULL, 高橋-NULL | 「注文の無いユーザー」も含めた一覧 |
RIGHT JOIN | 佐藤-本, 佐藤-ペン, NULL-机 | 通常は表を入れ替えてLEFTで書く方が読みやすい |
FULL OUTER JOIN | 上記すべて(鈴木-NULL, NULL-机 を含む) | 両側の突合・差分洗い出し(MySQL非対応) |
CROSS JOIN | 3×3=9行の全組合せ | 組合せ生成。意図せず書くと行数爆発 |
2-1. どの行が残るかを図で比較 / Visual comparison
左の円をusers、右の円をordersと考えると、JOINごとに残る範囲は次のようになります。#が結果に含まれる領域です。OUTER JOINでは、相手が見つからない側の列をNULLで補います。
INNER JOIN LEFT JOIN
users orders users orders
( ### ) (##### ### )
( ##### ) (######### )
( ### ) (####### )
intersection all left + matches
RIGHT JOIN FULL OUTER JOIN
users orders users orders
( ##### ) (#############)
( #######) (#############)
( #####) (###########)
matches + all right all rows on both sides
# = rows kept in the resultINNER keeps only the overlap. LEFT and RIGHT keep every row from their named side, while FULL OUTER keeps both unmatched regions as well as the overlap.
2-2. 同じデータで4種類の結果を確認 / Complete result sets
先ほどの3ユーザー・3注文をu.id = o.user_idで結合します。佐藤には注文が2件、 鈴木と高橋には注文がなく、「机」はuser_id = 4なので対応するユーザーがいません。
INNER JOIN: 一致した行だけ / Matched rows only
SELECT u.id AS user_id, u.name, o.user_id AS order_user_id, o.item
FROM users u
INNER JOIN orders o ON o.user_id = u.id;| user_id | name | order_user_id | item |
|---|---|---|---|
| 1 | 佐藤 | 1 | 本 |
| 1 | 佐藤 | 1 | ペン |
一致しないユーザー2人と、対応ユーザーがいない「机」の注文は結果に現れません。
LEFT JOIN: 左表を全件残す / Keep every left row
SELECT u.id AS user_id, u.name, o.user_id AS order_user_id, o.item
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;| user_id | name | order_user_id | item |
|---|---|---|---|
| 1 | 佐藤 | 1 | 本 |
| 1 | 佐藤 | 1 | ペン |
| 2 | 鈴木 | NULL | NULL |
| 3 | 高橋 | NULL | NULL |
左表の鈴木・高橋は残り、右表の列がNULLになります。右表にしかない「机」の注文は残りません。
RIGHT JOIN: 右表を全件残す / Keep every right row
SELECT u.id AS user_id, u.name, o.user_id AS order_user_id, o.item
FROM users u
RIGHT JOIN orders o ON o.user_id = u.id;| user_id | name | order_user_id | item |
|---|---|---|---|
| 1 | 佐藤 | 1 | 本 |
| 1 | 佐藤 | 1 | ペン |
NULL | NULL | 4 | 机 |
右表の注文はすべて残ります。「机」は対応ユーザーがいないため、左表由来の列がNULLです。
FULL OUTER JOIN: 両表を全件残す / Keep every row from both sides
SELECT u.id AS user_id, u.name, o.user_id AS order_user_id, o.item
FROM users u
FULL OUTER JOIN orders o ON o.user_id = u.id;| user_id | name | order_user_id | item |
|---|---|---|---|
| 1 | 佐藤 | 1 | 本 |
| 1 | 佐藤 | 1 | ペン |
| 2 | 鈴木 | NULL | NULL |
| 3 | 高橋 | NULL | NULL |
NULL | NULL | 4 | 机 |
LEFT JOINの4行と右表にしかない「机」を合わせた5行です。左右どちらかにしかない行も見つけたい 突合作業に向きます。MySQLではLEFT JOINとRIGHT JOINをUNIONして代用します。
3. 最大の罠: ONとWHEREの違い / The ON vs WHERE pitfall
「キャンセル以外の注文を、注文ゼロのユーザーも含めて一覧したい」とき、次のSQLは間違いです。
-- NG: 注文ゼロのユーザー(item IS NULL)がWHEREで消え、INNER JOINと同じ結果に
SELECT u.name, o.item
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.status <> 'cancelled';
-- OK: 右表への条件はONに書く
SELECT u.name, o.item
FROM users u
LEFT JOIN orders o ON o.user_id = u.id AND o.status <> 'cancelled';ONは結合の最中に評価され、マッチしなくても左の行はNULL付きで残ります。 WHEREは結合の後に評価されるため、NULLになった行が条件で落ちます。 「LEFT JOINしたのに件数がINNERと同じ」ときはまずここを疑ってください。
ON filters while matching (unmatched left rows survive with NULLs); WHERE filters after the join (NULL rows get dropped). Put right-table conditions in ON to keep the LEFT JOIN semantics.
4. その他の頻出ポイント / Other common points
- 1対多で行が増える: 集計を伴うときはサブクエリで右表を
GROUP BYしてから結合すると安全 - MySQLにFULL OUTER JOINは無い:
LEFT JOIN ... UNION ... RIGHT JOINで代用 - カンマ結合(
FROM a, b WHERE ...)は旧記法: CROSS JOINと同じ意味。明示的なJOIN構文に書き換える - NULL同士は結合されない:
NULL = NULLは真にならないため、キーにNULLを含む行はINNERでもLEFTでも対象外
JOINが3つ以上連なるSQLは、インデントが揃っているだけで読み違いが激減します。SQL整形ツールに貼り付ければJOIN句単位で整形できます。
5. 実務でのつまずきと確認方法 / Practical pitfalls
LEFT JOIN後のWHEREでNULL行を消してしまう
左表を全件残す目的でも、WHEREで右表の値を必須にするとNULL補完行は条件を満たせません。 結果件数がINNER JOINと同じなら、右表への条件がWHEREに置かれていないか確認します。
-- NG: o.statusがNULLの行も落ちる
SELECT u.name, o.item
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.status = 'paid';
-- OK: paid注文がなくてもユーザーは残る
SELECT u.name, o.item
FROM users u
LEFT JOIN orders o
ON o.user_id = u.id
AND o.status = 'paid';詳細はJOINのONとWHEREの違いも参照してください。
複数マッチを「重複」と誤解する
佐藤に注文が2件あれば、佐藤は2行現れます。これは右表で一致した行をすべて返す正しい動作です。 ユーザーごとに1行だけ必要なら、注文をユーザー単位に集約してから結合します。
SELECT u.id, u.name, o.order_count
FROM users u
LEFT JOIN (
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
) o ON o.user_id = u.id;詳細はSQL JOINで行が増える原因と対策で解説しています。
ON句を書き忘れて全組合せになる
古いカンマ結合で対応条件を書き忘れると、users 3行×orders 3行の9行が返ります。 意図しないCROSS JOINをエラーにせず実行できるDBMSもあるため注意が必要です。
-- NG: 対応条件がなく3 × 3 = 9行
SELECT u.name, o.item
FROM users u, orders o;
-- OK: 対応条件をON句で明示
SELECT u.name, o.item
FROM users u
INNER JOIN orders o ON o.user_id = u.id;レビューではJOINごとにON句があるか確認します。組合せ生成が目的なら、意図を示すためにCROSS JOINと明記すると安全です。
Check right-side filters in WHERE, legitimate one-to-many matches, and missing ON conditions when a JOIN returns a surprising number of rows.
6. English summary
INNER JOIN keeps only matching rows; LEFT JOIN keeps every left row and fills missing right columns with NULL; RIGHT JOIN is the mirror (usually rewritten as LEFT for readability); FULL OUTER JOIN keeps both sides (not supported by MySQL — emulate with UNION); CROSS JOIN produces every combination. The classic bug: putting a right-table condition in WHERE after a LEFT JOIN filters out the NULL rows and silently degrades it to an INNER JOIN — put that condition in ON instead. With one-to-many keys the row count multiplies, so aggregate before joining when you need stable counts.
よくある質問 / FAQ
- LEFT JOINしたら行数が増えたのはなぜ?
- 結合キーが1対多のとき、左の1行が右の複数行とマッチして行が複製されるためです。件数を保ちたい場合は右側を集約(GROUP BY)してから結合します。
- MySQLでFULL OUTER JOINを使うには?
- MySQLはFULL OUTER JOIN非対応のため、LEFT JOINとRIGHT JOINの結果をUNIONで結合して代用します。PostgreSQLやSQL Serverはそのまま使えます。
- What is the difference between ON and WHERE in a LEFT JOIN?
- Conditions in ON are applied while matching rows, so unmatched left rows are kept with NULLs. Conditions in WHERE are applied after the join, so rows with NULLs are filtered out — silently turning a LEFT JOIN into an INNER JOIN.
関連ツール / Related tools
- SQL Formatter - 複数JOINのSQLを読みやすく整形
- Fake Data Generator - JOIN検証用のテストデータ生成
- CSV ⇔ JSON Converter