ウェブエンジニア問題集
第6章

サブクエリとEXISTS — IN・相関サブクエリ・派生テーブル

6
この章の目次開く

サブクエリは、SQLの中に別の SELECT を書く方法です。 条件の候補を別クエリで作ったり、集計結果を一度作ってからさらに絞り込んだりできます。

この章では、INEXISTS、相関サブクエリ、派生テーブルを整理します。

学習者学習者

JOINでも書けるし、サブクエリでも書ける場面があるよね。どう使い分ければいいの?

サブクエリとは

サブクエリは、クエリの中に入る別のクエリです。 括弧 () で囲んで書きます。

SELECT id, name
FROM users
WHERE id IN (
  SELECT user_id
  FROM orders
  WHERE status = 'paid'
);
sql

このSQLは、支払い済み注文を持つユーザーを取得します。 内側のクエリで orders から user_id を取り出し、外側のクエリでそのIDに一致するユーザーを取得しています。

サブクエリは、SQLの一部として使うための中間結果を別の SELECT で作る書き方です。

INを使うサブクエリ

IN は、値が候補リストに含まれているかを判定します。

SELECT id, name
FROM users
WHERE id IN (1, 2, 3);
sql

候補リストをサブクエリで作ることもできます。

SELECT id, name
FROM users
WHERE id IN (
  SELECT user_id
  FROM orders
  WHERE total_amount >= 10000
);
sql

このSQLは、1万円以上の注文をしたユーザーを取得します。

IN のサブクエリは、「Aの中にBの結果が含まれるか」を表したいときに読みやすいです。

EXISTSを使う

EXISTS は、サブクエリの結果が1件でも存在するかを判定します。

SELECT id, name
FROM users
WHERE EXISTS (
  SELECT 1
  FROM orders
  WHERE orders.user_id = users.id
    AND orders.status = 'paid'
);
sql

このSQLも、支払い済み注文を持つユーザーを取得します。

EXISTS のサブクエリでは、外側の users.id を内側で参照しています。 このように、外側の行に依存して実行されるサブクエリを相関サブクエリと呼びます。

EXISTS は「条件に合う関連行が存在するか」を表すときに自然です。

SELECT 11 に特別な意味はありません。 EXISTS はサブクエリが行を返すかどうかだけを見るため、列の中身は重要ではありません。

先生先生

JOINとサブクエリで迷ったら、「関連テーブルの列も取りたいか」で考えよう。列も使うならJOIN、存在確認だけならEXISTSが読みやすいよ。

NOT EXISTSで存在しないものを探す

NOT EXISTS を使うと、関連行が存在しないものを探せます。

SELECT id, name
FROM users
WHERE NOT EXISTS (
  SELECT 1
  FROM orders
  WHERE orders.user_id = users.id
);
sql

これは注文が一度もないユーザーを取得します。

LEFT JOIN ... WHERE orders.id IS NULL でも同じようなことができます。

SELECT users.id, users.name
FROM users
LEFT JOIN orders
  ON users.id = orders.user_id
WHERE orders.id IS NULL;
sql

どちらでも書けますが、「存在しない関連行を探す」という意図は NOT EXISTS の方が読みやすいことがあります。

FROM句のサブクエリ

FROM の中にサブクエリを書くと、一時的なテーブルのように扱えます。 これを派生テーブルと呼ぶことがあります。

SELECT
  user_order_counts.user_id,
  user_order_counts.order_count
FROM (
  SELECT
    user_id,
    COUNT(*) AS order_count
  FROM orders
  GROUP BY user_id
) AS user_order_counts
WHERE user_order_counts.order_count >= 3;
sql

このSQLは、ユーザーごとの注文数を集計し、その結果から3件以上のユーザーだけを取り出します。

この例は HAVING でも書けます。

SELECT
  user_id,
  COUNT(*) AS order_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 3;
sql

派生テーブルは、集計結果をさらにJOINしたい場合や、複雑な処理を段階に分けたい場合に便利です。

JOINとサブクエリの使い分け

JOINとサブクエリは、同じ結果を出せる場面があります。

-- JOINで書く
SELECT DISTINCT users.id, users.name
FROM users
INNER JOIN orders
  ON users.id = orders.user_id
WHERE orders.status = 'paid';
sql
-- EXISTSで書く
SELECT users.id, users.name
FROM users
WHERE EXISTS (
  SELECT 1
  FROM orders
  WHERE orders.user_id = users.id
    AND orders.status = 'paid'
);
sql

結果は似ていますが、読み方が違います。

書き方向いている意図
JOIN関連テーブルの列も一緒に取得したい
EXISTS関連行が存在するかだけ知りたい
IN候補リストに含まれるかを表したい
派生テーブル一度集計・加工した結果をさらに使いたい
学習者学習者

NOT INとNOT EXISTSって結果は同じに見えるけど、どっちを使えばいいの?

先生先生

NOT INはサブクエリにNULLが混ざると結果が返らなくなることがある。安全なのはNOT EXISTS。迷ったらNOT EXISTSを選ぼう。

サブクエリを活用すると、複雑な条件や段階的なデータ加工もSQLだけで実現できます

スカラーサブクエリ

1つの値だけを返すサブクエリを、スカラーサブクエリと呼びます。

SELECT
  id,
  name,
  (SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id) AS order_count
FROM users;
sql

ユーザーごとの注文数を列として出しています。 読みやすい場面もありますが、行数が多いと重くなることがあります。 JOINと集計で書いた方がよい場合もあります。

SELECT
  users.id,
  users.name,
  COUNT(orders.id) AS order_count
FROM users
LEFT JOIN orders
  ON users.id = orders.user_id
GROUP BY users.id, users.name;
sql

よくあるハマりどころ

INのサブクエリにNULLが混ざる

NOT IN は、サブクエリ結果に NULL が混ざると直感と違う結果になることがあります。

SELECT id, name
FROM users
WHERE id NOT IN (
  SELECT user_id
  FROM orders
);
sql

orders.user_idNULL があると、結果が返らないことがあります。 存在しない関連行を探す場合は、NOT EXISTS の方が安全なことが多いです。

SELECT id, name
FROM users
WHERE NOT EXISTS (
  SELECT 1
  FROM orders
  WHERE orders.user_id = users.id
);
sql

サブクエリを深くネストしすぎる

サブクエリを何重にも入れると、読みづらくなります。 段階的に整理したい場合は、CTE(WITH)を使う方法もあります。 この本では発展トピックとして扱いますが、考え方は「途中結果に名前を付ける」です。

ちゃんと使うためのポイント

  • サブクエリはSQLの中で使う中間結果を作る
  • IN は候補リストに含まれるかを見る
  • EXISTS は関連行の存在確認に向いている
  • 関連行がないものを探すなら NOT EXISTS が読みやすい
  • 関連テーブルの列も取りたいなら JOIN が自然
  • サブクエリが深くなりすぎたら、クエリを分解して考える

次の章では、SQLを書く前提となるテーブル設計、正規化、制約について学びます。

SQLクイズに挑戦するこの章で学んだSQLの知識を、4択クイズでアウトプットして定着させよう