「とりあえずLEFT JOIN」から卒業する

SQLのJOINは、書き方自体は簡単なのに選び方を間違えるとデータが重複したり消えたりする厄介な機能です。「迷ったらLEFT JOIN」で乗り切っている人も多いと思いますが、それぞれの違いを理解しておくと、想定外の結果行数に悩まされる時間が減ります。

この記事は、JOIN種類の早見表と結果行数の図解を中心にしたリファレンスです。最後に、複数テーブルJOINの書き方と、既存SQLをJOINタイプごとの図で見返す方法も紹介します。

JOIN種類の早見表

users(3件: Alice, Bob, Carol)と orders(Aliceが2件、Bobが1件、Carolは0件)を例に、返る行を比較します。

JOIN種類返る行AliceとBobの行数Carol(注文なし)の扱い注文だけ存在する行
INNER JOIN両方に一致がある行のみAlice×2, Bob×1含まれない含まれない
LEFT JOIN左テーブル全件+一致する右Alice×2, Bob×1Carolの行が1件(右側はNULL)含まれない
RIGHT JOIN右テーブル全件+一致する左Alice×2, Bob×1含まれない存在すれば含まれる(左側はNULL)
FULL JOIN両テーブルの全件Alice×2, Bob×1Carolの行が1件存在すれば含まれる
CROSS JOIN全組み合わせ(直積)3人 × 3件 = 9行

INNER JOINが唯一「絞り込む」JOINで、他はすべて「片方または両方の全件を保証する」方向に働きます。この違いが行数のブレの正体です。

結果行数はなぜ「増える」のか

もっとも事故りやすいのがこれです。「JOINしたら行数が想定より多い」の9割は、1対多の関係を素直にJOINした結果です。

SELECT users.name, orders.id
FROM users
INNER JOIN orders ON users.id = orders.user_id;

users 3件・orders 3件でも、結果は注文テーブルの行数分(Aliceの注文が2件なら、Aliceの行も2行)になります。集計時にここを見落として COUNT(*) をそのまま取ると、ユーザー数ではなく注文数を数えてしまいます。

-- 誤り:ユーザー数のつもりが「ユーザー×注文」の行数になる
SELECT COUNT(*) FROM users INNER JOIN orders ON users.id = orders.user_id;

-- 正しい:重複を除いたユーザー数
SELECT COUNT(DISTINCT users.id) FROM users INNER JOIN orders ON users.id = orders.user_id;

「JOIN前に何件だったか」を常に意識し、JOIN後の行数がその整数倍になっていない場合は、結合条件を疑うのが実務での鉄則です。

LEFT JOINで一番踏む罠:WHERE句がJOINを台無しにする

「注文がないユーザーも含めたい」からLEFT JOINにしたのに、WHERE で条件を書いた瞬間にINNER JOINと同じ結果に戻ってしまう——これが2番目によくある事故です。

-- 罠:一見LEFT JOINだが、WHEREでorders側を絞るとNULL行が消える
SELECT users.name, orders.status
FROM users
LEFT JOIN orders ON users.id = orders.user_id
WHERE orders.status = 'completed';   -- Carol(NULL)はここで弾かれる

Carolは注文が無いので orders.statusNULL です。WHERE orders.status = 'completed'NULL = 'completed' を評価しますが、**SQLではNULLとの比較は常にNULL(true でも false でもない)**として扱われ、その行は結果から除外されます。結果として、実質的にINNER JOINと同じ挙動になります。

対処法は、条件を ON 句に書くことです。

-- 正しい:ON句に条件を含めればLEFT JOINの意味が保たれる
SELECT users.name, orders.status
FROM users
LEFT JOIN orders ON users.id = orders.user_id AND orders.status = 'completed';
-- Carolの行は残り、orders側は全部NULLになる

**「左側の全件を保証したいなら、右側への絞り込みはON句、左側への絞り込みはWHERE句」**と覚えておくと迷いません。

