NOT IN子查詢中出現NULL值對結果的影響你注意到了嗎

GreatSQL發表於2024-03-15

前言

開發人員寫的SQL語句中經常會用到in,exists,not in,not exists 這類子查詢,通常,含in、exists的子查詢稱為半連線(semijoin),含not in、 not exists的子查詢被稱之為反連線,經常會有技術人員來評論in 與exists 效率孰高孰低的問題,我在SQL最佳化工作中也經常對這類子查詢做最佳化改寫,比如半連線改為內連線,反連線改為外連線等,哪個效率高是要根據執行計劃做出判斷的,本文不是為了討論效率問題,是要提醒一點:not in子查詢的結果集含NULL值時,會導致整個語句結果集返回空,這可能造成與SQL語句書寫初衷不符。

實驗

建立實驗表t1,t2

greatsql> create table t1(c1 int primary key,c2 varchar(10), key idx_c1(c2));
greatsql> create table t2(c1 int primary key,c2 varchar(10)),key idx_c1(c2));

greatsql> insert into t1 values(1,'a'),(2,'b');
greatsql> insert into t2 values(1,'a'),(2,'c');

觀察下面兩條語句:

select * from t1 where t1.c2 not in (select t2.c2 from t2);

select * from t1 where not exists (select 1 from t2 where t2.c2=t1.c2);

這兩個語句,從表達的含義來看是等價的,都是查詢t1表中c2列值在t2表的c2列值中不存在的記錄。

從子查詢型別來看,第一條語句屬於非關聯查詢,第二條語句屬於關聯子查詢。所謂非關聯子查詢就是子查詢中內查詢可以獨立執行,與外查詢沒有關係,互不影響。而關聯子查詢的執行依賴於外部查詢,通常情況下都是因為子查詢中的表用到了外部的表,並進行了條件關聯,因此每執行一次外部查詢,子查詢都要重新計算一次。

從連線型別來看,使用not in與not exists子查詢構造的語句都屬於反連線,為了控制連線順序與連線方式,這種反連線經常被改寫為外連線,t1 與t2使用左外連線,條件加上右表t2的連線列 is null,也就是左外連線時沒有關聯上右表的資料,表達了這個含義“t1表中c2列值在t2表的c2列值中不存在的記錄”。反連線改寫為外連線,不會導致關聯結果集放大,因為沒有關聯上的t1表資料只顯示1條,半連線改為內連線時要注意去重。外連線語句如下所示:

greatsql> select t1.* from t1 left join t2 on t1.c2=t2.c2 where t2.c2 is null;

所以本質表達含義上,上面的三條語句都等價。

下面看一下三條語句的執行結果:

greatsql> select * from t1 where t1.c2 not in (select t2.c2 from t2);
+----+------+
| c1 | c2   |
+----+------+
|  2 | b    |
+----+------+
1 row in set (0.00 sec)

greatsql> select * from t1 where not exists (select 1 from t2 where t2.c2=t1.c2);
+----+------+
| c1 | c2   |
+----+------+
|  2 | b    |
+----+------+
1 row in set (0.01 sec)

greatsql> select t1.* from t1 left join t2 on t1.c2=t2.c2 where t2.c2 is null;
+----+------+
| c1 | c2   |
+----+------+
|  2 | b    |
+----+------+
1 row in set (0.00 sec)

可以看出就目前的資料,三條語句執行結果是相同的。

下面向子查詢的t2中插入一條c2列為null的記錄。

greatsql> insert into t2 values(3,null);

再觀察一下三條語句的執行結果:

greatsql> select * from t1 where t1.c2 not in (select t2.c2 from t2);
Empty set (0.00 sec)

greatsql> select * from t1 where not exists (select 1 from t2 where t2.c2=t1.c2);
+----+------+
| c1 | c2   |
+----+------+
|  2 | b    |
+----+------+
1 row in set (0.00 sec)

greatsql> select t1.* from t1 left join t2 on t1.c2=t2.c2 where t2.c2 is null;
+----+------+
| c1 | c2   |
+----+------+
|  2 | b    |
+----+------+
1 row in set (0.00 sec)

可以看出,not exists表示的關聯子查詢與 外連線方式表達的兩條語句結果相同,而not in表示的非關聯子查詢的結果集為空。這是因為子查詢select t2.c2 from t2 查詢結果含有NULL值導致的。NULL屬於未知值,無法與其他值進行比較,無從判斷,返回最終結果集為空。這一點在MySQL與Oracle中返回結果都是一致的。如果想表達最初的含義,需要將子查詢中NULL值去除。

greatsql> select * from t1 where t1.c2 not in (select t2.c2 from t2 where t2.c2 is not null);
+----+------+
| c1 | c2   |
+----+------+
|  2 | b    |
+----+------+
1 row in set (0.02 sec)

