コマンド道場

IN と EXISTS の使い分け

「該当する行があるか」で絞る

busho部署

busho_idbusho_name
1経営
2開発
6法務

EXISTS(社員が 1 人でもいるか)社員がいる部署

busho_idbusho_name
1経営
2開発

WHERE EXISTS (SELECT 1 FROM shain s WHERE s.busho_id = busho.busho_id)

  • 取り消し線= 条件に合わないので結果に出てこない行
  • 部署 7 行 → 社員がいる部署 61 行が消えます)

7 部署のうち 6 部署が残ります。消えるのは社員が 1 人もいない法務だけです。

「別の表に該当する行があるか」を聞く書き方は 2 つあります。 IN値の集合に含まれるかを、EXISTS行が 1 つでもあるかを聞きます。多くの場合どちらでも同じ結果になりますが、否定したときだけ結果が変わります

未確認の製品があります そのまま使える: PostgreSQL / MySQL / MariaDB / SQLite / SQL Server / Oracle Database

未確認: IBM Db2

製品ごとの対応表(根拠つき)を見る

このページの実行結果と図は、すべて本物のデータベースに同じ SQL を流して得たものです。手で書いた「たぶんこうなる」ではありません。

01同じことを 2 通りで書ける

結論: 「社員がいる部署」は IN でも EXISTS でも書けて、結果は同じです。

- INbusho_id IN (SELECT busho_id FROM shain) 値の集合を作って、その中にあるかを聞く- EXISTSEXISTS (SELECT 1 FROM shain s WHERE s.busho_id = b.busho_id) 条件に合う行が 1 つでもあるかを聞く

EXISTS のほうが長く見えますが、外側の行との対応関係が条件の中に書いてあるぶん、何を聞いているかが読み取りやすい形です。この「外側の行を参照する」書き方を相関と呼びます。

速さについては、どちらが速いかは一概に言えません。多くの製品は両者を同じ実行計画に書き換えるので、まずは読みやすいほうを選んでください。

1 行ずつ、何をしているか
  1. SELECT b.busho_nameどの列を出すか
  2. FROM busho b外側は部署の表
  3. WHERE EXISTS (行が 1 つでもあれば真
  4. SELECT 1 FROM shain s内側は社員の表
  5. WHERE s.busho_id = b.busho_id)★外側の行と結びつける(相関)

最後の行がこの書き方の心臓部です。ここを書き忘れても SQL としては通ってしまうため、結果を見るまで気づけません。

EXISTS で書く
SELECT b.busho_name
FROM busho b
WHERE EXISTS (SELECT 1
              FROM shain s
              WHERE s.busho_id = b.busho_id)
ORDER BY b.busho_id;
busho_name
経営
開発
営業
管理
基盤
マーケ

6

6 部署です。社員が 1 人もいない法務だけが出ません。

IN で書く(結果は同じ 6 部署)
SELECT busho_name
FROM busho
WHERE busho_id IN (SELECT busho_id FROM shain)
ORDER BY busho_id;
busho_name
経営
開発
営業
管理
基盤
マーケ

6

同じ 6 部署です。社員表の busho_id には所属未設定ぶんの NULL が入っていますが、否定していない IN は NULL があっても平気です。

02EXISTS が見ているのは行の有無だけ

結論: EXISTS は内側の SELECT が何を返すかを一切見ません。 行が 1 つでもあれば真、0 行なら偽です。

そのため SELECT 1 と書くのが慣用です。SELECT * でも SELECT name でも結果は変わりません。「値は使わないので、いちばん短いものを置いておく」という意思表示になります。

次の例では内側で文字列を返していますが、結果は SELECT 1 のときとまったく同じです。

⚠️ ここが IN との構造的な違いです。IN内側が返した値そのものを使うので、内側は必ず 1 列でなければなりません。EXISTS にはその制約がありません。

内側が何を返しても結果は同じ
SELECT b.busho_name
FROM busho b
WHERE EXISTS (SELECT 'なんでもよい'
              FROM shain s
              WHERE s.busho_id = b.busho_id)
ORDER BY b.busho_id;
busho_name
経営
開発
営業
管理
基盤
マーケ

6

6 部署です。SELECT 1 のときと 1 行も変わりません。内側の値は捨てられています。

03⚠️ よくある間違い:相関を書き忘れると全部通る

結論: EXISTS の中で外側の行を参照し忘れると、条件が「社員表に行があるか」だけになり、全行が通ります。

