クエリの中のクエリ:サブクエリ

サブクエリは、クエリの中に書くもうひとつのクエリです。値ひとつ、照合用のリスト、一時的なテーブルとして使えます。

値をひとつ返すサブクエリ

値ひとつを書ける場所には、1行1列だけを返すクエリを代わりに書けます。

1

SELECT name,
       price,
       ROUND((SELECT AVG(price) FROM products), 2) AS average_price
FROM products
WHERE price > (SELECT AVG(price) FROM products)
ORDER BY price DESC
LIMIT 20;
Planche で実行

結果 — 20 行のうち先頭 5 行

namepriceaverage_price
Glide GPS Watch Pro2484.41230.21
Terra 3D Puzzle2481.771230.21
Aria Castle Kit2479.411230.21
Aria LED Strip Pro2463.411230.21
Nova Brief Answers2445.491230.21

ブラウザの別タブで Planche が開き、サンプルデータベースを読み込んだ状態でこのクエリが入ります。このページの例はどれも同じタブを使い回します。F9 を押すまで実行はされません。

(SELECT AVG(price) FROM products) は数字をひとつ作り、残りの部分はそれを、あなたがその数字を打ち込んだかのように扱います。これが「なぜ WHERE price > AVG(price) と書けないのか」への答えでもあります。集計はグループを要約するものですが、WHERE が動いている時点ではまだグループがありません。サブクエリはそれ自体が完結したクエリで、外側が絞り込まれる前に終わっているので、この順序をすり抜けられます。

リストを返すサブクエリ

2

SELECT first_name, last_name, email, country
FROM customers
WHERE id IN (
  SELECT customer_id
  FROM orders
  WHERE status = 'refunded'
)
ORDER BY last_name
LIMIT 20;
Planche で実行

結果 — 20 行のうち先頭 5 行

first_namelast_nameemailcountry
MayaAhmedmaya.ahmed1@mail.exampleUS
NoahAhmednoah.ahmed@inbox.exampleFR
LiamAhmedliam.ahmed1@mail.exampleDE
AishaAhmedaisha.ahmed@mail.exampleGB
SofiaAhmedsofia.ahmed@example.comES

内側のクエリが顧客IDの列を作り、IN がそこに含まれるかを見ます。内側から外へ読んでください。「返金された注文の顧客IDの中に、自分のIDがある顧客」です。

EXISTS:一致したかどうかだけを聞く

3

SELECT c.first_name, c.last_name, c.country
FROM customers c
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.id
    AND o.total > 1000
)
ORDER BY c.last_name
LIMIT 20;
Planche で実行

結果 — 20 行のうち先頭 5 行

first_namelast_namecountry
AishaAhmedGB
AishaAhmedJP
AvaAhmedIT
EmmaAhmedIN
EthanAhmedFR

EXISTS はクエリを取り、それが1行でも返せば真になります。SELECT 1 は慣習です。選んだ値は誰も見ず、行が返ったかどうかしか見ないので、本物の列を選ぶ意味がありません。

このサブクエリは外側の c.id を参照しています。これが相関サブクエリで、一度計算するリストではなく、顧客ごとに違う質問になります。「関連する行があるか」を聞くなら、たいていこれがいちばん読みやすく、エンジンもよく最適化してくれる形です。

NOT IN の罠

120人の従業員のうち110人には部下がいません。それを探すクエリです。

4

-- Employees nobody reports to. 110 of the 120 qualify,
-- and this query finds none of them.
SELECT first_name, last_name, position
FROM employees
WHERE id NOT IN (SELECT manager_id FROM employees);
Planche で実行

該当する行なし

0行。エラーもなし。

manager_id は NULL を許す列で、組織図の頂点にいる10人はそこが NULL です。だから内側のクエリは NULL を含むリストを返し、x NOT IN (1, 2, NULL) は「x は1と違い、2とも違い、この不明な値とも違うか」と聞くことになります。最後の部分が不明なので条件全体が不明になり、どの行も残りません。どんな x であってもです。リストのどこかに NULL がひとつあるだけで、結果は空になります。

IN は同じようには壊れません。実在の値に一致した行は、リストに他に何が入っていても残ります。すべての比較が真でなければならないのは否定形だけで、だから不明がひとつあれば空になるのです。

直し方は、サブクエリの中に WHERE manager_id IS NOT NULL を足すか、あるいは忘れようがないぶん優れている NOT EXISTS に替えるかです。

