SQL:外部結合(LEFT JOIN)
1学習の目的
- LEFT JOIN で「対応するデータがない行」も残せるようになる。前章で消えてしまった高橋建設を、売上0円として表示できるようになる。
- COUNT(*) と COUNT(列名) の違いが結果を左右することを理解する。0件のはずが1件と表示されるという、実務で頻発するバグを回避できるようになる。
2基礎解説
前章の INNER JOIN では、注文が0件の高橋建設が結果から消えました。「取引のない顧客も一覧に出したい」——そのための結合が LEFT JOIN です。
| INNER JOIN | LEFT JOIN | |
|---|---|---|
| 残る行 | 両方にある行だけ | 左側は全部残る |
| 対応がないとき | その行ごと消える | 右側の列がNULLになる |
| 結果の行数 | 減ることがある | 左側の行数以上 |
| 使う場面 | 取引実績の分析 | 全件リスト・欠損の発見 |
- LEFT JOIN は「左側のテーブルを全部残す」。FROM customers c LEFT JOIN orders o なら、customers が左で全4社が残る。
- 対応する行がないと右側の列はすべて NULL になる。高橋建設の行は、注文IDも数量もNULLで埋まる。
- ⚠ COUNT(*) を使うとNULLの行も1件と数えてしまう。注文0件の顧客が「1件」と表示される。必ず COUNT(o.id) のように列名を指定する。
- 合計も同様に COALESCE(SUM(…), 0) で包む。SUM の対象が無いとNULLになるので、0円と表示したいなら明示的に置き換える(SQL06)。
- WHERE 右テーブルの列 IS NULL と書くと、「対応がなかった行だけ」が取り出せる。未取引の顧客や、注文のない商品を探す定番の書き方。
現場使用例:全顧客の売上一覧(0円も含む)、購入履歴のない会員の抽出、担当者未設定の案件、在庫はあるが売れていない商品。「無いこと」を見つけられるのがLEFT JOINの価値。
-- 全顧客を残す(高橋建設も出てくる) SELECT c.name, o.id FROM customers c LEFT JOIN orders o ON c.id = o.customer_id; -- → 高橋建設の o.id は NULL -- ❌ COUNT(*) だと注文0件が「1件」になってしまう SELECT c.name, COUNT(*) AS 件数 FROM customers c LEFT JOIN orders o ON c.id = o.customer_id GROUP BY c.name; -- → 高橋建設 1(誤り) -- ✅ 列名を指定すればNULLは数えられない SELECT c.name, COUNT(o.id) AS 件数 FROM customers c LEFT JOIN orders o ON c.id = o.customer_id GROUP BY c.name; -- → 高橋建設 0(正しい) -- 売上0円も表示する SELECT c.name, COALESCE(SUM(p.price * o.qty), 0) AS 売上 FROM customers c LEFT JOIN orders o ON c.id = o.customer_id LEFT JOIN products p ON o.product_id = p.id GROUP BY c.name; -- 「対応がなかった行だけ」を探す SELECT c.name FROM customers c LEFT JOIN orders o ON c.id = o.customer_id WHERE o.id IS NULL; -- → 高橋建設(一度も注文していない顧客)
3基本ドリル(10問)
2列8行。最後に 高橋建設 と空欄
FROM customers c LEFT JOIN orders o ON c.id = o.customer_id と書く。
SELECT c.name, o.id FROM customers c LEFT JOIN orders o ON c.id = o.customer_id;
高橋建設の行が残り、注文IDが空欄(NULL)になっている。INNER JOIN では消えていた行が、LEFT JOIN では保持される。
A. 0になる B. NULLになる C. 空文字になる D. 行ごと消える 選択★☆☆無料
解答 == B
「値が存在しない」を表すもの。
B
右側の列はすべてNULLで埋められる。だからSQL05で学んだNULLの知識が、そのままここで必要になる。
1行。高橋建設
LEFT JOIN してから WHERE o.id IS NULL で絞る。
SELECT c.name FROM customers c LEFT JOIN orders o ON c.id = o.customer_id WHERE o.id IS NULL;
「無いこと」を探す定番の書き方。LEFT JOIN で全件を残し、対応がなかった行(NULLになった行)だけを WHERE で拾う。
SELECT c.name, o.id FROM customers c ____ JOIN orders o ON c.id = o.customer_id;
2列8行。高橋建設が含まれる
「左側を全部残す」を意味する4文字。
LEFT
LEFT は「左側のテーブル」、つまり FROM に書いたほうを指す。RIGHT JOIN もあるが、左右を入れ替えれば同じことなので実務ではほぼ LEFT が使われる。
A. 0件 B. 1件 C. NULL D. 行が出ない 選択★☆☆無料
解答 == B
COUNT(*) は「行数」を数える。NULLの行も1行として存在している。
B
これがLEFT JOIN最大の落とし穴。行自体は存在するので1件と数えられる。正しく0件にするには COUNT(o.id) と列名を指定する。
2列4行。高橋建設が 0
COUNT(*) ではなく COUNT(o.id) を使う。列名を指定するとNULLは数えられない。
SELECT c.name AS 顧客, COUNT(o.id) AS 件数 FROM customers c LEFT JOIN orders o ON c.id = o.customer_id GROUP BY c.name;
COUNT(*) と COUNT(o.id) で結果が変わる。前者は高橋建設を1件、後者は0件と数える。LEFT JOIN のときは必ず列名を指定すると覚えておく。
SELECT p.name FROM products p LEFT JOIN orders o ON p.id = o.product_id WHERE o.id ____ NULL;
2行。新商品A / サンプル品
NULLの判定に使う2語(SQL05)。
IS
売れていない商品が見つかった。在庫はあるのに一度も注文されていない——LEFT JOIN でしか発見できない情報。
A. customers B. orders C. 両方 D. どちらでもない 選択★★☆無料
解答 == A
LEFT は FROM に書いたほうを指す。
A
左=FROM に書いたテーブル。「どちらを全件残したいか」を先に決めて、それを FROM に置くのが書き方のコツ。
2列4行。田中商事 297500 が先頭、高橋建設 0 が最後
LEFT JOIN を2回使い、COALESCE(SUM(...), 0) で0に置き換える。
SELECT c.name AS 顧客, COALESCE(SUM(p.price * o.qty), 0) AS 売上 FROM customers c LEFT JOIN orders o ON c.id = o.customer_id LEFT JOIN products p ON o.product_id = p.id GROUP BY c.name ORDER BY 売上 DESC;
COALESCE を外すと高橋建設の売上が空欄になる。0円と表示することで「取引がない」ことが明確に伝わり、並べ替えにも正しく参加する。
2列7行。新商品Aとサンプル品が 0
products を左にして LEFT JOIN し、COALESCE(SUM(o.qty), 0) を使う。
SELECT p.name AS 商品, COALESCE(SUM(o.qty), 0) AS 数量 FROM products p LEFT JOIN orders o ON p.id = o.product_id GROUP BY p.name;
売れていない商品も一覧に出る。INNER JOIN なら5件しか出ないが、LEFT JOIN なら全7件が並び、売上0の商品が浮かび上がる。
4実践シナリオ(5問)
3列4行。田中商事 東京 3 が先頭、高橋建設 福岡 0 が最後
COUNT(o.id) を使う。GROUP BY には c.name と c.area の両方を書く。
SELECT c.name AS 顧客, c.area AS エリア, COUNT(o.id) AS 件数 FROM customers c LEFT JOIN orders o ON c.id = o.customer_id GROUP BY c.name, c.area ORDER BY 件数 DESC;
全4社が並んだ。前章の INNER JOIN では3社しか出なかったので、営業リストとしてはこちらが正しい。
3列4行。高橋建設が 1 と 0
2つの COUNT を同時に書ける。違いが並んで見えるようにする。
SELECT c.name AS 顧客, COUNT(*) AS 誤った件数, COUNT(o.id) AS 正しい件数 FROM customers c LEFT JOIN orders o ON c.id = o.customer_id GROUP BY c.name;
高橋建設だけ 1 と 0 で食い違う。他の3社は同じ数字なので、テストデータに未取引の顧客がいなければこのバグには気づけない。実務で怖いのはまさにこのパターン。
3列2行。新商品A 未分類 0 / サンプル品 周辺機器 3
products を左に LEFT JOIN し、WHERE o.id IS NULL で絞る。COALESCE も併用する。
SELECT p.name AS 商品, COALESCE(p.category, '未分類') AS 分類, COALESCE(p.stock, 0) AS 在庫 FROM products p LEFT JOIN orders o ON p.id = o.product_id WHERE o.id IS NULL;
在庫を抱えているのに売れていない商品が特定できた。サンプル品は3個の在庫がある。これはINNER JOINでは絶対に見つからない情報。
3列3行。東京 2 457500 / 大阪 1 75000 / 福岡 1 0
customers を左に2回 LEFT JOIN する。顧客数は COUNT(DISTINCT c.id)、売上は COALESCE(SUM(...), 0)。
SELECT c.area AS エリア, COUNT(DISTINCT c.id) AS 顧客数, COALESCE(SUM(p.price * o.qty), 0) AS 売上 FROM customers c LEFT JOIN orders o ON c.id = o.customer_id LEFT JOIN products p ON o.product_id = p.id GROUP BY c.area ORDER BY 売上 DESC;
顧客数に COUNT(DISTINCT c.id) を使うのが要点。単に COUNT(*) だと注文件数の分だけ重複して数えてしまう。東京は2社だが注文が5件あるため、DISTINCT が無いと5になる。
4列7行。ノートPC 2 3 384000 が先頭、売上0の商品が3件
products を左に LEFT JOIN。注文回数は COUNT(o.id)、数量と売上は COALESCE(SUM(...), 0)。
SELECT p.name AS 商品, COUNT(o.id) AS 注文回数, COALESCE(SUM(o.qty), 0) AS 数量, COALESCE(SUM(p.price * o.qty), 0) AS 売上 FROM products p LEFT JOIN orders o ON p.id = o.product_id GROUP BY p.name ORDER BY 売上 DESC;
売上0の商品が3件あることに注目してほしい。新商品Aとサンプル品は注文なし。この一覧があれば「どの商品に販促をかけるべきか」が一目で分かる。
5仕上げ課題
表示する列
① 顧客名 → 別名「顧客」
② エリア → 別名「エリア」
③ 注文件数(0件も表示)→ 別名「注文件数」
④ 購入した商品の種類数(0も表示)→ 別名「商品種類」
⑤ 売上合計(0円も表示)→ 別名「売上」
⑥ 取引状態(売上が0なら「未取引」、そうでなければ「取引中」)→ 別名「状態」
並び順:売上の大きい順
期待される結果(6列 × 4行)
顧客 | エリア | 注文件数 | 商品種類 | 売上 | 状態
田中商事 | 東京 | 3 | 3 | 297500 | 取引中
佐藤物産 | 東京 | 2 | 2 | 160000 | 取引中
鈴木工業 | 大阪 | 2 | 2 | 75000 | 取引中
高橋建設 | 福岡 | 0 | 0 | 0 | 未取引
ヒント:⑥は CASE WHEN 条件 THEN '値' ELSE '値' END という書き方で作れます(詳しくは次章)。
6列 × 4行が完全一致
customers を左にして2回 LEFT JOIN する。件数は COUNT(o.id)、種類数は COUNT(DISTINCT p.id)、売上は COALESCE(SUM(p.price*o.qty), 0)。状態は CASE WHEN COALESCE(SUM(p.price*o.qty),0) = 0 THEN '未取引' ELSE '取引中' END。
SELECT
c.name AS 顧客,
c.area AS エリア,
COUNT(o.id) AS 注文件数,
COUNT(DISTINCT p.id) AS 商品種類,
COALESCE(SUM(p.price * o.qty), 0) AS 売上,
CASE
WHEN COALESCE(SUM(p.price * o.qty), 0) = 0 THEN '未取引'
ELSE '取引中'
END AS 状態
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
LEFT JOIN products p ON o.product_id = p.id
GROUP BY c.name, c.area
ORDER BY 売上 DESC;
前章で消えていた高橋建設が、正しく「未取引・0円」として復活した。営業リストとしては、この4行が正解だ。取引がない顧客こそ、営業がアプローチすべき相手なのだから。
この課題ではNULLに対する3つの対処がすべて登場している。件数は COUNT(o.id) で列名を指定し、種類数は COUNT(DISTINCT p.id)、金額は COALESCE(SUM(…), 0)。どれか一つでも COUNT(*) や素の SUM に置き換えると、高橋建設の行だけ数字が狂う。
そして最も恐ろしいのは、他の3社では正しく見えることだ。テストデータに未取引の顧客が含まれていなければ、このバグは発見されない。本番データで初めて「なぜ注文0件の顧客が1件と表示されるのか」と気づくことになる。境界のデータでテストする——これがデータを扱う仕事の鉄則になる。
次章では CASE 式を本格的に扱う。今回ヒントとして使った「条件によって値を変える」書き方を、自在に組み立てられるようになる。