對線面試官:SQL中的IN與NOT IN、EXISTS與NOT EXISTS的區別及效能分析

PHPer技術棧發表於2021-11-03

1、in和exists

in是把外表和內表作hash連線,而exists是對外表作loop迴圈,每次loop迴圈再對內表進行查詢,一直以來認為exists比in效率高的說法是不準確的。

如果查詢的兩個表大小相當,那麼用in和exists差別不大;如果兩個表中一個較小一個較大,則子查詢表大的用exists,子查詢表小的用in;

例如:表A(小表),表B(大表)

select * from A where cc in(select cc from B)  -->效率低,用到了A表上cc列的索引;

select * from A where exists(select cc from B where cc=A.cc)  -->效率高,用到了B表上cc列的索引。

相反的:

select * from B where cc in(select cc from A)  -->效率高,用到了B表上cc列的索引

select * from B where exists(select cc from A where cc=B.cc)  -->效率低,用到了A表上cc列的索引。

2、not in 和not exists

not in 邏輯上不完全等同於not exists,如果你誤用了not in,小心你的程式存在致命的BUG,請看下面的例子:

create table #t1(c1 int,c2 int);

create table #t2(c1 int,c2 int);

insert into #t1 values(1,2);

insert into #t1 values(1,3);

insert into #t2 values(1,2);

insert into #t2 values(1,null);

select * from #t1 where c2 not in(select c2 from #t2);  -->執行結果:無

select * from #t1 where not exists(select 1 from #t2 where #t2.c2=#t1.c2)  -->執行結果:1  3

正如所看到的,not in出現了不期望的結果集,存在邏輯錯誤。如果看一下上述兩個select 語句的執行計劃,也會不同,後者使用了hash_aj,所以,請儘量不要使用not in(它會呼叫子查詢),而儘量使用not exists(它會呼叫關聯子查詢)。

如果子查詢中返回的任意一條記錄含有空值,則查詢將不返回任何記錄。如果子查詢欄位有非空限制,這時可以使用not in,並且可以透過提示讓它用hasg_aj或merge_aj連線。

如果查詢語句使用了not in,那麼對內外表都進行全表掃描,沒有用到索引;而not exists的子查詢依然能用到表上的索引。所以無論哪個表大,用not exists都比not in 要快。

3、in 與 = 的區別

select name from student where name in('zhang','wang','zhao');select name from student where name='zhang' or name='wang' or name='zhao'

的結果是相同的。

其他分析:

1.EXISTS的執行流程

select * from t1 where exists ( select null from t2 where y = x ) 

可以理解為:

for x in ( select * from t1 ) loop 
if ( exists ( select null from t2 where y = x.x ) then 
OUTPUT THE RECORD 
end if 
end loop 

對於in 和 exists的效能區別:

如果子查詢得出的結果集記錄較少,主查詢中的表較大且又有索引時應該用in,反之如果外層的主查詢記錄較少,子查詢中的表大,又有索引時使用exists。

其實我們區分in和exists主要是造成了驅動順序的改變(這是效能變化的關鍵),如果是exists,那麼以外層表為驅動表,先被訪問,如果是IN,那麼先執行子查詢,所以我們會以驅動表的快速返回為目標,那麼就會考慮到索引及結果集的關係了

另外IN時不對NULL進行處理

如:select 1 from dual where null in (0,1,2,null) 為空

2.NOT IN 與NOT EXISTS:

NOT EXISTS的執行流程

select ..... from rollup R  where not exists ( select 'Found' from title T where R.source_id = T.Title_ID); 

可以理解為:

for x in ( select * from rollup ) loop 
if ( not exists ( that query ) ) then 
OUTPUT 
end if; 
end loop; 

注意:NOT EXISTS 與 NOT IN 不能完全互相替換,看具體的需求。如果選擇的列可以為空,則不能被替換。

例如下面語句,看他們的區別:

select x,y from t; 

查詢x和y資料如下:

x y 
------ ------ 
1 3 
3 1 
1 2 
1 1 
3 1 
5 

使用not in 和not exists查詢結果如下:

select * from t where x not in (select y from t t2 ) ;

查詢無結果:no rows

select * from t where not exists (select null from t t2 where t2.y=t.x ) ;

查詢結果為:

x y 
------ ------ 
5 NULL 

所以要具體需求來決定

對於not in 和 not exists的效能區別:

not in 只有當子查詢中,select 關鍵字後的欄位有not null約束或者有這種暗示時用not in,另外如果主查詢中表大,子查詢中的表小但是記錄多,則應當使用not in,並使用anti hash join.

如果主查詢表中記錄少,子查詢表中記錄多,並有索引,可以使用not exists,另外not in最好也可以用/*+ HASH_AJ */或者外連線+is null

NOT IN 在基於成本的應用中較好

比如:

select ..... 
from rollup R 
where not exists ( select 'Found' from title T 
where R.source_id = T.Title_ID); 

改成(佳)

select ...... 
from title T, rollup R 
where R.source_id = T.Title_id(+) 
and T.Title_id is null; 

或者(佳)

sql> select /*+ HASH_AJ */ ... 
from rollup R 
where ource_id NOT IN ( select ource_id 
from title T 
where ource_id IS NOT NULL ) 

討論IN和EXISTS。

select * from t1 where x in ( select y from t2 ) 

事實上可以理解為:

select * 
from t1, ( select distinct y from t2 ) t2 
where t1.x = t2.y; 

——如果你有一定的SQL最佳化經驗,從這句很自然的可以想到t2絕對不能是個大表,因為需要對t2進行全表的“唯一排序”,如果t2很大這個排序的效能是 不可忍受的。但是t1可以很大,為什麼呢?最通俗的理解就是因為t1.x=t2.y可以走索引。

但這並不是一個很好的解釋。試想,如果t1.x和t2.y 都有索引,我們知道索引是種有序的結構,因此t1和t2之間最佳的方案是走merge join。另外,如果t2.y上有索引,對t2的排序效能也有很大提高。

select * from t1 where exists ( select null from t2 where y = x ) 

可以理解為:

for x in ( select * from t1 ) 
loop 
if ( exists ( select null from t2 where y = x.x ) 
then 
OUTPUT THE RECORD! 
end if 
end loop 

——這個更容易理解,t1永遠是個表掃描!因此t1絕對不能是個大表,而t2可以很大,因為y=x.x可以走t2.y的索引。

綜合以上對IN/EXISTS的討論,我們可以得出一個基本通用的結論:IN適合於外表大而內表小的情況;EXISTS適合於外表小而內表大的情況。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章