前言
開發人員寫的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值不能算作能匹配上。
結論
-
使用not in 的非關聯子查詢注意NULL值對結果集的影響,為避免出現空結果集,需要子查詢中查詢列加
is not null
條件將NULL值去除。 -
實際使用時注意:需求表達的含義是否要將外查詢關聯欄位值為NULL的資料輸出,not in隱含了不輸出。
-
一般認為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
技術交流群:
微信:掃碼新增
GreatSQL社群助手
微信好友,傳送驗證資訊加群
。