こんにちは、おうどんです🍜
うどん屋さんの顧客データベース。 「まだ一度も注文していないお客さん」に、初回クーポンを送りたい。
SQL を書く。 NOT IN でサクッと。 昨日までは、ちゃんと1人出てきた。
今日。 実行する。
0件。
……待って。 お客さん、減ってないよ? 注文テーブルに、伝票が1枚増えただけだよ?🤔
説明用の架空の場面ですが、SQL を書いていると一度は踏む地雷です。
犯人は、その1枚。 お客さんの番号が書かれていない伝票、つまり customer_id が NULL(値が入っていない・不明という印)の行です。
名無しの伝票「お客さん番号? わかりません」
NOT IN「わからない人がいるなら、誰のことも『いない』とは言えません」
……いや、そこまで慎重にならなくても。 でも SQL 的には、これが正しい動きなんです。
今回は、NOT IN が NULL で0件になる理由と、NOT EXISTS・LEFT JOIN を使った安全な書き方をまとめます。
結論:まずはこれだけ
先に答えを置いておくと、サブクエリに NULL が1つでも混ざると、NOT IN は1行も返さないんです。
NOT IN (サブクエリ)のサブクエリが NULL を1つでも返すと、結果は0件になる(エラーは出ない)- 「〇〇に存在しないもの」を探すときは
NOT EXISTSで書く - どうしても
NOT INを使うなら、サブクエリにWHERE 列 IS NOT NULLを付ける
エラーが出ないなら、NULL 1個くらい見逃してくれても……
見逃してくれません。 しかも、黙って0件を返すタイプです。 うどん屋さんで「本日の営業は終了しました」の札だけ出して、店の中で店主が普通にうどんを打っている。 そういう感じ。
じゃあ NULL を全部消せば解決では?
それは名無しの伝票を、売上ごとシュレッダーにかける作戦です。 帳簿が合わなくなって、今度は経理さんが0件になります。
実際に試してみる
今回の検証環境(Windows 10 Pro / Python 3.13.1 / SQLite 3.45.3)で、Python 標準の sqlite3 モジュールを使って確かめます。 データベースはメモリ上に作るので、ファイルは残りません。
# h1.py
import sqlite3
con = sqlite3.connect(":memory:")
con.executescript("""
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, menu TEXT);
INSERT INTO customers VALUES (1, 'kitsune'), (2, 'tanuki'), (3, 'tsukimi');
INSERT INTO orders VALUES (1, 1, 'kitsune udon'), (2, 2, 'tanuki udon');
""")
q = "SELECT name FROM customers WHERE id NOT IN (SELECT customer_id FROM orders)"
print("before:", con.execute(q).fetchall())
con.execute("INSERT INTO orders VALUES (3, NULL, 'kake udon')")
print("after :", con.execute(q).fetchall())
before: [('tsukimi',)]
after : []
お客さんは kitsune・tanuki・tsukimi の3人。 注文したのは kitsune と tanuki なので、最初は tsukimi さんが出てきます。
そこへ、お客さん番号なしの「かけうどん」の伝票を1枚足す。 それだけで、tsukimi さんが消えました。
伝票が1枚増えて、お客さんが1人減る。 等価交換……いや、交換してない。一方的に消されてる。
tsukimi さん、かけうどんを食べて帰ったのでは?
いえ、それは名無しの伝票です。 tsukimi さんは、今日も家でお月見をしています。
なぜ0件? NOT IN は「<> を AND でつないだもの」
からくりは、NOT IN を分解すると見えてきます。
Oracle の SQL リファレンス(IN 条件)には、ずばりこの例が載っています。 department_id NOT IN (10, 20, NULL) は、次の条件と同じ意味になる、と。
department_id != 10 AND department_id != 20 AND department_id != null
そして最後の != null が UNKNOWN(不明)になるので、1行も返らない。 リファレンスには「サブクエリを使うときは特に見落としやすい」という注意まで書かれています。
今回のうどん屋さんで言うと、tsukimi さん(id=3)の判定はこうなります。
# h3.py
import sqlite3
con = sqlite3.connect(":memory:")
row = con.execute("""
SELECT 3 <> 1, 3 <> 2, 3 <> NULL,
3 NOT IN (1, 2, NULL),
3 IN (1, 2, NULL), 1 IN (1, 2, NULL)
""").fetchone()
print(row)
(1, 1, None, None, None, 1)
SQLite では真を 1、偽を 0 で返し、NULL は Python 側で None として受け取ります。
3 <> 1→ 1(真)3 <> 2→ 1(真)3 <> NULL→ NULL(不明)3 NOT IN (1, 2, NULL)→ NULL(不明)
「3 は NULL と違いますか?」と聞かれても、NULL は「値がわからない」印なので、SQL は「わかりません」と答えます。 PostgreSQL のドキュメント(比較演算子)にも、普通の比較演算子は、どちらかが NULL なら真でも偽でもなく NULL(unknown)になる、と書かれています。7 = NULL も 7 <> NULL も NULL です。
名無しの伝票「わかりません」
NOT IN「わかりません」
WHERE 句「わかりません、は通しません」
わかりませんの伝言ゲーム。。。
最後の WHERE 句の部分は、Oracle のリファレンス(Nulls)にこう書かれています。UNKNOWN になる条件はほぼ FALSE のように働き、WHERE 句の条件が UNKNOWN なら、その行は返らない。
つまり WHERE 句は、真の行だけを通すんです。 偽も不明も、門前払い。 お店の入口で「たぶんお客さんです」と言っても入れてもらえない。厳しい。
ちなみに、IN のほうは右端の 1 IN (1, 2, NULL) が 1(真)です。 IN は「どれか1つと等しければ真」なので、等しい値が見つかれば NULL がいても平気なんです(なぜ平気なのかは上級者向けのところで)。
直し方は3つ。おすすめは NOT EXISTS
ここは結論から。NOT EXISTS なら、NULL の伝票に振り回されません。
名無しの伝票を入れたままの状態で、3つの書き方を試しました。 ついでに、NULL が何件あるかも数えています。
# h2.py
import sqlite3
con = sqlite3.connect(":memory:")
con.executescript("""
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, menu TEXT);
INSERT INTO customers VALUES (1, 'kitsune'), (2, 'tanuki'), (3, 'tsukimi');
INSERT INTO orders VALUES (1, 1, 'kitsune udon'), (2, 2, 'tanuki udon'),
(3, NULL, 'kake udon');
""")
print("NULL rows in orders:", con.execute(
"SELECT count(*) FROM orders WHERE customer_id IS NULL").fetchone()[0])
queries = {
"A NOT IN + IS NOT NULL": """
SELECT name FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders
WHERE customer_id IS NOT NULL)""",
"B NOT EXISTS": """
SELECT name FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o
WHERE o.customer_id = c.id)""",
"C LEFT JOIN + IS NULL": """
SELECT c.name FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL""",
}
for label, sql in queries.items():
print(label, "->", con.execute(sql).fetchall())
NULL rows in orders: 1
A NOT IN + IS NOT NULL -> [('tsukimi',)]
B NOT EXISTS -> [('tsukimi',)]
C LEFT JOIN + IS NULL -> [('tsukimi',)]
3つとも tsukimi さんが帰ってきました。おかえりなさい🍵
A. NOT IN のサブクエリから NULL を除く いちばん手早い応急処置です。 ただし、サブクエリを直すたびに IS NOT NULL を付け忘れないか気にし続けることになります。
B. NOT EXISTS で書く(おすすめ) NOT EXISTS (サブクエリ) は「サブクエリが1行も返さなければ真」という意味です。 PostgreSQL のドキュメント(サブクエリ式)にあるとおり、EXISTS が見るのは行が返るかどうかだけで、行の中身は見ません。 名無しの伝票は o.customer_id = c.id に当てはまらないので、最初から話に入ってこないんです。
名無しの伝票「わかりません」
NOT EXISTS「聞いてないです。tsukimi さんの伝票があるかどうかだけ見てるので」
会話に入れてもらえない名無しの伝票、ちょっとかわいそう。。。
C. LEFT JOIN して、相手がいない行を拾う LEFT JOIN(左の表の行を全部残し、右の表に相手がいなければ右側の列を NULL で埋める結合)を使う書き方です。 右側の列が NULL になった行が「注文がないお客さん」。 ただし、どの列で IS NULL を見るかに落とし穴があります(中級者向けで説明します)。
じゃあ A でいいや。1行足すだけだし。
その1行を、半年後の自分が消します。 「この IS NOT NULL いらなくない? customer_id に NULL なんて入らないでしょ」と言いながら。 おうどんなら、たぶんやります。
🔰 ここまで読めば今日から困らない
NOT IN (サブクエリ)は、サブクエリに NULL が1つでもあると0件になる- 理由は、NOT IN が
<>を AND でつないだもので、NULL との比較が「わからない」になるから - 「存在しないもの」を探すなら
NOT EXISTSで書く - 急ぎなら、サブクエリに
WHERE 列 IS NOT NULLを足す
名無しの伝票が何枚あっても、もう0件で慌てなくて大丈夫です。
4行なら覚えられる。3行目だけ覚えておけばいいよね?
それ、2行目を忘れて、来月また0件で泣くやつです。 理由を覚えている人だけが、別の形で出てきた NULL に気づけます。
じゃあ4行とも付箋に書いてモニターに貼っておく。
モニターが付箋で見えなくなる未来まで見えました。 1枚だけにしましょう。「NOT IN を見たら NULL を疑え」。
ここから先は中級者向け。読み飛ばしてもOKです。
中級:アプリから渡すリストにも NULL は混ざる
サブクエリだけが犯人とは限りません。アプリ側で作った値のリストに None が1個まぎれても、同じことが起きます。
たとえば「除外したいお客さん番号」を画面やファイルから集めて、NOT IN (?, ?, ?) に渡すケース。 ? はプレースホルダ(あとから値を差し込む場所)で、Python の None は SQL の NULL として渡されます。
# h4.py
import sqlite3
con = sqlite3.connect(":memory:")
con.executescript("""
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO customers VALUES (1, 'kitsune'), (2, 'tanuki'), (3, 'tsukimi');
""")
ng_ids = [1, 2, None] # アプリ側で作ったリストに None が混ざった
sql = "SELECT name FROM customers WHERE id NOT IN (?, ?, ?)"
print("with None :", con.execute(sql, ng_ids).fetchall())
ids = [i for i in ng_ids if i is not None]
marks = ", ".join("?" * len(ids))
sql = f"SELECT name FROM customers WHERE id NOT IN ({marks})"
print("None removed:", con.execute(sql, ids).fetchall())
with None : []
None removed: [('tsukimi',)]
None 入りのリストでは0件。 取り除いてから渡すと、tsukimi さんが出てきました。
画面の入力欄が1つ空欄だっただけで、全員が除外される。 「空欄=誰も指定していない」のつもりが、SQL には「正体不明の人を1人指定した」と伝わっているんです。
正体不明の人を除外すると、全員が除外される……?
そう、正体不明なので「あなたはその人じゃないですよね?」に誰も答えられないんです。 覆面の人が1人いるだけで、全員が容疑者。サスペンスの冒頭みたい。
なお、リストが空になったときの扱いは DB によって違います。 SQLite のドキュメントによると、SQLite は NOT IN () のような空のリストを許しますが、ほかの多くの DB や SQL92 規格では少なくとも1つの要素が必要です。 空リストになりうるなら、その場合は条件ごと外す分岐をアプリ側に入れておきましょう。
SQLite で動いたから本番の DB でも大丈夫でしょ。
テストは SQLite、本番は別の DB。 手元では通ったのに、本番でだけ構文エラー。 家ではおいしく茹でられたのに、お店の釜だと全部のびる。環境が違えば結果も違います。
中級:LEFT JOIN で「どの列が NULL か」を間違える
LEFT JOIN の書き方には、見る列を間違えると、ちゃんと注文したお客さんまで拾ってしまう罠があります。
今度は、tanuki さんの伝票のメニュー欄が空(NULL)だった、という状況にします。 注文はしているけど、何を頼んだかの記録が抜けているケースです。
# h7.py
import sqlite3
con = sqlite3.connect(":memory:")
con.executescript("""
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, menu TEXT);
INSERT INTO customers VALUES (1, 'kitsune'), (2, 'tanuki'), (3, 'tsukimi');
INSERT INTO orders VALUES (1, 1, 'kitsune udon'), (2, 2, NULL),
(3, NULL, 'kake udon');
""")
print("o.menu IS NULL:", con.execute("""
SELECT c.name FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.menu IS NULL""").fetchall())
print("o.id IS NULL :", con.execute("""
SELECT c.name FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL""").fetchall())
print("EXCEPT :", con.execute("""
SELECT id FROM customers
EXCEPT
SELECT customer_id FROM orders""").fetchall())
o.menu IS NULL: [('tanuki',), ('tsukimi',)]
o.id IS NULL : [('tsukimi',)]
EXCEPT : [(3,)]
(3行目の EXCEPT は上級者向けのところで使います)
o.menu IS NULL で判定すると、tanuki さんまで「注文なし」に入ってしまいました。 LEFT JOIN で右側が NULL になるのは「相手がいなかった」ときだけじゃなく、「相手はいたけど、その列が元から NULL だった」ときもあるからです。
判定には、NULL になりえない列(主キーや、ON 句で結合に使った列)を使いましょう。 今回は主キーの o.id にしたら、tsukimi さんだけになりました。
全部の列で IS NULL を見れば確実では?
AND でつなげば一応動きますが、列が20個ある表で20個並べることになります。 それはもう WHERE 句ではなく、写経です。主キー1個で済ませましょう。
tanuki さん「頼みましたよ、たぬきうどん」
SQL「記録がないので、初回クーポンをどうぞ」
tanuki さん「……もらっておきます」
もらうんかい。
中級:0件が出たときの切り分け手順
NOT IN が怪しいと思ったら、次の順で見ていきます。
- サブクエリだけを単独で実行して、NULL が混ざっていないか見る(
SELECT count(*) FROM orders WHERE customer_id IS NULLのように数える) - 値のリストをアプリで組み立てているなら、組み立てたリストをログに出して
None・nullが入っていないか見る - 見つかったら、その場しのぎで
IS NOT NULLを足すか、NOT EXISTS に書き換える - ついでに、その列に NULL が入ってよいのかを考える。入ってはいけないなら、NOT NULL 制約を付けるのがいちばん確実
手順0は「とりあえず DB を再起動」ですよね?
再起動しても、名無しの伝票は名無しのままです。 データは、再起動で名前を思い出したりしません。
4番目が大事なんです。 名無しの伝票を「書き方でよける」のではなく、「そもそも名無しで受け付けない」。
おうどん「じゃあ明日から、名前を書かないお客さんには、うどんを出しません!」
……それはお店がつぶれます。 制約を付けるのは、本当に NULL が入ってはいけない列だけです。
店頭のお客さんは名前を聞かないから、NULL じゃないと困るんだけど……
それなら NULL は正しいデータです。 その場合は、NULL がある前提で NOT EXISTS を使うのが正解。 名前を言わないお客さんにも、うどんは出す。それが商売です。
ここから先は上級者向け。読み飛ばしてもOKです。
上級:三値論理で見る NOT IN と IN の非対称
SQL の条件は、真・偽・不明の3つの値をとります。これを三値論理(2つではなく3つの真理値で考える論理)と呼びます。 不明がからんだときの AND・OR・NOT を実際に見てみます。
# h5.py
import sqlite3
con = sqlite3.connect(":memory:")
print("sqlite", sqlite3.sqlite_version)
for expr in ["NULL AND 0", "NULL AND 1", "NULL OR 1", "NULL OR 0",
"NOT 0", "NOT NULL"]:
print(f"{expr:<11} ->", con.execute(f"SELECT {expr}").fetchone()[0])
sqlite 3.45.3
NULL AND 0 -> 0
NULL AND 1 -> None
NULL OR 1 -> 1
NULL OR 0 -> None
NOT 0 -> 1
NOT NULL -> None
読み方はこうです。
- 不明 AND 偽 → 偽(片方が偽なら、もう片方が何でも偽)
- 不明 AND 真 → 不明
- 不明 OR 真 → 真(片方が真なら、もう片方が何でも真)
- 不明 OR 偽 → 不明
- NOT 不明 → 不明
ここから、IN と NOT IN の差がはっきりします。
x IN (a, b, NULL)はx = a OR x = b OR x = NULL。どれか1つが真なら全体が真なので、等しい値が見つかった行は NULL に邪魔されないx NOT IN (a, b, NULL)はx <> a AND x <> b AND x <> NULL。最後が必ず不明になるので、全体は偽か不明のどちらか。真には絶対にならない
真にならない条件は、WHERE 句を1行も通過できません。 これが0件の正体です。
そして NOT 不明 → 不明 なので、NOT (id IN (...)) と書き換えても助かりません。 Oracle のリファレンス(Nulls)でも、NOT FALSE は TRUE だが NOT UNKNOWN は UNKNOWN、と明記されています。
じゃあ NOT を2回つければ……
NOT NOT 不明 も不明です。 NOT を何回重ねても、名無しの伝票に名前は書かれません。 写経しても名前は浮かび上がってこないんです。
PostgreSQL のドキュメント(サブクエリ式)の NOT IN の項にも、「等しい値がなく、右側に NULL の行が1つでもあれば、結果は true ではなく null になる」と書かれています。SQL の NULL の通常の規則どおり、とのこと。 つまりこれはバグではなく仕様です。仕様なんです。泣いても仕様。。。
不具合報告を出したら直してもらえるかも。
「NULL と比べたら不明になります」と報告したら、「はい、そう書いてあります」と返ってきて終わりです。 マニュアルに書いてあることを報告するのは、メニューに「かけうどん」と書いてある店で「かけうどんがあります!」と叫ぶのと同じ。
上級:NOT IN と NOT EXISTS は、実は同じ意味じゃない
NOT IN を NOT EXISTS に書き換えると、左側が NULL の行の扱いが変わることがあります。 あわせて、サブクエリが空のときの動きも見ておきましょう。 書き換えで「直した」つもりが、別の差を持ち込むこともあるので、ここは押さえておきたいところ。
# h6.py
import sqlite3
con = sqlite3.connect(":memory:")
con.executescript("""
CREATE TABLE a (x INTEGER);
CREATE TABLE b (y INTEGER);
INSERT INTO a VALUES (1), (NULL);
INSERT INTO b VALUES (2);
""")
print("NOT IN :", con.execute(
"SELECT x FROM a WHERE x NOT IN (SELECT y FROM b)").fetchall())
print("NOT EXISTS :", con.execute(
"SELECT x FROM a WHERE NOT EXISTS (SELECT 1 FROM b WHERE b.y = a.x)").fetchall())
print("empty sub :", con.execute(
"SELECT x FROM a WHERE x NOT IN (SELECT y FROM b WHERE 0)").fetchall())
NOT IN : [(1,)]
NOT EXISTS : [(1,), (None,)]
empty sub : [(1,), (None,)]
1. 左側が NULL の行 表 a の NULL の行は、NOT IN では消え、NOT EXISTS では残りました。 NOT IN では NULL <> 2 が不明になるので落ちます。 NOT EXISTS では b.y = NULL に当てはまる行がないので、「存在しない」が真になって残ります。
さっきのうどん屋さんで言えば、「お客さん番号が NULL のお客さん」が customers 側にいたら、NOT EXISTS 版はその人をクーポン対象に含めます。 主キーの id は NULL にならないので今回は起きませんが、主キー以外の列で突き合わせるときは要注意です。
名前のないお客さんにもクーポンが届くなら、それは優しさでは?
届け先がわからないクーポンは、ただの紙です。 優しさが宛先不明で戻ってきます。
2. サブクエリが空のとき この場合は、同じ空の集合を対象にした NOT EXISTS も全行を返すので、両者の結果は同じです。 サブクエリが1行も返さないと、NOT IN は左側が NULL の行も含めて全部返しました。 SQLite のドキュメントには、右側が空の集合なら、左側が NULL でも IN は偽、NOT IN は真になる、と書かれています。 Oracle のリファレンス(IN 条件)にも、行を返さないサブクエリを参照する NOT IN は全行を返す、という例があります。
つまり NOT IN は、
- 右側が空 → 全員通す
- 右側に NULL が1つ → 誰も通さない
という、極端な門番なんです。
誰も来なければ全員入れて、覆面が1人来たら店を閉める……
たぶん、昔なにかあったんだと思います。 優しくしてあげてください。
使い分けの目安としては、
- 左側の列が NULL になりえない(主キーなど)なら、NOT EXISTS と
IS NOT NULL付きの NOT IN は同じ結果になる - 左側が NULL の行を「残したい」なら NOT EXISTS、「落としたい」なら条件に
x IS NOT NULLを明示する
どちらにするにしても、「NULL の行をどうしたいか」を仕様として先に決めるのが先です。
上級:NULL を「値」として比べたいとき
逆に、NULL 同士を「同じもの」として扱いたい場面もあります。 そのための演算子が DB ごとに用意されています。
# h8.py
import sqlite3
con = sqlite3.connect(":memory:")
print(con.execute("""
SELECT NULL <> 1, NULL IS NOT 1, NULL IS DISTINCT FROM 1,
NULL IS NOT NULL, 3 IS NOT 1
""").fetchone())
(None, 1, 1, 0, 1)
<> だと NULL(不明)ですが、SQLite の IS NOT と IS DISTINCT FROM は 1(真)を返しました。 NULL IS NOT NULL は、NULL 同士なので 0(偽)です。
SQLite のドキュメントによると、IS・IS NOT は =・!= と同じように動くが、NULL がからむときは NULL を返さず、必ず真か偽になります。 また、IS NOT という短い書き方は SQLite の拡張で、ほかの多くの DB では IS DISTINCT FROM を使う必要がある、とも書かれています。 PostgreSQL のドキュメント(比較演算子)でも、IS DISTINCT FROM は NULL を普通の値のように扱う述語として説明されています。
名無しの伝票同士を「同じ人」とみなすかどうか。 これは技術の問題というより、お店の方針の問題です。
名無しの伝票は全部同じ人、ってことにすれば集計が楽では?
その人、1日に47杯食べたことになります。 常連どころじゃない。
もうひとつ、h7.py の3行目で試した EXCEPT(左の結果から、右の結果にある行を引く集合演算)も、今回の SQLite では右側に NULL があっても 3 を返しました。 ただし、EXCEPT は両側で同じ列の並びを比べるので、名前ではなく番号の差を取ってから名前を引き直す形になります。NOT EXISTS の代わりにそのまま使えるとは限らない点と、ほかの DB では試していない点に注意してください。
Oracle では空文字も NULL になる
最後に Oracle を使う人向けの注意です(今回は Oracle では未実行で、ドキュメントに基づく説明です)。
Oracle のリファレンス(Nulls)には、Oracle は長さ0の文字列(空文字 '')を NULL として扱う、と書かれています。 つまり Oracle では、NOT IN ('A', '') のように空文字を並べたつもりでも、それは NULL を並べたのと同じことになり、IN 条件のリファレンスの説明どおり1行も返らないはずです。
空文字をリストに入れる。 他の DB では「空文字は除外」の意味。 Oracle では「店じまい」の意味。
同じ SQL で、方言がきつすぎる。 同じ「空っぽ」でも、店によって意味が違う。関西と関東でうどんの汁の色が違うくらいの差です。
チェックリスト
NOT IN (サブクエリ)を見つけたら、サブクエリの列に NULL が入りうるか確認した- 「存在しないもの」を探す条件は、
NOT EXISTSで書いた NOT INを残すなら、サブクエリにWHERE 列 IS NOT NULLを付けた- アプリで組み立てる値のリストから
None・nullを取り除いた(空リストのときの分岐も入れた) - LEFT JOIN で相手がいない行を探すときは、主キーなど NULL にならない列で
IS NULLを判定した - 左側の列が NULL の行を残すか落とすか、仕様として決めた
- NULL が入ってはいけない列には、NOT NULL 制約を付けた
7個もある。全部チェックしたら、今日の仕事が終わる。
大丈夫、1個目だけでも今日の0件は防げます。 残りの6個は、明日のおうどんに任せましょう。
……明日のおうどん「聞いてない」
まとめ
冒頭では、伝票が1枚増えただけで、初回クーポンの対象者が0人になっていました。
NOT IN は、嘘をついていたわけではありません。 名無しの伝票に「わかりません」と言われて、正直に「じゃあ誰についても断言できません」と答えただけ。 正直すぎて、仕事にならなかったんです。
正直者が損をする。SQL の世界も世知辛い。
いや、損をしたのはクーポンをもらえなかった tsukimi さんです。 NOT IN はむしろ、ちょっと胸を張っています。
NOT IN は、NULL が1つでも混ざると誰も通さない門番です。 「存在しないもの」を探すなら、行があるかどうかだけを見る NOT EXISTS に任せましょう。
おうどん「よし、全部の SQL を今夜中に NOT EXISTS に書き換える!」
……それ、明日の朝に別の0件を生むやつです。 書き換えは1か所ずつ、結果を比べながら。
まずは、手元の SQL から NOT IN (SELECT を1か所だけ検索して、サブクエリの列に NULL が入りうるか見てみてください。 入らないと分かったら、今夜は安心して月見うどんです🍜
参考資料
- Oracle AI Database SQL Language Reference 26:IN Condition — NOT IN のリストに NULL があると行が返らないこと、
!=を AND でつないだ形への展開例、行を返さないサブクエリなら全行が返ること - Oracle AI Database SQL Language Reference 26:Nulls — UNKNOWN の条件は WHERE 句で行を返さないこと、NOT UNKNOWN は UNKNOWN、長さ0の文字列を NULL として扱うこと
- PostgreSQL 18 Documentation:Subquery Expressions — EXISTS は行が返るかだけを見ること、IN・NOT IN で NULL がからむと結果が null になること
- PostgreSQL 18 Documentation:Comparison Functions and Operators — 比較演算子は NULL がからむと null を返すこと、
IS NULLとIS DISTINCT FROM - SQLite:SQL Language Expressions — IN・NOT IN の結果の表(空の集合の扱いを含む)、空リストは SQLite 独自の許容であること、EXISTS、
IS・IS NOTとIS DISTINCT FROM
確認日:2026-09-26

コメント