SQL / LESSON 11 / SQL11

SQL:サブクエリ

1学習の目的

2基礎解説

ここまでは1回のクエリで完結していました。「まず何かを調べて、その結果を使って絞り込む」——2段階の処理が必要なとき、サブクエリを使います。

書き方返す値
スカラWHERE price > (SELECT AVG(price) …)値1つ
INWHERE id IN (SELECT product_id …)値のリスト
EXISTSWHERE EXISTS (SELECT 1 …)true / false
FROM句FROM (SELECT … ) AS t表そのもの
✅ 覚えるべき重要ポイント
  1. サブクエリは必ず丸カッコで囲む。内側のSELECTが先に実行され、その結果が外側の条件として使われる。
  2. スカラサブクエリは「値を1つだけ」返す必要があるWHERE price > (SELECT AVG(price) …) のように、比較演算子の右に置ける。
  3. IN複数の値のリストを受け取れる。「注文されたことがある商品」のように、値の集合と照合したいときに使う。
  4. NOT IN のリストにNULLが1つでも混ざると、結果が全体で0件になる。NULLとの比較は「不明」を返すため、SQLは安全側に倒して何も一致させない。NOT IN を使うなら、サブクエリに IS NOT NULL を必ず付ける
  5. EXISTS存在するかどうかだけを見る。中身の値を使わないので、NULLが混ざっていても安全。NOT IN より NOT EXISTS のほうが事故が起きにくい

現場使用例:平均以上の売上を出した商品、一度も購入されていない会員、他店の最安値より安い商品、部門の平均給与を超える社員。「基準そのものを計算しながら絞り込む」場面はすべてサブクエリ

-- 平均より高い商品(スカラサブクエリ)
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);

-- 注文されたことがある商品(IN)
SELECT name
FROM products
WHERE id IN (SELECT product_id FROM orders);

-- ⚠ NOT IN の罠:NULLが混ざると0件になる
SELECT name FROM products
WHERE id NOT IN (SELECT customer_id FROM orders);  -- customer_idにNULLがあれば全滅

-- ✅ IS NOT NULL で防ぐ
SELECT name FROM products
WHERE id NOT IN (
  SELECT product_id FROM orders WHERE product_id IS NOT NULL
);

-- 一度でも注文された顧客(EXISTS・NULLに強い)
SELECT name FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

-- 商品ごとの注文回数(相関サブクエリ)
SELECT
  p.name,
  (SELECT COUNT(*) FROM orders o WHERE o.product_id = p.id) AS 注文数
FROM products p;

3基本ドリル(10問)

SQL11-D01 price が全商品の平均より高い商品の name と price を取り出すSQLを書け。 コーディング★☆☆無料
期待される結果

2行。ノートPC / モニター

ヒント

WHERE price > (SELECT AVG(price) FROM products) と書く。サブクエリは丸カッコで囲む。

模範解答
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);
解説

先に平均を計算し、その値で絞り込んでいる。平均値は35200円だが、この数字を自分で調べて書き込む必要はない——サブクエリが自動で計算してくれる。

SQL11-D02 サブクエリはどのように書くか。
A. 波カッコ { } で囲む B. 丸カッコ ( ) で囲む C. 角カッコ [ ] で囲む D. 特に囲まない
選択★☆☆無料
期待される結果

解答 == B

ヒント

関数の引数と同じ記号。

模範解答
B
解説

丸カッコで囲まないと構文エラーになる。内側のSELECTが先に実行され、その結果が外側から見える。

SQL11-D03 一度でも注文されたことがある商品の name を、IN を使って取り出すSQLを書け。 コーディング★☆☆無料
期待される結果

5行。ノートPC / マウス / キーボード / モニター / USBメモリ

ヒント

WHERE id IN (SELECT product_id FROM orders) と書く。

模範解答
SELECT name
FROM products
WHERE id IN (SELECT product_id FROM orders);
解説

orders テーブルに登場する product_id のリストと照合している。新商品Aとサンプル品は一度も注文されていないので除外される。

SQL11-D04 スカラサブクエリで最高値の商品を取り出すSQLを完成させよ。 穴埋め★☆☆無料
コード
SELECT name
FROM products
WHERE price = (SELECT ____(price) FROM products);
期待される結果

1行。ノートPC

ヒント

「最大」を意味する3文字の集計関数。

模範解答
MAX
解説

「最高値と等しい商品」を探すという書き方。ORDER BY と LIMIT 1 でも同じことができるが、サブクエリなら同点があっても全部拾える。

SQL11-D05 WHERE price > (SELECT price FROM products) のように、サブクエリが複数行を返す可能性があるとき、比較演算子(=, > など)を使うとどうなるか。
A. 常に正しく動く B. エラーになるか、最初の1行だけが暗黙的に使われる C. 自動的にリストとして扱われる D. NULLになる
選択★☆☆無料
期待される結果

解答 == B

ヒント

比較演算子は「値1つ」を前提にしている。

模範解答
B
解説

