順番に読み進めながら学べます

WHERE 詳細 ③ — IS NULL と IN で NULL・集合の絞り込み

この記事は、基礎から複雑なSQL,SQLチューニングまでSQLの実践的なスキルを1からマスターする「SQL入門講座の一部」です。
3値論理(真・偽・不明)、IS NULL/IS NOT NULL、IN/NOT IN、NULLを含むとNOT INがはまる落とし穴まで、CSVの顧客データで実行しながら学べます。

本記事で使うデータ — customer テーブル(NULL を含む)

WHERE 詳細の 3 本目はNULL集合での絞り込みを扱います。題材は CSV から自動ロードするcustomerテーブル(8 行)で、age / email / countryの各列にNULL(値が不明)の行が混じっています。実務のテーブルでも「メールアドレスが未登録」「国が空」のように、必須項目以外は NULL が混じるのが普通です。

本記事の 5 演習では、まず NULL の正しい取り出し方(IS NULL / IS NOT NULL)を押さえ、続いて候補リストとの照合(IN / NOT IN)、最後に NULL を含むデータでNOT INを使うときに見落としがちな落とし穴までを順に体験します。

演習に入る前に、customerテーブルの列定義データのサンプルを確認しておきます。

PRAGMA table_info(customer);で列名・型・主キーを確認してください。

SELECT * FROM customer LIMIT 5;で先頭 5 行のデータをプレビューしてください。NULL になっているセルがあることを観察してください。

SQL エディタ

クエリを実行してください

NULL と 3 値論理 — 真・偽・不明

プログラミング言語では条件はTRUE(真)か FALSE(偽)の 2 択ですが、SQL ではもう 1 つNULL(不明)という状態が加わります。これを3 値論理と呼びます。NULL は「値が空である」ではなく「値が分からない」という意味で、NULL = NULLですらNULL(不明)になります(不明と不明を比較しても、それが等しいかは不明なため)。

WHERE 条件条件が TRUE になった行だけを残します。NULL や FALSE は両方とも除外されます。このルールを覚えておくと、なぜWHERE email = NULLが常に空っぽの結果を返すかが見えてきます。

SQL の 3 値論理
TRUE(真)FALSE(偽)NULL(不明)
条件式の評価結果はこの 3 種類のいずれか。WHERE は TRUE のときだけ行を残し、FALSE と NULL は同じ「除外」扱いになります。
3 値論理 — TRUE / FALSE / NULL の評価
比較式評価結果WHERE で残るかname = 'Alice'(name が Alice のとき)TRUE○ 残るname = 'Bob'(name が Alice のとき)FALSE× 除外email = NULL(email が NULL のとき)NULL× 除外(TRUE でない)email IS NULL(email が NULL のとき)TRUE○ 残る
WHERE は条件が TRUE の行だけを残します。NULL を含む比較は結果が NULL になり、NULL は TRUE ではないので除外されます。

なぜ`email = NULL`で取り出せないか

email = NULLという比較は、両辺のどちらかが NULL のとき常に NULL(不明)を返します。'unknown' = NULLNULL = NULLも結果はすべて NULL です。WHERE は TRUE の行だけを残すので、NULL になった条件は除外され、結果は 0 行になります。

NULL を取り出すには、専用の列 IS NULL / 列 IS NOT NULLを使う必要があります。これは「等価比較ではなく、NULL かどうかを直接確認する」専用の構文で、結果は必ず TRUE か FALSE のどちらかになります。

退会していない顧客が 0 人と表示された?

「退会していない顧客(deleted_atが NULL)の件数を出す」ときに、SELECT COUNT(*) FROM user WHERE deleted_at = NULL;と書いて結果が0で返ってきたことがあります。

実際には数千人の現役会員がいたのですが、deleted_at = NULLは常に NULL(不明)に評価されて WHERE で除外されるため、何人いても結果は常に 0 人でした。SQL を始めたばかりの頃よくあるミスなので、NULL 判定は必ずIS NULL / IS NOT NULLを使うようにしましょう。

IS NULL と IS NOT NULL — 不明値を取り出す

NULL(値が不明)の行を取り出すには列 IS NULLを使います。逆に NULL 以外の行を取り出すには列 IS NOT NULLです。= NULL / <> NULLは使わないと覚えておくと、3 値論理の落とし穴を避けられます。

下のコードのとおり、書き方は=の代わりにISを使うだけですが、結果はまったく違うものになります。= NULLの方は何も返らず、IS NULLの方は NULL の行を正しく取り出します。

`= NULL`と`IS NULL`の挙動の違い
WHERE email = NULLWHERE email IS NULLNULL = NULL→ NULLemail IS NULL→ TRUENULL は TRUE ではないので常に除外されるTRUE のとき残るのでNULL の行が取り出せる結果: 0 行結果: 4 行(Bob / Dave / Frank / Henry)
= NULL は常に NULL(不明)と評価されるため WHERE が 0 行になります。IS NULL は NULL であるかを直接判定し、TRUE / FALSE のどちらかを返します。
-- 1) NG: = NULL では取り出せない(常に NULL = 除外)
SELECT name, email FROM customer WHERE email = NULL;
-- 結果: 0 行

