NOT IN子查詢中出現(xiàn)NULL值對結(jié)果的影響你注意到了嗎
前言
開發(fā)人員寫的SQL語句中經(jīng)常會用到in,exists,not in,not exists 這類子查詢,通常,含in、exists的子查詢稱為半連接(semijoin),含not in、 not exists的子查詢被稱之為反連接,經(jīng)常會有技術(shù)人員來評論in 與exists 效率孰高孰低的問題,我在SQL優(yōu)化工作中也經(jīng)常對這類子查詢做優(yōu)化改寫,比如半連接改為內(nèi)連接,反連接改為外連接等,哪個效率高是要根據(jù)執(zhí)行計劃做出判斷的,本文不是為了討論效率問題,是要提醒一點(diǎn):not in子查詢的結(jié)果集含NULL值時,會導(dǎo)致整個語句結(jié)果集返回空,這可能造成與SQL語句書寫初衷不符。
實(shí)驗(yàn)
創(chuàng)建實(shí)驗(yàn)表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);
這兩個語句,從表達(dá)的含義來看是等價的,都是查詢t1表中c2列值在t2表的c2列值中不存在的記錄。
從子查詢類型來看,第一條語句屬于非關(guān)聯(lián)查詢,第二條語句屬于關(guān)聯(lián)子查詢。所謂非關(guān)聯(lián)子查詢就是子查詢中內(nèi)查詢可以獨(dú)立執(zhí)行,與外查詢沒有關(guān)系,互不影響。而關(guān)聯(lián)子查詢的執(zhí)行依賴于外部查詢,通常情況下都是因?yàn)樽硬樵冎械谋碛玫搅送獠康谋?,并進(jìn)行了條件關(guān)聯(lián),因此每執(zhí)行一次外部查詢,子查詢都要重新計算一次。
從連接類型來看,使用not in與not exists子查詢構(gòu)造的語句都屬于反連接,為了控制連接順序與連接方式,這種反連接經(jīng)常被改寫為外連接,t1 與t2使用左外連接,條件加上右表t2的連接列 is null,也就是左外連接時沒有關(guān)聯(lián)上右表的數(shù)據(jù),表達(dá)了這個含義“t1表中c2列值在t2表的c2列值中不存在的記錄”。反連接改寫為外連接,不會導(dǎo)致關(guān)聯(lián)結(jié)果集放大,因?yàn)闆]有關(guān)聯(lián)上的t1表數(shù)據(jù)只顯示1條,半連接改為內(nèi)連接時要注意去重。外連接語句如下所示:
greatsql> select t1.* from t1 left join t2 on t1.c2=t2.c2 where t2.c2 is null;
所以本質(zhì)表達(dá)含義上,上面的三條語句都等價。
下面看一下三條語句的執(zhí)行結(jié)果:
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)
可以看出就目前的數(shù)據(jù),三條語句執(zhí)行結(jié)果是相同的。
下面向子查詢的t2中插入一條c2列為null的記錄。
greatsql> insert into t2 values(3,null);
再觀察一下三條語句的執(zhí)行結(jié)果:
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表示的關(guān)聯(lián)子查詢與 外連接方式表達(dá)的兩條語句結(jié)果相同,而not in表示的非關(guān)聯(lián)子查詢的結(jié)果集為空。這是因?yàn)樽硬樵僺elect t2.c2 from t2 查詢結(jié)果含有NULL值導(dǎo)致的。NULL屬于未知值,無法與其他值進(jìn)行比較,無從判斷,返回最終結(jié)果集為空。這一點(diǎn)在MySQL與Oracle中返回結(jié)果都是一致的。如果想表達(dá)最初的含義,需要將子查詢中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值的記錄后,結(jié)果集會怎樣呢,兩個表都存在c2列為NULL的值數(shù)據(jù),那么t1表這條NULL值數(shù)據(jù)能否出現(xiàn)在最終結(jié)果集中呢?
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)
從執(zhí)行結(jié)果來看,使用not in非關(guān)聯(lián)子查詢,其執(zhí)行結(jié)果與其他兩條語句的執(zhí)行結(jié)果還是不同,因?yàn)閠1.c2 使用not in在參與比較時就隱含了t1.c2 is not null的含義,所以最終結(jié)果集中不含(3,NULL)這條數(shù)據(jù)。
而not exists關(guān)聯(lián)子查詢,在將外查詢的NULL值傳遞給內(nèi)查詢時執(zhí)行子查詢 select * from t2 where t2.c2=NULL,子查詢中找不到記錄,所以條件返回false, 表示not exists 為true,則最終結(jié)果集中含(3,NULL)這條記錄。
左外left join 與 not exists相同,左表的NULL值在右表中關(guān)聯(lián)不上數(shù)據(jù),所以要返回(3,NULL)這條數(shù)據(jù)。這里要注意NULL 不等于 NULL。
greatsql> select NULL=NULL;
+-----------+
| NULL=NULL |
+-----------+
| NULL |
+-----------+
1 row in set (0.01 sec)
說到這里,GreatSQL支持<=>安全等于這個符號,用來判斷NULL值:當(dāng)兩個操作數(shù)均為NULL時,其返回值為1而不為NULL;而當(dāng)一個操作數(shù)為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 子查詢中的= 換成 <=> 時,最終結(jié)果集中去除了(3,NULL)這條數(shù)據(jù)。
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)
注意,一般表關(guān)聯(lián)時不使用<=>安全等于這個符號,想象一下,如果關(guān)聯(lián)的兩個表在關(guān)聯(lián)字段上都存在很多NULL記錄,關(guān)聯(lián)后的結(jié)果集對NULL記錄的關(guān)聯(lián)是以笛卡爾積的形式體現(xiàn)的,嚴(yán)重影響效率,嚴(yán)格來說關(guān)聯(lián)字段都為NULL值不能算作能匹配上。
結(jié)論
使用not in 的非關(guān)聯(lián)子查詢注意NULL值對結(jié)果集的影響,為避免出現(xiàn)空結(jié)果集,需要子查詢中查詢列加 is not null條件將NULL值去除。
實(shí)際使用時注意:需求表達(dá)的含義是否要將外查詢關(guān)聯(lián)字段值為NULL的數(shù)據(jù)輸出,not in隱含了不輸出。
一般認(rèn)為not exists關(guān)聯(lián)子查詢與外連接語句是等價的,可以進(jìn)行相互改寫。
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;
如果不需要輸出外查詢中關(guān)聯(lián)字段為NULL值的數(shù)據(jù),還需再加條件 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)等價了。