DevToolBox

SQL JOINの種類と使い分け

最終更新日: 2026-06-12公開日: 2026-06-12執筆: DevToolBox編集部

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.

SQL Formatter

複数のJOINを含むクエリを自動整形。INNER/LEFTの指定と結合条件が句ごとに並び、構造を追いやすくなります。

今すぐ試す →

TL;DR

1. サンプルデータ / Sample data

以下の2表で全JOINの結果を比較します。

users(左表)orders(右表)
idnameuser_iditem
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 JOIN3×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 result

INNER 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_idnameorder_user_iditem
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_idnameorder_user_iditem
1佐藤1
1佐藤1ペン
2鈴木NULLNULL
3高橋NULLNULL

左表の鈴木・高橋は残り、右表の列が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_idnameorder_user_iditem
1佐藤1
1佐藤1ペン
NULLNULL4

右表の注文はすべて残ります。「机」は対応ユーザーがいないため、左表由来の列が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_idnameorder_user_iditem
1佐藤1
1佐藤1ペン
2鈴木NULLNULL
3高橋NULLNULL
NULLNULL4

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

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

関連ガイド / Related guides