Postgres / SQL: サブクエリの NOT IN は NULL が1つあると0件になる。NOT EXISTS を使う
x NOT IN (SELECT ...) は、サブクエリの結果に NULL が1行でも混ざると、どの行に対しても真にならず結果が0件になる。除外条件は NOT EXISTS で書く。
なぜ起きるか
x NOT IN (1, NULL) は x <> 1 AND x <> NULL と同じ意味になる。x <> NULL は常に NULL(不明)なので、全体は「偽」か「NULL」にしかならず、WHERE を通らない。
sql
-- 「注文したことがないユーザー」を探すつもり
SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM orders);
-- orders.user_id に NULL が1行でもあると 0 件(エラーにもならない)ゲスト注文などで user_id が NULL になる行が後から入ると、ある日突然クエリが何も返さなくなる。テストデータに NULL が無いと気づけない。
やり方
sql
SELECT * FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);NOT EXISTSは「一致する行があるか」だけを見るので、NULL の行は単に一致しないだけで結果を壊さない。- Postgres では
NOT EXISTSは anti-join(Hash Anti Join など)として計画されることが多く、速度面でも有利なことが多い。NOT IN (サブクエリ)は NULL の扱いのせいで anti-join に変換できず、遅いプランになりやすい。 LEFT JOIN ... WHERE o.user_id IS NULLでも同じ結果になる。
注意点
- 値のリストを直書きする
NOT IN (1, 2, 3)は、リストに NULL を入れない限り問題ない。危ないのはサブクエリや、アプリから組み立てた配列に NULL が混ざる場合。 - どうしても
NOT INを使うなら、サブクエリ側にWHERE user_id IS NOT NULLを付ける。 INのほうは NULL が混ざっても一致する行は返るので、この問題はNOT IN特有。