1、in与exists之争(10g);2、in与exists之争(11g)

在网上搜了下关于oracle中not exists和not in性能的比较,发现没有描述的太全面的,可能是问题太简单了,达人们都不屑于解释吧。于是自己花了点时间,试图把这个问题简单描述清楚,其实归根结底一句话:not in性能并不比not exists差,关键看你用的是否正确。    



我先建两个示范表,便于说明:    

create table  ljn_test1 (col number);    

create table  ljn_test2 (col number);    

然后插入一些数据:    

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;    

然后来分别看一下使用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之间的比较也该结束了。    

11g执行一下
select * from v$version;

drop table emp purge;
drop table dept purge;
create table emp as select * from scott.emp;
create table dept as select * from scott.dept;
set timing on
set linesize 1000

set autotrace traceonly explain
select * from dept where deptno NOT IN ( select deptno from emp ) ;
select * from dept where not exists ( select deptno from emp where emp.deptno=dept.deptno) ;

select * from dept where deptno NOT IN ( select deptno from emp where deptno is not null) and deptno is not null;

–结论:11g与空值有关,都可以用到anti的半连接算法,执行计划一样,性能一样

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值