Q1WHERE col IN (SELECT ...)とWHERE EXISTS (SELECT 1 ... WHERE ...)の関係として最も適切なものはどれですか。
IN と EXISTS・相関サブクエリの書き換え
この記事は、基礎から複雑なSQL,SQLチューニングまでSQLの実践的なスキルを1からマスターする「SQL入門講座の一部」です。
INとEXISTSの計画上の違い、相関サブクエリをJOIN+集約に書き換える方法、NOT INがNULLで0行になる落とし穴とNOT EXISTSでの回避を実行計画で確かめます。
本記事で使うデータ — perf_sales と employee
同じ結果を返すクエリでも、書き方によって実行計画(データベースがどの順でテーブルを読み、索引を使うかを決めた手順)が変わります。
本記事ではIN と EXISTS の使い分け、相関サブクエリ(外側の行ごとに内側を評価し直すサブクエリ)の除去、そして「一致がある/ない行だけを残す」書き方の書き換えを、EXPLAIN QUERY PLANで計画を見比べながら学びます。
IN と EXISTS — 同じ判定を 2 通りで書く
「別テーブルに一致する行があるか」を問う書き方は 2 通りあります。
WHERE 列 IN (SELECT ...)は内側が返す値の集合に含まれるかを判定し、WHERE EXISTS (SELECT 1 FROM ... WHERE ...)は内側が 1 行でも返すかを判定します。
多くの場合どちらも同じ結果になり、データベースは両方を一致がある行だけを残す処理として扱えます。
EXPLAIN QUERY PLAN クエリ;を先頭に付けると、データベースが選んだ手順が行で表示されます。
SCAN perf_salesは全行走査、SEARCH ... USING INDEXは索引で必要な行へ直接到達、CORRELATED SCALAR SUBQUERYは外側の行ごとに評価し直すサブクエリを表します。
-- 例: dept_id=2 の社員が担当した売上の件数を 2 通りで
-- IN 形式
SELECT COUNT(*) FROM perf_sales
WHERE emp_id IN (SELECT emp_id FROM employee WHERE dept_id = 2);
-- EXISTS 形式 (相関あり)
SELECT COUNT(*) FROM perf_sales p
WHERE EXISTS (
SELECT 1 FROM employee e
WHERE e.emp_id = p.emp_id AND e.dept_id = 2
);
-- 計画を見るには先頭に EXPLAIN QUERY PLAN を付ける
-- EXPLAIN QUERY PLAN SELECT COUNT(*) FROM perf_sales WHERE ...;
もう少し具体的に、外側 (outer) のテーブル規模と内側 (inner) のサブクエリ規模・索引の有無で IN と EXISTS の処理がどう変わるかを見比べます。
次の 2 つのケースについて、IN と EXISTS のそれぞれの処理とどちらが速いかを下の図で整理します。
-- ケース A: 外側 perf_sales (5 万行), 内側 dept_id=1 の社員 (~5 行・小さく固定)
-- IN 形式
SELECT * FROM perf_sales
WHERE emp_id IN (SELECT emp_id FROM employee WHERE dept_id = 1);
-- EXISTS 形式
SELECT * FROM perf_sales p
WHERE EXISTS (
SELECT 1 FROM employee e
WHERE e.emp_id = p.emp_id AND e.dept_id = 1
);
-- ケース B: 外側 employee (30 行), 内側 perf_sales (5 万行・emp_id に索引あり)
-- IN 形式
SELECT * FROM employee e
WHERE e.emp_id IN (SELECT emp_id FROM perf_sales);
-- EXISTS 形式
SELECT * FROM employee e
WHERE EXISTS (
SELECT 1 FROM perf_sales p
WHERE p.emp_id = e.emp_id
);
Tips — 最近のデータベースでは実速度差はほぼない
ここまで「IN が向く / EXISTS が向く」と分けて整理しましたが、これは古い最適化器を前提とした傾向です。
SQLite / PostgreSQL / MySQL 8.0 以降など現代のデータベースでは、IN (SELECT ...) と相関EXISTSはどちらも同じ「一致がある行だけを残す処理」に変換され、実行計画と実速度はほぼ同じになります。
つまり、書き方の好み(可読性)で選んで構いません。
計画の差や速度差が気になるときは、実測(EXPLAIN QUERY PLANと所要時間)で確かめるのが原則です。
相関サブクエリを JOIN・集約に書き換える
SELECT リストに置いた相関サブクエリ(外側の各行ごとに評価し直すサブクエリ)は、行数が多いと評価回数が膨らみます。
EXPLAIN QUERY PLANではCORRELATED SCALAR SUBQUERYという行で現れます。
多くの場合、これは1 回の集約をあらかじめ作って JOIN する形へ書き換えられ、外側の行ごとの再評価がなくなります。
書き換えの型は「外側の行ごとに(SELECT ... WHERE 子.キー = 親.キー)を呼ぶ」を、「GROUP BY キーで集計表を 1 回作り、それをJOINで親に結合する」に変えるものです。
結果は同じで、計画から相関の再評価が消えます。
-- 書き換え前: 社員ごとの売上合計を相関サブクエリで
SELECT e.emp_id, e.name,
(SELECT SUM(p.amount) FROM perf_sales p
WHERE p.emp_id = e.emp_id) AS total
FROM employee e
ORDER BY e.emp_id;
-- 書き換え後: 1 回の集約を作って JOIN
WITH sales_sum AS (
SELECT emp_id, SUM(amount) AS total
FROM perf_sales
GROUP BY emp_id
)
SELECT e.emp_id, e.name, s.total
FROM employee e
LEFT JOIN sales_sum s ON s.emp_id = e.emp_id
ORDER BY e.emp_id;
NOT IN の NULL の落とし穴 — NOT EXISTS で回避する
「一致する行がない」を問う反結合では、NOT IN (SELECT ...)に落とし穴があります。
内側の集合に NULL が 1 つでも混じると、NOT INは 3 値論理(真・偽・不明の 3 状態で評価する論理)により全体が不明になり、結果が 0 行になってしまいます。
一方NOT EXISTS (SELECT 1 ... WHERE ...)は内側に行があるかだけを問うので、NULL の影響を受けず正しく反結合になります。
反結合を書くときはNOT EXISTSを使う、またはNOT INを使うなら内側でWHERE 列 IS NOT NULLを付けて NULL を除く、のどちらかにします。
本講座ではNOT EXISTSを標準の書き方として使います。
-- 内側の集合に NULL が混じる例を team で作る
-- team(mgr_emp) に NULL を 1 つ入れておく
WITH team(mgr_emp) AS (
VALUES (3), (7), (NULL)
)
-- 落とし穴: NULL があると NOT IN は 0 行になる
SELECT COUNT(*) FROM perf_sales
WHERE emp_id NOT IN (SELECT mgr_emp FROM team);
-- 回避: NOT EXISTS なら NULL に影響されず正しい反結合
WITH team(mgr_emp) AS (
VALUES (3), (7), (NULL)
)
SELECT COUNT(*) FROM perf_sales p
WHERE NOT EXISTS (
SELECT 1 FROM team t WHERE t.mgr_emp = p.emp_id
);
理解度チェック
まずは1問ずつ答えてみましょう。
Q2SELECT リストの相関スカラーサブクエリ(行ごとに(SELECT SUM(...) WHERE 子=親)を呼ぶ)の典型的な書き換えはどれですか。
Q3内側の集合に NULL が混じる可能性があるとき、「一致しない行」を求める反結合の安全な書き方はどれですか。