Day9 前半のゴール
「EXISTS と IN を“なんとなく同じ”から卒業する」
今日はサブクエリの中でも、実務で頻出のテーマ
EXISTS と IN
を扱います。
多くの人が最初こう思います。
「EXISTS でも IN でも、書き方が違うだけでしょ?」
ところが、
パフォーマンス・意味・書きやすさの面で、
ちゃんと使い分けられるとSQLの質が一段上がります。
Day9 前半のゴールは、
EXISTS と IN の基本的な書き方を理解する
「どちらでも書けるけど、考え方が違う」ことを知る
“存在チェック”なら EXISTS という感覚を持つ
ここまで行ければ、後半の「どっちが速いか」「どう最適化されるか」が入ってきます。
まずは題材テーブルを決める
「users と orders で“注文があるユーザー”を取りたい」
いつもの2テーブルを使います。
users
ユーザー情報(id, name, email など)
orders
注文情報(id, user_id, total など)
やりたいことはシンプルです。
「1件以上の注文を持っているユーザーだけを取得したい」
これを、EXISTS と IN の両方で書いてみます。
IN を使った書き方
「“値の集合に含まれているか”という発想」
まずは IN からいきます。
SELECT
u.id,
u.name
FROM users u
WHERE u.id IN (
SELECT o.user_id
FROM orders o
);
SQLこのSQLは、
サブクエリで「orders に登場する user_id の集合」を作る
users.id がその集合の中に含まれているユーザーだけを残す
という意味です。
頭の中のイメージとしては、
orders から user_id を全部集めて
→ そのリストの中に users.id があるかどうかをチェック
という動きです。
SQLiteでも IN はよく使っていたと思いますが、
MySQLでも基本の考え方は同じです。
EXISTS を使った書き方
「“対応する行が1件でも存在するか”という発想」
同じ条件を、EXISTS で書き直してみます。
SELECT
u.id,
u.name
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);
SQLこのSQLは、
users の各行について
「orders に user_id = u.id の行が1件でも存在するか?」をチェックする
という意味です。
サブクエリの中身は、
SELECT 1
FROM orders o
WHERE o.user_id = u.id
となっていますが、
ここで SELECT しているのが 1 であることに意味はありません。
EXISTS は「行が1件でも返ってくるかどうか」だけを見ていて、
返ってきた値の中身には興味がないからです。
IN と EXISTS の“考え方の違い”
「集合に属するか vs 対応する行が存在するか」
同じ「注文があるユーザー」を取るSQLでも、
IN と EXISTS では発想が違います。
IN の発想
「orders.user_id の集合を作って、その集合に users.id が含まれているかを見る」
EXISTS の発想
「users の1行ごとに、orders に対応する行が1件でもあるかを聞く」
結果としては同じユーザーが返ってきますが、
頭の中のモデルが違う、というのがポイントです。
この違いが、
パフォーマンスや書きやすさに影響してきます。
なぜ EXISTS で SELECT 1 と書くのか
「EXISTS は“行の有無”だけを見るから」
EXISTS のサブクエリで、よくこう書かれます。
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);
SQL「1って何?idじゃなくていいの?」と感じるかもしれません。
答えはシンプルで、
EXISTS は「1行でも返ってくるかどうか」しか見ていない
→ 何をSELECTしても結果は同じ
だからです。
極端な話、こう書いても意味は同じです。
SELECT 999
FROM orders o
WHERE o.user_id = u.id
SQLでも、慣習的に「中身はどうでもいいよ」という意思表示として、SELECT 1 と書くことが多いです。
ここで押さえたいのは、
EXISTS は「存在チェック専用の構文」
→ 値そのものには興味がない
という性質です。
「どちらでも書ける」典型パターン
「“注文があるユーザー”は IN でも EXISTS でも書ける」
さっきの例をもう一度並べます。
IN 版:
SELECT
u.id,
u.name
FROM users u
WHERE u.id IN (
SELECT o.user_id
FROM orders o
);
SQLEXISTS 版:
SELECT
u.id,
u.name
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);
SQLどちらも、
orders に1件以上の注文を持つユーザーだけを返す
という意味で、結果は同じです。
Day9 前半では、
「このパターンは IN でも EXISTS でも書ける」
「でも、EXISTS は“存在チェックしてる感”が強い」
という感覚を持っておけばOKです。
SQLite と MySQL での“サブクエリの温度差”
「SQLiteではJOINで書き直していたものを、MySQLでは素直に使ってよい」
SQLite では、
サブクエリの最適化がそこまで強くなかったり、
EXPLAINを見て細かくチューニングする機会が少なかったりして、
「IN や EXISTS より JOIN で書き直した方が分かりやすい」
と感じていたかもしれません。
MySQL では、
サブクエリの最適化がかなり進んでいる
EXISTS / IN / JOIN を内部で書き換えることもある
という世界観です。
Day9 では、
「EXISTS は存在チェックのための構文」
「IN は“集合に属するか”を見る構文」
という役割を意識しながら、
どちらで書くと意図が伝わりやすいか、を考えていきます。
EXISTS を選びたくなる典型的な場面
「“とにかく存在するかだけ知りたい”とき」
前半の締めとして、
「こういうときは EXISTS を選びたい」という典型を1つだけ挙げておきます。
例えば、
「不正アクセスの疑いがあるIPアドレスからのログインが1件でもあるユーザーを抽出したい」
というケース。
SELECT
u.id,
u.name
FROM users u
WHERE EXISTS (
SELECT 1
FROM login_logs l
WHERE l.user_id = u.id
AND l.ip = '192.0.2.123'
);
SQLこのとき、欲しいのは
「そのIPからのログインが何件あるか」ではなく
「1件でもあるかどうか」
です。
こういう「存在チェック」のとき、
EXISTS は読み手にとっても意図が分かりやすく、
MySQLにとっても最適化しやすい形になります。
Day9 前半のまとめ
IN は「サブクエリが返す“値の集合”に、左側の値が含まれているか」を見る構文であり、WHERE u.id IN (SELECT o.user_id FROM orders o) のように書くと、「orders に登場する user_id のリストの中に users.id があるユーザーだけを残す」という意味になる。
EXISTS は「サブクエリが1行でも返すかどうか(存在するかどうか)」だけを判定する構文であり、WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id) のように書くと、「users の各行について、orders に user_id = u.id の行が1件でも存在するユーザーだけを残す」という意味になる。
同じ「注文があるユーザー」を取る場合でも、IN は“集合に属するか”という発想、EXISTS は“対応する行が存在するか”という発想で書いており、結果は同じでも頭の中のモデルが違うため、「存在チェックをしている」と明示したいときには EXISTS の方が意図が伝わりやすい。
EXISTS のサブクエリで SELECT 1 と書くのは、EXISTS が「行の有無」しか見ておらず、返される値の中身には興味がないためであり、「不正IPからのログインが1件でもあるユーザー」のような“とにかく存在するかだけ知りたい”ケースでは、EXISTS を使うのが自然な選択になる。
後半では、
IN と EXISTS がパフォーマンス的にどう違うか、
MySQL が内部でどう最適化するか、
そして「JOINで書き直した方がよいケース」との境界線を、
具体的なパターンで掘り下げていきます。
