ほかのRDBMSも同様だと思うがMySQLで検証してる時に発生したのでメモ
not inを使うとデータ取得できない場合がある
SQLのWhere句としてAA not in (select …)と使う場合があります。
通常であれば、データ取得することができるのだが、IN句内の値にNULLがあるとデータ取得ができなくなります。
検証
準備
docker環境でMySQL8.0.23を準備し、下記のようなテーブルとデータを用意しました。
create table test1(id int,name1 varchar(255));
create table test2(id int,name2 varchar(255));
insert into test1 value (1,'テスト1_1');
insert into test1 value (2,'テスト1_2');
insert into test1 value (3,'テスト1_3');
insert into test1 value (null,'テスト2_null');
insert into test1 value (4,'テスト1_4');
insert into test2 value (1,'テスト2_1');
insert into test2 value (2,'テスト2_2');
insert into test2 value (3,'テスト2_3');
insert into test2 value (null,'テスト2_null');
各テーブルを取得すると下記のように取得できます。
id | name1 |
---|---|
1 | テスト1_1 |
2 | テスト1_2 |
3 | テスト1_3 |
(null) | テスト2_null |
4 | テスト1_4 |
id | name2 |
---|---|
1 | テスト2_1 |
2 | テスト2_2 |
3 | テスト2_3 |
(null) | テスト2_null |
検証1(in句の副問い合わせ)
IN句の副問い合わせでtest2テーブルにデータを取得します。
select * from test1 where id in(select id from test2 );
id | name1 |
---|---|
1 | テスト1_1 |
2 | テスト1_2 |
3 | テスト1_3 |
データ取得可能ですが、「id:NULL」のデータは取得できませんでした。
検証2(not in句の副問い合わせ)
検証1のin句をnot in句に変更して実行します。
select * from test1 where id not in(select id from test2 );
id | name1 |
---|
本来ならば「id = 4」のデータが取得できるはずなのに、すべて取得できない形になりました。
検証3(not in句の副問い合わせ2)
検証2の副問い合わせ内で検索条件:id is not nullを追加します。
select * from test1 where id not in(select id from test2 where id is not null );
id | name1 |
---|---|
4 | テスト1_4 |
NULL以外を検索条件に付与すると取得できるようです。
原因
MySQL上においてNULLはunknownなデータとして扱われます。
そしてMySQL上ではNULLで評価した場合は「=」でも「<>」でも「<」でも「>」でもNULLとして扱います。
MySQL:3.3.4.6 NULL 値の操作
SELECT 1 IS NULL, 1 IS NOT NULL;
+-----------+---------------+
| 1 IS NULL | 1 IS NOT NULL |
+-----------+---------------+
| 0 | 1 |
+-----------+---------------+
SELECT 1 = NULL, 1 <> NULL, 1 < NULL, 1 > NULL;
+----------+-----------+----------+----------+
| 1 = NULL | 1 <> NULL | 1 < NULL | 1 > NULL |
+----------+-----------+----------+----------+
| NULL | NULL | NULL | NULL |
+----------+-----------+----------+----------+
そして、IN句では()の中身を1つずつ評価していく形になります。
select * from test1 where id in(select id from test2);
↓イメージとして下記と同じ
select * from test1 where id in (1,2,3,null);
↓イメージとして下記と同じ
select * from test1 where id = 1 or id = 2 or id = 3 or id = null;
「id = null」の評価結果は「NULL(不明)」となり、検索条件としてヒットできなくなります。
そのため、「id = 1 or id = 2 or id = 3」のデータ分のみ取得できる状態になります。
次にNOT IN句の場合ですと
select * from test1 where id not in(select id from test2);
↓イメージとして下記と同じ
select * from test1 where id not in (1,2,3,null);
↓イメージとして下記と同じ
select * from test1 where id <> 1 and id <> 2 and id <> 3 and id <> null;
という評価になります。
各条件がAND条件となっているおり、「id <> NULL」の評価結果は「NULL(不明)」となります。
そのため、検索結果自体が「1以外 and 2以外 and 3以外 and NULL(不明)」となるため、データ取得ができなくなります。
そのため、NOT IN句でデータ取得する場合はIN句よりも注意しないといけないです。
NULLデータを評価する場合はEXISTSを使う
NOT IN句は条件に当てはまるデータを抽出して比較するに対し、NOT EXISTSはデータの存在結果に対してTRUE・FALSEを返すという評価方式が異なります。
データ存在に対して評価を行うEXISTSはNULLデータの取り扱いも可能となるわけです。
select * from test1 where not exists (select 'x' from test2 where test1.id = test2.id);
id | name1 |
---|---|
(null) | テスト2_null |
4 | テスト1_4 |
評価方式が異なるので積極的にEXISTSを使うべきというわけでもないのですが、NULLデータが入る可能性のあるカラムでNOT INは使わない方がいいです。
補足
EXISTSでNULLデータも抽出条件を行いたい場合は、「or xxx is null」条件を追加すれば取得できます。
select * from test1 where exists (select 'x' from test2 where test1.id = test2.id or test1.id is null);
id | name1 |
---|---|
1 | テスト1_1 |
2 | テスト1_2 |
3 | テスト1_3 |
(null) | テスト2_null |
雑感
IN句とEXISTS句は評価方法が違うので一概には言えないのですが、カラムにNULLデータを許容しているのならば極力EXISTSを使った方が不具合リスクは減らせれるのでいいのではと思ってます。(ORACLEでは1000件までしか使えなかったりしてるせいでもありますが。)
テーブルカラムの型やNULL制約を見てSQL分を作るようにしていきましょう。