5

SELECT e.first_name, e.last_name, e.position
FROM employees e
WHERE NOT EXISTS (
  SELECT 1
  FROM employees m
  WHERE m.manager_id = e.id
)
ORDER BY e.last_name
LIMIT 20;
Planche で実行

結果 — 20 行のうち先頭 5 行

first_namelast_nameposition
PriyaAhmedSenior Engineer
DiegoAhmedIntern
YukiAhmedEngineer
RaviBrownLead
JonasBrownSpecialist

NOT EXISTS は値ではなく行の有無を聞くので、NULL が比較に入ってきません。既定で NOT EXISTS に手を伸ばすようにしておくと、黙って間違う類の答えを丸ごと1種類なくせます。

テーブルとしてのサブクエリ

複数行・複数列を返すクエリは、テーブルが書ける場所、つまり FROM に置けます。そういうクエリを導出テーブルと呼びます。

6

SELECT category, products, average
FROM (
  SELECT cat.name AS category,
         COUNT(*) AS products,
         ROUND(AVG(p.price), 2) AS average
  FROM products p
  JOIN categories cat ON cat.id = p.category_id
  GROUP BY cat.id
)
WHERE products >= 10
ORDER BY average DESC;
Planche で実行

結果 — 26 行のうち先頭 5 行

categoryproductsaverage
Wearables111672.29
Programming101585.67
Tablets171529.45
Audio111482.89
Kitchen121443.21

これが「集計で絞り込みたい」へのもうひとつの答えです。内側でグループ化し、その結果を普通のテーブルのように絞り込みます。HAVING と同じ仕事をしますが、HAVING が移植性を保ったままではできない別名での絞り込みもできます。

導出テーブルは入れ子にできて、入れ子になった途端に追いにくくなります。3段も重なれば内側から読むしかありません。次のレッスンでは、それを上から順に読める形に置き換えます。

行ごとに1本のサブクエリ

7

SELECT p.name,
       p.price,
       (SELECT COUNT(*) FROM reviews r WHERE r.product_id = p.id) AS reviews,
       (SELECT ROUND(AVG(r.rating), 2) FROM reviews r WHERE r.product_id = p.id) AS avg_rating
FROM products p
ORDER BY avg_rating DESC, reviews DESC
LIMIT 20;
Planche で実行

結果 — 20 行のうち先頭 5 行

namepricereviewsavg_rating
Atlas LED Strip1856.7525
Orbit Road Bike1731.2915
Onyx Sneakers187844.75
Brief Answers (3rd ed.)117.9664.67
Pulse Good to Great392.9734.67

ここまでで書けるもの

複雑なものを読みやすくすること以外は、ひととおり書けます。それが次のレッスンです。

自分で書いてみる

  1. レビューを一度も書いていない顧客は誰ですか。

    答えを見る

    答えを見る

    SELECT c.id, c.first_name, c.last_name
    FROM customers c
    WHERE NOT EXISTS (
      SELECT 1 FROM reviews r WHERE r.customer_id = c.id
    )
    ORDER BY c.id
    LIMIT 20;
    Planche で実行

    結果 — 20 行のうち先頭 5 行

    idfirst_namelast_name
    3YukiJang
    4InesMeyer
    12KenjiPark
    13JonasLarsen
    26AvaSilva

    NOT IN ではなく NOT EXISTS を使います。ここでは reviews.customer_id にたまたま NULL がないのでどちらでも動きますが、習慣として NOT EXISTS を選んでおけば、効いてくる場面でも最初から正しく書けています。

  2. 平均的な商品より在庫を多く持っている商品を、多い順に。

    答えを見る

    答えを見る

    SELECT name, stock_quantity
    FROM products
    WHERE stock_quantity > (SELECT AVG(stock_quantity) FROM products)
    ORDER BY stock_quantity DESC
    LIMIT 15;
    Planche で実行

    結果 — 15 行のうち先頭 5 行

    namestock_quantity
    Luma Phone Lite500
    Atlas Lens 35mm X499
    Sol Good to Great Plus498
    Echo Phone Pro Plus498
    Pulse Phone Pro Max497

    スカラーサブクエリが平均を一度だけ計算し、外側のクエリが各行をそれと比べます。「平均より上/下」を聞かれたときは、いつもこの形です。