INNER JOIN と LEFT JOIN の違い — 結果がどう変わるか実例で確認

INNER JOIN と LEFT JOIN の違いは一言でいうと、相手のいない行を結果に残すかどうかです。INNER JOIN(内部結合)は両方のテーブルに対応する行があるものだけを返し、LEFT JOIN(左外部結合)は左のテーブルの行を全部残して、相手がいない行は右側の列を NULL で埋めます(これを NULL パディングと呼びます)。

この記事では同じデータに両方を実行して結果を見比べたあと、条件を ON に書くか WHERE に書くかで LEFT JOIN の結果が変わる罠と、「LEFT JOIN は遅いのか」という疑問を扱います。実行結果はすべて PostgreSQL 17.10 で実際に確認したもので、ON と WHERE の罠については MySQL 8.4.10 でも同じ結果になることを確認しています。JOIN 全種類のまとめは SQL の JOIN の種類と違いまとめ — INNER/LEFT/RIGHT/FULL/CROSS【2026年更新】 にあります。

サンプルデータ

社員表 emp と部署表 dept を使います。

CREATE TABLE emp (id INT, name VARCHAR(20), dept_id INT);
CREATE TABLE dept (dept_id INT, dept_name VARCHAR(20));
INSERT INTO emp VALUES (1,'佐藤',10),(2,'鈴木',20),(3,'高橋',NULL),(4,'田中',40);
INSERT INTO dept VALUES (10,'営業部'),(20,'開発部'),(30,'総務部');

わざと対応の崩れた行を入れてあります。高橋は dept_id が NULL(部署未定)、田中の dept_id 40 は dept 側に存在しません。この2人の扱いに INNER と LEFT の差が出ます。

同じデータでの結果比較

まず INNER JOIN です。

SELECT * FROM emp INNER JOIN dept USING(dept_id);
dept_ididnamedept_name
101佐藤営業部
202鈴木開発部

2行だけになりました。dept_id が両方のテーブルで一致した行、つまり佐藤と鈴木だけです。高橋(dept_id が NULL)と田中(dept_id 40 は dept に無い)は結合相手がいないので、結果から消えます。

次に、同じクエリの INNER を LEFT に変えます。

SELECT * FROM emp LEFT JOIN dept USING(dept_id);
dept_ididnamedept_name
101佐藤営業部
202鈴木開発部
NULL3高橋NULL
404田中NULL

今度は4行です。左のテーブル emp の行は相手がいなくても全部残り、高橋と田中は dept 側の列(dept_name)が NULL で埋められています。これが LEFT JOIN です。

なお USING(dept_id) で結合しているため、結合キーの dept_id は結果の先頭に1回だけ現れます。ON e.dept_id = d.dept_id と書いた場合は両テーブルの dept_id が2回現れます。INNER JOIN なら2列とも同じ値ですが、LEFT JOIN の相手なし行では右テーブル側の dept_id は NULL になる(田中なら emp 側は 40、dept 側は NULL)ので、どちらの dept_id を SELECT するかに注意してください。

ちなみに LEFT JOIN は LEFT OUTER JOIN の省略形で、OUTER を書いても書かなくても意味は変わりません。キーワードの省略ルールは SQL の JOIN は省略できる — INNER/OUTER キーワードとカンマ結合の整理 にまとめました。

RIGHT JOIN は LEFT JOIN の左右反転で、右のテーブルの行を全部残します。FROM emp RIGHT JOIN deptFROM dept LEFT JOIN emp とテーブル順を入れ替えれば同じ行の組を LEFT で書けるので(SELECT * では列の並び順だけ変わります)、実務では LEFT に統一して書くことが多いです。

罠:条件を ON に書くか WHERE に書くかで結果が変わる

ここが本題です。INNER JOIN では結合条件以外の条件を ON に足しても WHERE に書いても結果は同じですが、LEFT JOIN では ON と WHERE で結果が変わります

「営業部」に関する条件を、まず ON に書いてみます。

SELECT e.name, d.dept_name FROM emp e
  LEFT JOIN dept d ON e.dept_id = d.dept_id AND d.dept_name = '営業部';
namedept_name
佐藤営業部
鈴木NULL
高橋NULL
田中NULL

4行です。ON の条件は「どの行を結合相手として採用するか」の選別に使われるだけなので、左テーブルの行は落ちません。条件に合わなかった鈴木は、相手なし扱いになって NULL が付くだけです。

同じ条件を WHERE に移すとこうなります。

SELECT e.name, d.dept_name FROM emp e
  LEFT JOIN dept d ON e.dept_id = d.dept_id WHERE d.dept_name = '営業部';
namedept_name
佐藤営業部

1行だけになりました。WHERE は結合が終わったあとの結果に対するフィルタです。dept_name が NULL の行は = '営業部' の比較を満たせないので全部落ち、結果として INNER JOIN と同じになります。せっかく LEFT JOIN にした意味が消えるわけです。

PostgreSQL の公式マニュアルにも、ON の条件は結合前に、WHERE の条件は結合後に処理される、内部結合では違いがないが外部結合では大きく違う、と明記されています。

使い分けはこう覚えてください。左テーブルの行を残したまま右テーブル側を絞りたいなら ON に書く。結合後の結果を絞りたいなら WHERE に書く(そしてその場合、実は INNER JOIN で足りていることが多い)。「LEFT JOIN したのに行が減った」というときは、まず WHERE に右テーブルの条件が紛れていないかを疑ってください。

LEFT JOIN は INNER JOIN より遅い?

「LEFT JOIN は遅いから INNER JOIN に書き換えるべき」という話を見かけますが、そのままでは正しくありません。

先ほどの「WHERE に右テーブルの条件を書いた LEFT JOIN」を PostgreSQL 17 で EXPLAIN すると、実行計画は Hash Join になります。Hash Left Join ではありません。WHERE の条件で NULL パディングされた行がどうせ残れないとオプティマイザが判断すると、LEFT JOIN は内部的に INNER JOIN へ変換されるからです。

つまり、同じ結果を返すクエリであれば INNER と LEFT に原理的な速度差はほぼありません。逆に結果が違うクエリ同士(2行返るものと4行返るもの)の速度を比べても意味がありません。書き分けの基準は速度ではなく、前の節までで見てきた「どの行を残したいか」です。

使い分けの指針

  • 結合相手が必ず存在するデータ(外部キー制約などで対応が保証されている親子)を突き合わせるなら INNER JOIN。対応の無い行が混ざりようがないので、意図がそのまま伝わります。
  • 相手がいない行も見たいなら LEFT JOIN。「部署未定の社員も一覧に出す」「注文が1件も無い顧客も集計に含める」といった場面です。相手がいない行だけを探す(dept_name IS NULL で絞る)という欠損チェックにも使えます。

迷ったら「相手のいない行を結果に出したいか」を自問すれば決まります。

さいごに

INNER JOIN は両方に対応する行だけ、LEFT JOIN は左の全行+相手なしは NULL、というのが基本の違いです。それに加えて、LEFT JOIN では条件を ON に置くか WHERE に置くかで結果が変わること、速度は書き分けの基準にならないこと、この2点を押さえておけば実務で困ることはほぼありません。

あわせて読みたい: