设为首页 收藏本站
查看: 469|回复: 0

[经验分享] Oracle之not in 和not exists 的比较(转)

[复制链接]
YunVN网友  发表于 2016-8-15 06:40:14 |阅读模式
      在网上搜了下关于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之间的比较也该结束了。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-257756-1-1.html 上篇帖子: Oracle第三方工具PL/SQL Developer使用一 下篇帖子: SQL Server与Oracle对比学习:权限管理(一)
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表