【轉載】[ORACLE]詳解not in與not exists的區別與用法

在網上搜了下關於oracle中not exists和not in性能的比較,發現沒有描述的太全面的,多是問題太簡單了,達人們都不屑於解釋吧。因而本身花了點時間,試圖把這個問題簡單描述清楚,其實歸根結底一句話:not in性能並不比not exists差,關鍵看你用的是否正確。oracle

 

我先建兩個示範表,便於說明:ide

create table  ljn_test1 (col number);oop

create table  ljn_test2 (col number);性能

而後插入一些數據:優化

insert into ljn_test1spa

select level from dual connect by level <=30000;orm

insert into ljn_test2server

select level+1 from dual connect by level <=30000;xml

commit;htm

而後來分別看一下使用not exists和not in的性能差別:

select * from ljn_test1 where not exists (select 1 from ljn_test2 where ljn_test1.col = ljn_test2.col);

 

       COL

----------

         1

 

Elapsed: 00:00:00.06

select * from ljn_test1 where col not in (select col from ljn_test2);

 

       COL

----------

         1

 

Elapsed: 00:00:21.28

能夠看到,使用not exists須要0.06秒,而使用not in須要21秒,差了3個數量級!爲何呢?其實答案很簡答,以上兩個SQL其實並非等價的。

我把以上兩個表的數據清除掉,從新插入數據:

truncate table ljn_test1;

truncate table ljn_test2;

insert into ljn_test1 values(1);

insert into ljn_test1 values(2);

insert into ljn_test1 values(3);

insert into ljn_test2 values(2);

insert into ljn_test2 values(null);

commit;

而後再次執行兩個SQL:

select * from ljn_test1 where not exists (select 1 from ljn_test2 where ljn_test1.col = ljn_test2.col);

 

       COL

----------

         3

         1

 

select * from ljn_test1 where col not in (select col from ljn_test2);

 

no rows selected

這回not in的原形暴露了,居然獲得的是空集。來仔細分解一下緣由:

A.  select * from ljn_test1 where col not in (select col from ljn_test2);

A在這個例子中能夠轉化爲下面的B:

B.  select * from ljn_test1 where col not in (2,null);

B能夠進一步轉化爲下面的C:

C.  select * from ljn_test1 where col <> 2 and col <> null;

由於col <> null是一個永假式,因此最終查出的結果確定也就是空了。

由此能夠得出結論:只要not in的子查詢中包含空值,那麼最終的結果就爲空!

not exists語句不會出現這種狀況,由於not exists子句中寫的是ljn_test1與ljn_test2的關聯,null是不參與等值關聯的,因此ljn_test2的col存在空值對最終的查詢結果沒有任何影響。

我在這裏暫且把ljn_test1叫作外表,ljn_test2叫作內表。

只要稍作概括,就能夠獲得更詳細的結論:

1、對於not exists查詢,內表存在空值對查詢結果沒有影響;對於not in查詢,內表存在空值將致使最終的查詢結果爲空。

2、對於not exists查詢,外表存在空值,存在空值的那條記錄最終會輸出;對於not in查詢,外表存在空值,存在空值的那條記錄最終將被過濾,其餘數據不受影響。

 

講到這裏,我就能夠開始解釋爲何上面的not in語句比not exists語句效率差這麼多了。

not exists語句很顯然就是一個簡單的兩表關聯,內表與外表中存在空值自己就不參與關聯,在CBO(基於成本的優化器)中經常使用的執行計劃是hash join,因此它的效率徹底沒有問題,看一下它的執行計劃:

set autot on;

select * from ljn_test1 where not exists (select 1 from ljn_test2 where ljn_test1.col = ljn_test2.col);

 

       COL

----------

         3

         1

 

Elapsed: 00:00:00.01

 

Execution Plan

----------------------------------------------------------

Plan hash value: 385135874

 

--------------------------------------------------------------------------------

| Id  | Operation          | Name      | Rows  | Bytes | Cost (%CPU)| Time     |

--------------------------------------------------------------------------------

|   0 | SELECT STATEMENT   |           |     3 |    78 |     7  (15)| 00:00:01 |

|*  1 |  HASH JOIN ANTI    |           |     3 |    78 |     7  (15)| 00:00:01 |

|   2 |   TABLE ACCESS FULL| LJN_TEST1 |     3 |    39 |     3   (0)| 00:00:01 |

|   3 |   TABLE ACCESS FULL| LJN_TEST2 |     2 |    26 |     3   (0)| 00:00:01 |

--------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

 

   1 - access("LJN_TEST1"."COL"="LJN_TEST2"."COL")

 

這個執行計劃很清晰,沒有什麼須要解釋的,再看一下not in:

 

select * from ljn_test1 where col not in (select col from ljn_test2);

 

no rows selected

 

Elapsed: 00:00:00.01

 

Execution Plan

----------------------------------------------------------

Plan hash value: 3267714838

 

--------------------------------------------------------------------------------

| Id  | Operation          | Name      | Rows  | Bytes | Cost (%CPU)| Time     |

--------------------------------------------------------------------------------

|   0 | SELECT STATEMENT   |           |     1 |    13 |     5   (0)| 00:00:01 |

|*  1 |  FILTER            |           |       |       |           |          |

|   2 |   TABLE ACCESS FULL| LJN_TEST1 |     3 |    39 |     3   (0)| 00:00:01 |

|*  3 |   TABLE ACCESS FULL| LJN_TEST2 |     2 |    26 |     2   (0)| 00:00:01 |

--------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

 

   1 - filter( NOT EXISTS (SELECT 0 FROM "LJN_TEST2" "LJN_TEST2"

              WHERE LNNVL("COL"<>:B1)))

   3 - filter(LNNVL("COL"<>:B1))

 