-- 2) OK: IS NULL で取り出す
SELECT name, email FROM customer WHERE email IS NULL;
-- 結果: 4 行(Bob / Dave / Frank / Henry)

-- 3) NULL 以外を取り出す
SELECT name, email FROM customer WHERE email IS NOT NULL;
-- 結果: 4 行(Alice / Carol / Eve / Grace)

顧客サポートで「メールアドレスが未登録の顧客リストを出して、登録依頼を送りたい」という要件を想定します。(正しく実行できれば解説が表示されます)

customerテーブルからnameemailの 2 列を取り出してください。

email列が NULLの行に絞り込んでください。

③ 結果が 4 行(Bob / Dave / Frank / Henry、いずれも email が NULL)になることを確認してください。

SQL エディタ

クエリを実行してください

ターゲット広告の配信対象として「年齢とメールが両方登録されている顧客」を年齢の高い順に並べたい、という要件を想定します。

customerテーブルからnameageemailの 3 列を取り出してください。

ageもemailも NULL でない行に絞り込んでください。

ageの降順で並べてください。

④ 結果が 3 行(Eve 42 / Grace 35 / Alice 30)になることを確認してください。

SQL エディタ

クエリを実行してください

IN と NOT IN — 候補リストとの照合

「country がJapanUSUKのいずれか」のように、列の値が候補リストのどれかに一致するかを判定したいときに使うのがINです。列 IN (値1, 値2, ...)と書くと、候補のいずれかに一致した行を取り出せます。列 = 値1 OR 列 = 値2 OR 列 = 値3と書くのと同じ意味で、可読性のために IN を選ぶ場面が多いです。

NOT INを付ければ、候補のどれにも一致しない行を取り出せます。

`IN`の動き — 候補リストとの照合
country の値IN ('Japan', 'US')WHERE で残るかJapanTRUE○ 残るUSTRUE○ 残るUKFALSE× 除外ItalyFALSE× 除外NULLNULL(不明)× 除外(NULL は TRUE でない)
IN は値が候補リストのいずれかと一致するかを判定します。列の値が NULL の行は IN / NOT IN ともに NULL として評価されて除外されるので、NULL が混じるデータでは別途 IS NULL / IS NOT NULL と組み合わせます。
-- 1) IN: 候補リストのいずれかに一致
SELECT name, country FROM customer
WHERE country IN ('Japan', 'US');

-- 上の式と同じ意味(OR での書き換え)
SELECT name, country FROM customer
WHERE country = 'Japan' OR country = 'US';

-- 2) NOT IN: 候補のどれにも一致しない
SELECT name, country FROM customer
WHERE country NOT IN ('Japan');

「日本またはアメリカの顧客にお知らせメールを送りたい」という要件を想定します。

customerテーブルからnamecountryの 2 列を取り出してください。

countryが'Japan'または'US'の行に絞り込んでください(IN を使ってください)。

③ 結果が 5 行(Alice Japan / Bob US / Carol Japan / Frank Japan / Grace US)になることを確認してください。

SQL エディタ

クエリを実行してください

`NOT IN`と NULL の落とし穴

NOT IN ('Japan')は「country が Japan でない人」を取り出すように見えますが、country が NULL の行も除外されます。これはcountry NOT IN ('Japan')の評価が、内部的にはcountry <> 'Japan'(= 等しくない)になり、NULL <> 'Japan'NULL(不明)を返すためです。NULL は TRUE ではないので WHERE が除外します。

さらに踏み込んで、候補リスト自体に NULL が混じったNOT IN ('Japan', NULL)のような書き方をすると、全行が NULL 評価で消えるという事故も起きます。NULL が含まれる可能性のある列に NOT IN を使うときは、必ずcolumn NOT IN (...) AND column IS NOT NULLのようにNULL 除外を明示するか、IN 側で書き換える設計を選びます。

「日本以外の在住顧客のうち、年齢の高い順に 2 名にプレミアム特典を案内したい」という要件を想定します。

customerテーブルからnameagecountryの 3 列を取り出してください。

countryが'Japan'でないNOT IN)行に絞り込んでください。

ageの降順で並べ、先頭 2 行に絞ってください。

④ 結果が 2 行(Grace 35 US / Henry 29 Italy)になることを確認してください。Eve(country が NULL)が除外されている理由はresultExplanationでも確認します。

SQL エディタ

クエリを実行してください

実践 4 の落とし穴を踏まえ、「日本在住でないことが確定している顧客」と「国そのものが未登録の顧客」の両方をまとめて出したい、という要件を想定します。

customerテーブルからnameagecountryの 3 列を取り出してください。

countryが'Japan'以外の行に加えて、countryが NULLの行も含めてください(OR でつなぎます)。

ageの降順で並べてください。

④ 結果が 5 行(Eve 42 NULL / Grace 35 US / Henry 29 Italy / Dave 28 UK / Bob 25 US)になることを確認してください。

SQL エディタ

クエリを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1次のうち、SELECT * FROM customer WHERE email = NULL;の結果として正しいものはどれですか。

Q2次のうち、IN を使った絞り込みの説明として正しいものはどれですか。

Q3country列に NULL が混じったcustomerテーブルに対してWHERE country NOT IN ('Japan')を実行した時、country が NULL の行はどう扱われますか。