SQL / LESSON 10 / SQL10

SQL:外部結合(LEFT JOIN)

1学習の目的

2基礎解説

前章の INNER JOIN では、注文が0件の高橋建設が結果から消えました。「取引のない顧客も一覧に出したい」——そのための結合が LEFT JOIN です。

INNER JOINLEFT JOIN
残る行両方にある行だけ左側は全部残る
対応がないときその行ごと消える右側の列がNULLになる
結果の行数減ることがある左側の行数以上
使う場面取引実績の分析全件リスト・欠損の発見
✅ 覚えるべき重要ポイント
  1. LEFT JOIN は「左側のテーブルを全部残す」FROM customers c LEFT JOIN orders o なら、customers が左で全4社が残る。
  2. 対応する行がないと右側の列はすべて NULL になる。高橋建設の行は、注文IDも数量もNULLで埋まる。
  3. ⚠ COUNT(*) を使うとNULLの行も1件と数えてしまう。注文0件の顧客が「1件」と表示される。必ず COUNT(o.id) のように列名を指定する
  4. 合計も同様に COALESCE(SUM(…), 0) で包む。SUM の対象が無いとNULLになるので、0円と表示したいなら明示的に置き換える(SQL06)。
  5. 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問)

SQL10-D01 customers を左にして orders と LEFT JOIN し、顧客名(c.name)と注文ID(o.id)を取り出すSQLを書け。 コーディング★☆☆無料
期待される結果

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 では保持される。

SQL10-D02 LEFT JOIN で、右側のテーブルに対応する行がないとき、右側の列はどうなるか。
A. 0になる B. NULLになる C. 空文字になる D. 行ごと消える
選択★☆☆無料
期待される結果

解答 == B

ヒント

「値が存在しない」を表すもの。

模範解答
B
解説

右側の列はすべてNULLで埋められる。だからSQL05で学んだNULLの知識が、そのままここで必要になる。

SQL10-D03 一度も注文していない顧客の名前を取り出すSQLを書け。 コーディング★☆☆無料
期待される結果

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 で拾う。

SQL10-D04 全顧客を残して結合するSQLを完成させよ。 穴埋め★☆☆無料
コード
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 が使われる。

SQL10-D05 LEFT JOIN のあとに COUNT(*) で件数を数えると、注文0件の顧客はどう表示されるか。
A. 0件 B. 1件 C. NULL D. 行が出ない
選択★☆☆無料
期待される結果

解答 == B

ヒント

COUNT(*) は「行数」を数える。NULLの行も1行として存在している。

模範解答
B
解説

これがLEFT JOIN最大の落とし穴。行自体は存在するので1件と数えられる。正しく0件にするには COUNT(o.id) と列名を指定する

SQL10-D06 顧客ごとの注文件数を、顧客名(別名「顧客」)と件数(別名「件数」)で取り出すSQLを書け。注文0件の顧客は0と表示すること コーディング★★☆無料
期待される結果

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 のときは必ず列名を指定すると覚えておく。

SQL10-D07 注文のない商品を探すSQLを完成させよ。 穴埋め★★☆無料
コード
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 でしか発見できない情報。

SQL10-D08 FROM customers c LEFT JOIN orders o のとき、全件残るのはどちらのテーブルか。
A. customers B. orders C. 両方 D. どちらでもない
選択★★☆無料
期待される結果

解答 == A

ヒント

LEFT は FROM に書いたほうを指す。

模範解答
A
解説

左=FROM に書いたテーブル。「どちらを全件残したいか」を先に決めて、それを FROM に置くのが書き方のコツ。

SQL10-D09 顧客ごとの売上合計を、顧客名(別名「顧客」)と売上(別名「売上」)で取り出すSQLを書け。取引のない顧客は0と表示し、売上の多い順に並べること。 コーディング★★☆無料
期待される結果

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円と表示することで「取引がない」ことが明確に伝わり、並べ替えにも正しく参加する。

SQL10-D10 全商品について、商品名(別名「商品」)と注文された合計数量(別名「数量」、注文がなければ0)を取り出すSQLを書け。 コーディング★★☆無料
期待される結果

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問)

SQL10-S01 【全顧客の取引状況】全顧客について、顧客名(別名「顧客」)・エリア(別名「エリア」)・注文件数(別名「件数」、0件も表示)を、件数の多い順に取り出すSQLを書け。 コーディング★★☆無料
期待される結果

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社しか出なかったので、営業リストとしてはこちらが正しい。

SQL10-S02 【COUNT の違いを確かめる】全顧客について、顧客名(別名「顧客」)・COUNT(*) の結果(別名「誤った件数」)・COUNT(o.id) の結果(別名「正しい件数」)を並べて取り出すSQLを書け。 コーディング★★☆無料
期待される結果

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社は同じ数字なので、テストデータに未取引の顧客がいなければこのバグには気づけない。実務で怖いのはまさにこのパターン。

SQL10-S03 【死に筋商品の抽出】一度も注文されていない商品について、商品名(別名「商品」)・分類(NULLは「未分類」、別名「分類」)・在庫(NULLは0、別名「在庫」)を取り出すSQLを書け。 コーディング★★☆無料
期待される結果

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では絶対に見つからない情報

SQL10-S04 【エリア別の顧客数と売上】エリア(別名「エリア」)ごとに、顧客数(別名「顧客数」)と売上合計(別名「売上」、取引がなければ0)を取り出し、売上の多い順に並べるSQLを書け。全エリアを表示すること コーディング★★★無料
期待される結果

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になる。

SQL10-S05 【全商品の販売状況】全商品について、商品名(別名「商品」)・注文回数(別名「注文回数」、0も表示)・販売数量(別名「数量」、0も表示)・売上(別名「売上」、0も表示)を、売上の多い順に取り出すSQLを書け。 コーディング★★★無料
期待される結果

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仕上げ課題

SQL10-FINAL 【全顧客 取引状況レポート】営業部から「取引のない顧客も含めた全社リストを作ってほしい」と依頼された。前章のレポートでは高橋建設が抜け落ちていたため、その修正版になる。

表示する列
① 顧客名 → 別名「顧客」
② エリア → 別名「エリア」
③ 注文件数(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 式を本格的に扱う。今回ヒントとして使った「条件によって値を変える」書き方を、自在に組み立てられるようになる。