那麼如果t1表的c2列也插入一條NULL值的記錄後,結果集會怎樣呢,兩個表都存在c2列為NULL的值資料,那麼t1表這條NULL值資料能否出現在最終結果集中呢?

greatsql> insert into t1 values(3,null);
Query OK, 1 row affected (0.07 sec)

greatsql> select * from t1 where t1.c2 not in (select t2.c2 from t2 where t2.c2 is not null);
+----+------+
| c1 | c2   |
+----+------+
|  2 | b    |
+----+------+
1 row in set (0.00 sec)

greatsql> select * from t1 where not exists (select 1 from t2 where t2.c2=t1.c2);
+----+------+
| c1 | c2   |
+----+------+
|  3 | NULL |
|  2 | b    |
+----+------+
2 rows in set (0.00 sec)

greatsql> select t1.* from t1 left join t2 on t1.c2=t2.c2 where t2.c2 is null;
+----+------+
| c1 | c2   |
+----+------+
|  3 | NULL |
|  2 | b    |
+----+------+
2 rows in set (0.00 sec)

從執行結果來看,使用not in非關聯子查詢,其執行結果與其他兩條語句的執行結果還是不同,因為t1.c2 使用not in在參與比較時就隱含了t1.c2 is not null的含義,所以最終結果集中不含(3,NULL)這條資料。

而not exists關聯子查詢,在將外查詢的NULL值傳遞給內查詢時執行子查詢 select * from t2 where t2.c2=NULL,子查詢中找不到記錄,所以條件返回false, 表示not exists 為true,則最終結果集中含(3,NULL)這條記錄。

左外left join 與 not exists相同,左表的NULL值在右表中關聯不上資料,所以要返回(3,NULL)這條資料。這裡要注意NULL 不等於 NULL。

greatsql> select NULL=NULL;
+-----------+
| NULL=NULL |
+-----------+
|      NULL |
+-----------+
1 row in set (0.01 sec)

說到這裡,GreatSQL支援<=>安全等於這個符號,用來判斷NULL值:當兩個運算元均為NULL時,其返回值為1而不為NULL;而當一個運算元為NULL時,其返回值為0而不為NULL。

greatsql> select NULL<=>NULL;
+-------------+
| NULL<=>NULL |
+-------------+
|           1 |
+-------------+
1 row in set (0.00 sec)

greatsql> select 1<=>NULL;
+----------+
| 1<=>NULL |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec)

所以not exists 子查詢中的= 換成 <=> 時,最終結果集中去除了(3,NULL)這條資料。

greatsql> select * from t1 where not exists (select 1 from t2 where t2.c2<=>t1.c2);
+----+------+
| c1 | c2   |
+----+------+
|  2 | b    |
+----+------+
1 row in set (0.00 sec)

注意,一般表關聯時不使用<=>安全等於這個符號,想象一下,如果關聯的兩個表在關聯欄位上都存在很多NULL記錄,關聯後的結果集對NULL記錄的關聯是以笛卡爾積的形式體現的,嚴重影響效率,嚴格來說關聯欄位都為NULL值不能算作能匹配上。

結論

  1. 使用not in 的非關聯子查詢注意NULL值對結果集的影響,為避免出現空結果集,需要子查詢中查詢列加 is not null條件將NULL值去除。

  2. 實際使用時注意:需求表達的含義是否要將外查詢關聯欄位值為NULL的資料輸出,not in隱含了不輸出。

  3. 一般認為not exists關聯子查詢與外連線語句是等價的,可以進行相互改寫。

select * from t1 where not exists (select 1 from t2 where t2.c2=t1.c2);
   
select t1.* from t1 left join t2 on t1.c2=t2.c2 where t2.c2 is null;

如果不需要輸出外查詢中關聯欄位為NULL值的資料,還需再加條件 t1.c2 is not null。

select * from t1 where not exists (select 1 from t2 where t2.c2=t1.c2) and t1.c2 is not null;
   
select t1.* from t1 left join t2 on t1.c2=t2.c2 where t2.c2 is null and t1.c2 is not null;

這樣寫就與select * from t1 where t1.c2 not in (select t2.c2 from t2 where t2.c2 is not null)等價了。


Enjoy GreatSQL 😃

關於 GreatSQL

GreatSQL是適用於金融級應用的國內自主開源資料庫,具備高效能、高可靠、高易用性、高安全等多個核心特性,可以作為MySQL或Percona Server的可選替換,用於線上生產環境,且完全免費併相容MySQL或Percona Server。

相關連結: GreatSQL社群 Gitee GitHub Bilibili

GreatSQL社群:

社群部落格有獎徵稿詳情:https://greatsql.cn/thread-100-1-1.html

image-20230105161905827

技術交流群:

微信:掃碼新增GreatSQL社群助手微信好友,傳送驗證資訊加群

image-20221030163217640

相關文章