環境によって挙動が違う危険な書き方。多くの環境ではエラーになるが、一部では警告なく最初の1行だけが使われることがある。複数行を扱うなら IN を使う。

SQL11-D06 一度も注文されていない商品の name を、IN と NOT を使って取り出すSQLを書け。 コーディング★★☆無料
期待される結果

2行。新商品A / サンプル品

ヒント

WHERE id NOT IN (SELECT product_id FROM orders) と書く。orders.product_id にNULLは無いので安全に使える。

模範解答
SELECT name
FROM products
WHERE id NOT IN (SELECT product_id FROM orders);
解説

今回は product_id にNULLが無いので正しく動く。ただし実務のデータではNULLが混ざることがあり、その場合は次の問題のように対策が必要になる。

SQL11-D07 NOT IN の罠を避けるコードを完成させよ。 穴埋め★★☆無料
コード
SELECT name FROM products
WHERE id NOT IN (
  SELECT product_id FROM orders WHERE product_id ____ NULL
);
期待される結果

2行。新商品A / サンプル品

ヒント

NULLを除外する条件(SQL05・SQL10)。

模範解答
IS NOT
解説

サブクエリの中でNULLを先に除いておくのが安全策。この一手間を忘れると、customer_id や product_id にNULLが1件でも入った瞬間、NOT IN の結果が全体で0件になる。

SQL11-D08 NOT IN (1, 2, NULL) の結果はどうなるか。
A. 1と2以外にマッチする B. 常に0件になる C. NULLは無視されて1と2以外にマッチする D. エラーになる
選択★★☆無料
期待される結果

解答 == B

ヒント

NULLとの比較は「不明」を返す。「不明」を含むANDやORの結果は。

模範解答
B
解説

これがNOT INの罠の核心。「1でも2でもNULLでもない」という判定に、NULLとの比較が紛れ込むと結果全体が確定できなくなり、SQLは安全側に倒して0件を返す。

SQL11-D09 一度でも注文したことがある顧客の name を、EXISTS を使って取り出すSQLを書け。 コーディング★★☆無料
期待される結果

3行。田中商事 / 鈴木工業 / 佐藤物産

ヒント

WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id) と書く。外側のテーブルに別名を付けておく。

模範解答
SELECT name
FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);
解説

EXISTS は中身の値を見ず、行があるかないかだけを判定するSELECT 1 の1に意味はなく、何を選んでも結果は変わらない。

SQL11-D10 商品ごとに、その商品が注文された回数を「注文数」という別名で、name と並べて取り出すSQLを書け(相関サブクエリを使うこと)。 コーディング★★☆無料
期待される結果

2列7行。ノートPC 2 / 新商品A 0

ヒント

SELECT の中に (SELECT COUNT(*) FROM orders o WHERE o.product_id = p.id) と書く。

模範解答
SELECT
  p.name,
  (SELECT COUNT(*) FROM orders o WHERE o.product_id = p.id) AS 注文数
FROM products p;
解説

SELECT の中にもサブクエリを書ける。外側の p.id を内側のサブクエリが参照しており、商品ごとに違う結果を返す(相関サブクエリ)。

4実践シナリオ(5問)

SQL11-S01 【平均以上の高額商品】price が全商品の平均以上の商品を、name・price・平均との差(price引く平均、別名「差額」)で取り出し、差額の大きい順に並べるSQLを書け。 コーディング★★☆無料
期待される結果

3列3行。ノートPC 128000 92800.0 が先頭

ヒント

WHERE と SELECT の両方でスカラサブクエリを使う。同じサブクエリを2回書くことになる。

模範解答
SELECT
  name,
  price,
  price - (SELECT AVG(price) FROM products) AS 差額
FROM products
WHERE price >= (SELECT AVG(price) FROM products)
ORDER BY 差額 DESC;
解説

同じサブクエリをWHEREとSELECTの両方で使っている。書く手間は増えるが、「平均」という基準を数値としてハードコードしていないので、データが増減しても常に正しい結果になる。

SQL11-S02 【死に筋商品(サブクエリ版)】一度も注文されていない商品を、EXISTS を使わずに NOT IN で取り出すSQLを書け。name・category・stock を取り出すこと。NULLの罠を避けるように書くこと。 コーディング★★☆無料
期待される結果

3列2行。新商品A / サンプル品

ヒント

サブクエリの中に WHERE product_id IS NOT NULL を必ず入れる。

模範解答
SELECT name, category, stock
FROM products
WHERE id NOT IN (
  SELECT product_id FROM orders WHERE product_id IS NOT NULL
);
解説

SQL10のLEFT JOINと同じ結果を、サブクエリでも作れる。書き方は違っても目的が同じなら結果は一致する——これがSQLの面白いところで、複数の道具で同じ課題を解けるようになる。

SQL11-S03 【未取引顧客の抽出(EXISTS版)】一度も注文していない顧客を、name と area で、NOT EXISTS を使って取り出すSQLを書け。 コーディング★★☆無料
期待される結果

2列1行。高橋建設 福岡

ヒント

NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id) と書く。