複数テーブルのJOIN:起点を1つに固定する

3テーブル以上のJOINで迷ったら、「どのテーブルを主語にするか」を先に決めるのがコツです。

SELECT
  users.name,
  orders.id       AS order_id,
  order_items.quantity,
  products.name   AS product_name
FROM users
LEFT JOIN orders      ON users.id = orders.user_id
LEFT JOIN order_items ON orders.id = order_items.order_id
LEFT JOIN products    ON order_items.product_id = products.id
WHERE users.deleted_at IS NULL;

FROM users を起点に、そこから「注文へ」「注文明細へ」「商品へ」とLEFT JOINを連鎖させています。すべて users を主語にしているので、「注文が無いユーザー」「注文はあるが明細行が異常なデータ」もすべて洗い出せます(途中でINNER JOINを混ぜると、そこで絞り込みが発生する点に注意)。

SELF JOINは「別名」で2つのテーブルに見せかける

同じテーブル同士を結合する SELF JOIN は、エイリアス(別名)で2つの異なるテーブルであるかのように扱うのがポイントです。典型例は「社員とその上司」のような自己参照構造です。

SELECT
  emp.name  AS employee_name,
  mgr.name  AS manager_name
FROM employees emp
LEFT JOIN employees mgr ON emp.manager_id = mgr.id;

employeesempmgr という別名を与えることで、SQL上は「2つのテーブルのJOIN」として書けます。上司がいない(manager_id が NULL)社員も含めたいので、ここでもLEFT JOINが自然な選択です。

JOINを組み立てる・整形する

複数テーブルのJOINをゼロから書くのが面倒なら、Visual SQL Builder でテーブルとJOIN条件をフォームから組み立てられます。JOIN種類(JOIN/LEFT JOIN/RIGHT JOIN/FULL JOIN)を切り替えながら生成結果を見比べられるので、この記事の早見表と合わせて挙動を確認するのに向いています。

既存の複雑なJOINクエリを読み解きたいときは、SQLフォーマッター で整形すると、どのJOINがどのテーブルに掛かっているかが見やすくなります。どちらもブラウザ内で完結し、テーブル名やスキーマ情報が外部に送信されることはありません。

まとめ

  • INNER JOINだけが「絞り込む」方向。LEFT/RIGHT/FULLは片方または両方の全件を保証する
  • 1対多のJOINは行が増える。集計前に COUNT(DISTINCT ...) を検討する
  • LEFT JOINで右側を絞りたいなら ON句、左側を絞りたいなら WHERE句。WHERE句に右側の条件を書くとLEFT JOINが実質INNER JOINになる
  • 複数テーブルは主語となるテーブルを固定し、そこからLEFT JOINを連鎖させると崩れにくい
  • SELF JOINはエイリアスで同じテーブルを2つに見せかける

よくある質問

INNER JOINとJOINは違うものですか?

同じです。多くのRDBMSで JOININNER JOIN の省略形として扱われます。ただし、コードレビューでの明確さのために INNER JOIN と明示的に書く運用も一般的です。

RIGHT JOINはあまり使われないと聞きましたが本当ですか?

A RIGHT JOIN BB LEFT JOIN A とテーブルの順序を入れ替えれば同じ結果になるため、「常にLEFT JOINで統一し、RIGHT JOINは使わない」というコーディング規約のチームは多いです。ただし、既存のクエリを読む機会はあるので、意味を理解しておく価値はあります。

FULL JOINが使えないデータベースがあると聞きました

MySQLは長らく FULL JOIN を直接サポートしていません(UNIONLEFT JOINRIGHT JOIN を合成して代用します)。PostgreSQLやSQL Serverでは標準でサポートされています。使用するデータベースの対応状況を事前に確認してください。

JOINの結合条件を貼り付けて確認したいのですが、データは送信されますか?

いいえ。Visual SQL BuilderSQLフォーマッター はどちらもブラウザ内で完結し、入力したテーブル名やクエリがサーバーに送信されることはありません。