内側だけを見ると SELECT 1 FROM shain は 18 行返るので、常に真です。したがって外側の部署は1 つも絞られず 7 部署すべてが返ります。

エラーになりません。 SQL として完全に正しいからです。件数を数えないと気づけません。

さらに危ないのが否定側で、NOT EXISTS で同じ書き忘れをすると常に偽になり、結果が 0 行になります。「該当なし」という、もっともらしい答えが返ります。

見分け方は 1 つ。 EXISTS の中に外側の表の列が出てくるかを目で確かめてください。出てこなければ、それは相関していません。

相関を書き忘れると 7 部署すべて通る
SELECT b.busho_name
FROM busho b
WHERE EXISTS (SELECT 1 FROM shain s)
ORDER BY b.busho_id;
busho_name
経営
開発
営業
管理
基盤
法務
マーケ

7

社員が 1 人もいない法務まで出ています。内側に b.busho_id が出てこないことに注目してください。

否定側だと 0 行になる
SELECT b.busho_name
FROM busho b
WHERE NOT EXISTS (SELECT 1 FROM shain s)
ORDER BY b.busho_id;
busho_name

0

1 行も返りません。「社員が 1 人もいない部署は無い」と読めてしまいますが、実際には法務が該当します。

04⚠️ よくある間違い:否定するときは NOT EXISTS を選ぶ

結論: 「該当が無い行」を出したいときは NOT EXISTS を使ってください。 NOT IN は NULL 1 つで結果が 0 行になります。

社員表の busho_id には、所属が未設定の社員 2 人ぶんの NULL が入っています。この列を NOT IN に渡すと、エラーも警告も出ずに 0 行が返ります。

仕組みは NOT IN<> AND <> AND … に展開されることによるもので、詳しくは下の「関連するトピック」の IN のページで扱っています。ここで押さえるべきは選択の基準です。

  • 否定するなら NOT EXISTS … NULL の影響を受けない
  • 否定しないなら好きなほうIN でも EXISTS でも同じ結果になる

NOT IN を使ってよいのは、その列に NULL が入りえないと分かっているときだけです。「今は入っていない」ではなく「制約で入らない」まで確かめてください。

NOT EXISTS なら正しく出る
SELECT b.busho_name
FROM busho b
WHERE NOT EXISTS (SELECT 1
                  FROM shain s
                  WHERE s.busho_id = b.busho_id)
ORDER BY b.busho_id;
busho_name
法務

1

法務が 1 行返ります。これが期待する答えです。

NOT IN だと 0 行
SELECT busho_name
FROM busho
WHERE busho_id NOT IN (SELECT busho_id FROM shain);
busho_name

0

同じことを聞いているのに 1 行も返りません。社員表の busho_id に NULL が混ざっているためです。

05結合との使い分け

結論: 「あるかどうか」だけを知りたいなら EXISTS、相手の列も出したいなら結合です。

EXISTS は行を増やしません。外側の 1 行に対して内側が何行一致しても、結果はその 1 行のままです。

一方、内部結合で同じことを書くと、一致した数だけ行が増えます。「社員がいる部署」を結合で出すと、部署が社員の人数ぶん重複します。DISTINCT で潰すこともできますが、それは増やしてから減らしているので、意図が読み取りにくくなります。

  • 存在の確認だけ → EXISTS(行数が変わらない)
  • 相手の列も欲しい → 結合(行が増えるのは当然)

次の例は外部結合で人数を数えたものです。法務が 0 人であることが数字で確かめられます。

人数まで知りたいなら結合
SELECT b.busho_name,
       count(s.shain_id) AS ninzu
FROM busho b
  LEFT JOIN shain s ON s.busho_id = b.busho_id
GROUP BY b.busho_id, b.busho_name
ORDER BY b.busho_id;
busho_nameninzu
経営1
開発4
営業4
管理3
基盤2
法務0
マーケ2

7

法務が 0 人だと数字で分かります。EXISTS では「いる/いない」しか分かりません。目的で使い分けてください。

自分で打ってみる

このページの例と同じデータが入った本物のデータベースを、この場(あなたのブラウザの中)で起動できます。打った SQL がサーバーへ送られることはありません。

押すと初回だけ約 9MB を読み込みます(2 回目以降はキャッシュされます)。

解いてみる

読んだだけでは書けるようになりません。実行結果で採点します。

関連するトピック

根拠(一次情報)