模範解答
SELECT name, area
FROM customers c
WHERE NOT EXISTS (
  SELECT 1 FROM orders o WHERE o.customer_id = c.id
);
解説

NOT EXISTS は customer_id にNULLが混ざっていても安全。存在の有無だけを見ているので、NOT INのような罠が起きない。「無いことを調べる」なら NOT EXISTS が第一候補

SQL11-S04 【顧客ごとの最高額注文】各顧客について、その顧客の中で最も金額が高かった注文を、顧客名(別名「顧客」)・商品名(別名「商品」)・金額(別名「金額」)で取り出すSQLを書け(相関サブクエリを使うこと)。 コーディング★★★無料
期待される結果

3列3行。田中商事 ノートPC 256000 / 鈴木工業 モニター 45000 / 佐藤物産 ノートPC 128000

ヒント

WHERE の中で、同じ顧客の注文だけを対象にした MAX を計算する。サブクエリの中で外側の customer_id を参照する。

模範解答
SELECT
  c.name AS 顧客,
  p.name AS 商品,
  p.price * o.qty AS 金額
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN products  p ON o.product_id  = p.id
WHERE p.price * o.qty = (
  SELECT MAX(p2.price * o2.qty)
  FROM orders o2
  JOIN products p2 ON o2.product_id = p2.id
  WHERE o2.customer_id = o.customer_id
);
解説

「顧客ごとの最大」という基準が、顧客によって変わるのが相関サブクエリの本質。田中商事の基準は256000円、鈴木工業の基準は45000円——サブクエリが行ごとに違う値で評価される。

SQL11-S05 【カテゴリ平均を超える商品】各商品について、その商品が属するカテゴリの平均価格より高い商品を、name・category・price を取り出すSQLを書け(相関サブクエリを使うこと)。 コーディング★★★無料
期待される結果

2行。ノートPC / キーボード

ヒント

サブクエリの中で WHERE category = p.category と、外側のカテゴリを参照する。

模範解答
SELECT name, category, price
FROM products p
WHERE price > (
  SELECT AVG(price)
  FROM products p2
  WHERE p2.category = p.category
);
解説

全体の平均ではなく「自分のカテゴリ内の平均」と比較している。PCの中ではノートPCが平均より高く、周辺機器の中ではキーボードが平均より高い——カテゴリごとに基準が変わる高度な絞り込み。

5仕上げ課題

SQL11-FINAL 【優良顧客の抽出】経営陣から「平均以上の売上がある顧客を選び出してほしい」と依頼された。

手順
① 顧客ごとの売上合計を計算する(customersordersproducts を結合)
全顧客の平均売上をサブクエリで計算する(取引のない顧客も0円として計算に含める
③ 平均以上の売上がある顧客だけを残す

表示する列
① 顧客名 → 別名「顧客」
② 売上合計 → 別名「売上」
③ 全体平均との差(別名「平均との差」)

並び順:売上の大きい順

期待される結果(3列 × 2行)
顧客 | 売上 | 平均との差
田中商事 | 297500 | 164625.0
佐藤物産 | 160000 | 27125.0


※ 全体平均(4社・0円含む)は 132875円 になります。これがヒントです。
期待される結果

3列 × 2行が完全一致

ヒント

顧客ごとの売上を計算するには LEFT JOIN と COALESCE(SUM(...),0) を使う(SQL10)。全体平均は、顧客ごとの売上をまとめたサブクエリ(FROM句のサブクエリ)から AVG を取る。外側のクエリで HAVING を使い、売上がその平均以上のものだけ残す。

模範解答
SELECT
  顧客,
  売上,
  売上 - (
    SELECT AVG(売上)
    FROM (
      SELECT 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.id
    )
  ) AS 平均との差
FROM (
  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
) AS sales
WHERE 売上 >= (
  SELECT AVG(売上)
  FROM (
    SELECT 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.id
  )
)
ORDER BY 売上 DESC;
解説

「FROM句のサブクエリ」を使うと、集計結果をさらに集計できる。まず顧客ごとの売上を1つの表として作り(内側)、それを材料に平均や比較を行う(外側)。集計の集計という、SQLならではの多段階の分析がここでできるようになった。

最も重要なのは「取引のない顧客も0円として平均に含めた」ことだ。もしLEFT JOINを使わずINNER JOINで顧客ごとの売上を作っていたら、高橋建設(0円)が計算から漏れ、平均は177500円まで跳ね上がり、結果として佐藤物産(160000円)が「平均未満」に落ちてしまう。母数に何を含めるかで、平均という一見客観的な数字さえ変わってしまう

この章で学んだサブクエリには、大きく3つの形があった。値を1つ返すスカラ(平均との比較)、リストを返すIN(注文された商品)、存在確認のEXISTS(取引の有無)。どれも「別のクエリを先に解いて、その答えを使う」という同じ発想から生まれている。

次章では文字列と日付の操作を学ぶ。ここまで数値の集計を中心に扱ってきたが、「今月の注文だけ」「名前に特定の文字を含む」といった、テキストと日付ならではの処理に入っていく。