能夠看到關聯謂詞是filter,它相似於兩表關聯中的nested loop,也就是跑兩層循環,可見它的效率有多差。爲何not in不能使用hash join做爲執行計劃呢?正如上面解釋的,由於內表或外表中存在空值對最終結果產生的影響是hash join沒法實現的,由於hash join不支持把空值放到hash桶中,因此它沒辦法處理外表和內表中存在的空值,效率與正確性放在一塊兒時,確定是要選擇正確性,因此oracle必須放棄效率,保證正確性,採用filter謂詞。

 

這個執行計劃中咱們還有感興趣的東西,那就是:LNNVL("COL"<>:B1),關於LNNVL的解釋能夠參見官方文檔:http://download.oracle.com/docs/cd/B19306_01/server.102/b14200/functions078.htm

它在這裏的做用很巧妙,oracle知道使用filter性能不好,因此它在掃描內表ljn_test2時,會使用LNNVL來檢查ljn_test2.col是否存在null值,只要掃描到null值,就能夠判定最終的結果爲空值,也就沒有了繼續執行的意義,因此oracle能夠立刻終止執行,在某種意義上它彌補了filter較差的性能。

我用例子來證實這一點,首先先造一些數據:

truncate table ljn_test1;

truncate table ljn_test2;

insert into ljn_test1

select level from dual connect by level <=30000;

insert into ljn_test2

select level+1 from dual connect by level <=30000;

commit;

而後我爲了讓oracle儘快掃描到ljn_test2.col爲null的那條記錄,我要先找到物理地址最小的那條記錄,由於一般狀況全表掃描會先掃描物理地址最小的那條記錄:

select col from ljn_test2 where rowid=(select min(rowid) from ljn_test2);

 

       COL

----------

      1982

而後我把這條記錄更新爲空:

update ljn_test2 set col = null where col=1982;

commit;

而後再來看一下not in的查詢效率:

select * from ljn_test1 where col not in (select col from ljn_test2);

 

no rows selected

 

Elapsed: 00:00:00.17

 

看到這個結果後我很爽,它和以前查詢須要用時21秒有很大的差異!

固然,咱們不能老是期望oracle掃描表時老是最早找到null值,看下面的例子:

update ljn_test2 set col = 1982 where col is null;

select col from ljn_test2 where rowid=(select max(rowid) from ljn_test2);

 

       COL

----------

     30001

update ljn_test2 set col = null where col=30001;

commit;

再看一下not in的查詢效率:

select * from ljn_test1 where col not in (select col from ljn_test2);

 

       COL

----------

         1

 

Elapsed: 00:00:21.11

這一下not in再一次原形畢露了!

機會主義不行,更杯具的是若是內表中沒有空值,那LNNVL優化就永遠起不到做用,相反它還會增大開銷!

其實只要找到緣由,問題很好解決,不就是空值在做怪嘛!在正常的邏輯下用戶原本就是想獲得和not exists等價的查詢結果,因此只要讓oracle知道咱們不須要空值參與進來就能夠了。

第一種解決方案:

將內表與外表的關聯字段設定爲非空的

alter table ljn_test1 modify col not null;

alter table ljn_test2 modify col not null;

好了,再看一下執行計劃:

set autot on;

select * from ljn_test1 where col not in (select col from ljn_test2);

 

       COL

----------

         1

 

Elapsed: 00:00:00.07

 

Execution Plan

----------------------------------------------------------

Plan hash value: 385135874

 

--------------------------------------------------------------------------------

| Id  | Operation          | Name      | Rows  | Bytes | Cost (%CPU)| Time     |

--------------------------------------------------------------------------------

|   0 | SELECT STATEMENT   |           |     1 |    26 |    28   (8)| 00:00:01 |

|*  1 |  HASH JOIN ANTI    |           |     1 |    26 |    28   (8)| 00:00:01 |

|   2 |   TABLE ACCESS FULL| LJN_TEST1 | 30000 |   380K|    13   (0)| 00:00:01 |

|   3 |   TABLE ACCESS FULL| LJN_TEST2 | 30000 |   380K|    13   (0)| 00:00:01 |

--------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

 

   1 - access("COL"="COL")

 

很好!這回oracle已經知道使用hash join了!不過有時候表中須要存儲空值,這時候就不能在表結構上指定非空了,那也一樣簡單:

第二種解決方案:

查詢時在內表與外表中過濾空值。

先把表結構恢復爲容許空值的:

alter table ljn_test1 modify col null;

alter table ljn_test2 modify col null;

而後改造查詢:

select * from ljn_test1 where col is not null and col not in (select col from ljn_test2 where col is not null);

 

       COL

----------

         1

 

Elapsed: 00:00:00.07

 

Execution Plan

----------------------------------------------------------

Plan hash value: 385135874

 

--------------------------------------------------------------------------------

| Id  | Operation          | Name      | Rows  | Bytes | Cost (%CPU)| Time     |

--------------------------------------------------------------------------------

|   0 | SELECT STATEMENT   |           |     1 |    26 |    28   (8)| 00:00:01 |

|*  1 |  HASH JOIN ANTI    |           |     1 |    26 |    28   (8)| 00:00:01 |

|*  2 |   TABLE ACCESS FULL| LJN_TEST1 | 30000 |   380K|    13   (0)| 00:00:01 |

|*  3 |   TABLE ACCESS FULL| LJN_TEST2 | 30000 |   380K|    13   (0)| 00:00:01 |

--------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

 

   1 - access("COL"="COL")

   2 - filter("COL" IS NOT NULL)

   3 - filter("COL" IS NOT NULL)

 

OK! hash join出來了!我想我關於not exists與not in之間的比較也該結束了。

相關文章
相關標籤/搜索