SQL开发实战技巧系列:从执行计划看INEXISTS 和 INNER JOIN效率,我们要分场景不要死记网上结论

Posted 赵延东的一亩三分地

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SQL开发实战技巧系列:从执行计划看INEXISTS 和 INNER JOIN效率,我们要分场景不要死记网上结论相关的知识,希望对你有一定的参考价值。

系列文章目录

【SQL开发实战技巧】系列(一):关于SQL不得不说的那些事
【SQL开发实战技巧】系列(二):简单单表查询
【SQL开发实战技巧】系列(三):SQL排序的那些事
【SQL开发实战技巧】系列(四):从执行计划讨论UNION ALL与空字符串&UNION与OR的使用注意事项
【SQL开发实战技巧】系列(五):从执行计划看IN、EXISTS 和 INNER JOIN效率,我们要分场景不要死记网上结论
【SQL开发实战技巧】系列(六):从执行计划看NOT IN、NOT EXISTS 和 LEFT JOIN效率,记住内外关联条件不要乱放
【SQL开发实战技巧】系列(七):从有重复数据前提下如何比较出两个表中的差异数据及对应条数聊起
【SQL开发实战技巧】系列(八):聊聊如何插入数据时比约束更灵活的限制数据插入以及怎么一个insert语句同时插入多张表
【SQL开发实战技巧】系列(九):一个update误把其他列数据更新成空了?Merge改写update!给你五种删除重复数据的写法!
【SQL开发实战技巧】系列(十):从拆分字符串、替换字符串以及统计字符串出现次数说起
【SQL开发实战技巧】系列(十一):拿几个案例讲讲translate|regexp_replace|listagg|wmsys.wm_concat|substr|regexp_substr常用函数
【SQL开发实战技巧】系列(十二):三问(如何对字符串字母去重后按字母顺序排列字符串?如何识别哪些字符串中包含数字?如何将分隔数据转换为多值IN列表?)
【SQL开发实战技巧】系列(十三):讨论一下常用聚集函数&通过执行计划看sum()over()对员工工资进行累加
【SQL开发实战技巧】系列(十四):计算消费后的余额&计算银行流水累计和&计算各部门工资排名前三位的员工
【SQL开发实战技巧】系列(十五):查找最值所在行数据信息及快速计算总和百之max/min() keep() over()、fisrt_value、last_value、ratio_to_report
【SQL开发实战技巧】系列(十六):数据仓库中时间类型操作(初级)日、月、年、时、分、秒之差及时间间隔计算
【SQL开发实战技巧】系列(十七):数据仓库中时间类型操作(初级)确定两个日期之间的工作天数、计算—年中周内各日期出现次数、确定当前记录和下一条记录之间相差的天数
【SQL开发实战技巧】系列(十八):数据仓库中时间类型操作(进阶)INTERVAL、EXTRACT以及如何确定一年是否为闰年及周的计算
【SQL开发实战技巧】系列(十九):数据仓库中时间类型操作(进阶)如何一个SQL打印当月或一年的日历?如何确定某月内第一个和最后—个周内某天的日期?
【SQL开发实战技巧】系列(二十):数据仓库中时间类型操作(进阶)获取季度开始结束时间以及如何统计非连续性时间的数据
【SQL开发实战技巧】系列(二十一):数据仓库中时间类型操作(进阶)识别重叠的日期范围,按指定10分钟时间间隔汇总数据
【SQL开发实战技巧】系列(二十二):数仓报表场景☞ 从分析函数效率一定快吗聊一聊结果集分页和隔行抽样实现方式
【SQL开发实战技巧】系列(二十三):数仓报表场景☞ 如何对数据排列组合去重以及通过如何找到包含最大值和最小值的记录这个问题再次用执行计划给你证明分析函数性能不一定高
【SQL开发实战技巧】系列(二十四):数仓报表场景☞通过案例执行计划详解”行转列”,”列转行”是如何实现的
【SQL开发实战技巧】系列(二十五):数仓报表场景☞结果集中的重复数据只显示一次以及计算部门薪资差异高效的写法以及如何对数据进行快速分组
【SQL开发实战技巧】系列(二十六):数仓报表场景☞聊聊ROLLUP、UNION ALL是如何分别做分组合计的以及如何识别哪些行是做汇总的结果行


文章目录


前言

本篇文章讲解的主要内容是:从执行计划角度分析IN、EXISTS 和 INNER JOIN效率而不是死记网上结论、表的5种关联:INNER JOIN、LEFT JOIN、RIGHT JOIN 和 FULL JOIN 解析
【SQL开发实战技巧】这一系列博主当作复习旧知识来进行写作,毕竟SQL开发在数据分析场景非常重要且基础,面试也会经常问SQL开发和调优经验,相信当我写完这一系列文章,也能再有所收获,未来面对SQL面试也能游刃有余~。


一、组合相关的行

相对查询单表中的数据来说,平时更常见的需求是要在多个表中返回数据。比如,显示部门10的员工编码、姓名及所在部门名称和工作地址。

select a.empno,a.deptno,b.dname
from emp a inner join dept b
on(a.deptno=b.deptno)
where a.deptno=10;
EMPNO DEPTNO DNAME
----- ------ --------------
 7782     10 ACCOUNTING
 7839     10 ACCOUNTING
 7934     10 ACCOUNTING

另外有下面写法:

select a.empno,a.deptno,b.dname
from emp a,dept b
where a.deptno=b.deptno
EMPNO DEPTNO DNAME
----- ------ --------------
 7369     20 RESEARCH
 7499     30 SALES
 7521     30 SALES
 7566     20 RESEARCH
 7654     30 SALES
 7698     30 SALES
 7782     10 ACCOUNTING
 7788     20 RESEARCH
 7839     10 ACCOUNTING
 7844     30 SALES
 7876     20 RESEARCH
 7900     30 SALES
 7902     20 RESEARCH
 7934     10 ACCOUNTING

14 rows selected

其中,JOIN的写法是SQL-92的标准,当有多个表关联时,JOIN方式的写法能更清楚地看清各表之间的关系,因此,建议大家写查询语句时优先使用JOIN的写法。

二、从执行计划看IN、EXISTS 和 INNER JOIN效率

下面先 创建一个表 emp2.

drop index IDX_ENAME;
DROP TABLE emp2 PURGE ;
CREATE TABLE emp2 AS
SELECT ename,job,sal,comm FROM emp WHERE job ='CLERK';

要求返回与表emp2(empno,job,sal)中数据相匹配的emp(empno,ename,job,sal,deptno)
信息。
IN、EXISTS、INNER JOIN三种写法。为了加强理解,请大家看一下三种写法及其PLAN(此处用的是Oracle 11g)。

  • in写法
SQL> explain plan for select empno,ename,job,sal,deptno from emp where (ename,job,sal) in(select ename,job,sal from emp2);

Explained


SQL> select * from table(dbms_xplan.display());

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
Plan hash value: 4039873364
---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |     1 |    67 |     6   (0)| 00:00:01 |
|*  1 |  HASH JOIN SEMI    |      |     1 |    67 |     6   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| EMP  |    15 |   780 |     3   (0)| 00:00:01 |
|   3 |   TABLE ACCESS FULL| EMP2 |     4 |    60 |     3   (0)| 00:00:01 |
---------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - access("ENAME"="ENAME" AND "JOB"="JOB" AND "SAL"="SAL")
Note
-----
   - dynamic statistics used: dynamic sampling (level=2)

19 rows selected
  • exists写法
SQL>  EXPLAIN PLAN FOR SELECT empno,ename,job,sal,deptno FROM emp a
  2  WHERE EXISTS (SELECT NULL
  3  FROM emp2 b
  4  WHERE b.ename = a.ename AND b.job = a.job
  5  AND b.sal = a.sal) ;

Explained


SQL> select * from table(dbms_xplan.display());

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
Plan hash value: 4039873364
---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |     1 |    67 |     6   (0)| 00:00:01 |
|*  1 |  HASH JOIN SEMI    |      |     1 |    67 |     6   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| EMP  |    15 |   780 |     3   (0)| 00:00:01 |
|   3 |   TABLE ACCESS FULL| EMP2 |     4 |    60 |     3   (0)| 00:00:01 |
---------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - access("B"."ENAME"="A"."ENAME" AND "B"."JOB"="A"."JOB" AND
              "B"."SAL"="A"."SAL")
Note
-----
   - dynamic statistics used: dynamic sampling (level=2)

20 rows selected
  • 因为子查询的JOIN列(emp2.ename,emp2.job,ernp2.sal)没有重复行,所以这个查询可以直接改为INNER JOIN。
SQL> EXPLAIN PLAN  FOR  SELECT a.empno,a.ename,a.job,a.sal,a.deptno from emp a
  2  INNER JOIN emp2 b ON (b.ename = a.ename AND b.job = a.job AND b.sal =a.sal);

Explained


SQL> select * from table(dbms_xplan.display());

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
Plan hash value: 166525280
---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |     4 |   268 |     6   (0)| 00:00:01 |
|*  1 |  HASH JOIN         |      |     4 |   268 |     6   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| EMP2 |     4 |    60 |     3   (0)| 00:00:01 |
|   3 |   TABLE ACCESS FULL| EMP  |    15 |   780 |     3   (0)| 00:00:01 |
---------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - access("B"."ENAME"="A"."ENAME" AND "B"."JOB"="A"."JOB" AND
              "B"."SAL"="A"."SAL")
Note
-----
   - dynamic statistics used: dynamic sampling (level=2)

20 rows selected

或许与大家想象的不一样,以上三个PLAN中JOIN写法利用了HASH JOIN(哈希连接),其他两种运用的都是HASH JOIN SEMI(哈希半连接),说明在这个语句中的IN与EXISTS效率是一样的。所以,在不知哪种写法高效时应查看PLAN,而不是去记固定的结论

三、INNER JOIN、LEFT JOIN、RIGHT JOIN 、FULL JOIN、自关联解析

有很多人对这几种连接方式,特别是LEFT JOIN与RIGHT JOIN分不清,下面通过案例来解析一下。
首先建立两个测试用表。

DROP TABLE L PURGE ; DROP TABLE R PURGE;
--左表
CREATE TABLE L AS
SELECT 'left_1'  AS  str ,  '1' AS  v  FROM  dual  UNION  ALL 
SELECT 'left_2'  AS  str ,  '2' AS  v  FROM  dual  UNION  ALL 
SELECT 'left_3'  AS  str ,  '3' AS  v  FROM  dual  UNION  ALL 
SELECT 'left_4'  AS  str ,  '4' AS  v  FROM  dual;
--右表
CREATE TABLE R AS
SELECT 'right_3'  AS  str ,  '3' AS  v,1 as status  FROM  dual  UNION  ALL
SELECT 'right_4'  AS  str ,  '4' AS  v,0 as status  FROM  dual  UNION  ALL
SELECT 'right_5'  AS  str ,  '5' AS  v,0 as status  FROM  dual  UNION  ALL
SELECT 'right_6'  AS  str ,  '6' AS  v,0 as status  FROM  dual; 

1、INNER JOIN 的 特点

该方式返回两表相匹配的数据,左表的"1、2"以及右表的"5、6"都没有显示。
JOIN写法

SQL> 
SQL> select l.str as left_str, r.str as right_str
  2    from l
  3   inner join r
  4      on (l.v = r.v)
  5   order by 1, 2;

LEFT_STR RIGHT_STR
-------- ---------
left_3   right_3
left_4   right_4

2、LEFTJOIN的特点

该方式的左表为主表,左表返回所有的数据,右表中只返回与左表匹配的数据,"5、6"都没有显示。
join写法:

select l.str as left_str, r.str as right_str
  from l
 left join r
    on (l.v = r.v)
 order by 1, 2;
LEFT_STR RIGHT_STR
-------- ---------
left_1   
left_2   
left_3   right_3
left_4   right_4

加(+)写法

select l.str as left_str, r.str as right_str
  from l, r
    where l.v = r.v(+)
 order by 1, 2;

3、RIGHT JOIN的特点

该方式的右表为主表,左表中只返回与右表匹配的数据"3、4",而"1、2"都没有显示,右表返回所有的数据。
join写法

select l.str as left_str, r.str as right_str
  from l
 right join r
    on (l.v = r.v)
 order by 1, 2;
LEFT_STR RIGHT_STR
-------- ---------
left_3   right_3
left_4   right_4
         right_5
         right_6

加(+)写法

select l.str as left_str, r.str as right_str
  from l, r
    where l.v(+) = r.v
 order by 1, 2;

4、FULL JOIN的特点

该方式的左右表均返回所有的数据,但只有相匹配的数据显示在同一行,非匹配的行只显示一个表的数据。
JOIN写法

select l.str as left_str, r.str as right_str
  from l
 full join r
    on (l.v = r.v)
 order by 1, 2;
LEFT_STR RIGHT_STR
-------- ---------
left_1   
left_2   
left_3   right_3
left_4   right_4
         right_5
         right_6

6 rows selected

FULL JOIN 无(+ )的写法。

5、自关联

表emp中有一个字段mgr,其中是主管的编码(对应于emp.empno),如:
(EMPNO:7698,ENAME:BLAKE)-->(MGR:7839)-->(EMPNO:7839,ENAME:KING),说明BLAKE的主管就是KING
如何根据这个信息返回主管的姓名呢?
这里用到的就是自关联。也就是两次查询表emp,分别取不同的别名,这样就可以当作是两个表,后面的任务就是将这两个表和JOIN连接起来就可以。
为了方便理解,这里用汉字作为别名,并把相关列一起返回。

SELECT 员工.empno AS 职工编码,
       员工.ename AS 职工姓名,
       员工.job   AS 工作,
       员工.mgr   AS 员工表_主管编码,
       主管.empno AS 主管表_主管编码,
       主管.ename AS 主管姓名
  FROM emp 员工
  LEFT JOIN emp 主管
    ON (员工.mgr = 主管.empno)
 ORDER BY 1;
 职工编码 职工姓名   工作      员工表_主管编码 主管表_主管编码 主管姓名
----- ---------- --------- -------- -------- ----------
 1001 test                                   
 7369 SMITH      CLERK         7902     7902 FORD
 7499 ALLEN      SALESMAN      7698     7698 BLAKE
 7521 WARD       SALESMAN      7698     7698 BLAKE
 7566 JONES      MANAGER       7839     7839 KING
 7654 MARTIN     SALESMAN      7698     7698 BLAKE
 7698 BLAKE      MANAGER       7839     7839 KING
 7782 CLARK      MANAGER       7839     7839 KING
 7788 SCOTT      ANALYST       7566     7566 JONES
 7839 KING       PRESIDENT                   
 7844 TURNER     SALESMAN      7698     7698 BLAKE
 7876 ADAMS      CLERK         7788     7788 SCOTT
 7900 JAMES      CLERK         7698     7698 BLAKE
 7902 FORD       ANALYST       7566     7566 JONES
 7934 MILLER     CLERK         7782     7782 CLARK

15 rows selected

总结

这一章主要介绍两块,之所以拿出来这两块说是因为:

  • IN、EXISTS 和 INNER JOIN这三者或则说前两者的效率,博主在日常工作和面试过程中,经常遇到大家斩钉截铁的说in效率远远低于EXISTS 和 INNER JOIN,这类人大都是自己没有亲测,从网上搜了相关信息就记下来了,有些时候,网上的内容并不代表绝对正确,就像网上很多文章说scala的入参不能超过22个参数一样~
  • 其次,表的INNER JOIN、LEFT JOIN、RIGHT JOIN 、FULL JOIN、自关联这5种关联和简写方式,在工作中也很容易出错,所以在写这篇文章时候,博主自己也做个总结~

SQL开发实战技巧系列:从执行计划看NOT INNOT EXISTS 和 LEFT JOIN效率,记住内外关联条件不要乱放

系列文章目录

【SQL开发实战技巧】系列(一):关于SQL不得不说的那些事
【SQL开发实战技巧】系列(二):简单单表查询
【SQL开发实战技巧】系列(三):SQL排序的那些事
【SQL开发实战技巧】系列(四):从执行计划讨论UNION ALL与空字符串&UNION与OR的使用注意事项
【SQL开发实战技巧】系列(五):从执行计划看IN、EXISTS 和 INNER JOIN效率,我们要分场景不要死记网上结论
【SQL开发实战技巧】系列(六):从执行计划看NOT IN、NOT EXISTS 和 LEFT JOIN效率,记住内外关联条件不要乱放
【SQL开发实战技巧】系列(七):从有重复数据前提下如何比较出两个表中的差异数据及对应条数聊起
【SQL开发实战技巧】系列(八):聊聊如何插入数据时比约束更灵活的限制数据插入以及怎么一个insert语句同时插入多张表
【SQL开发实战技巧】系列(九):一个update误把其他列数据更新成空了?Merge改写update!给你五种删除重复数据的写法!
【SQL开发实战技巧】系列(十):从拆分字符串、替换字符串以及统计字符串出现次数说起
【SQL开发实战技巧】系列(十一):拿几个案例讲讲translate|regexp_replace|listagg|wmsys.wm_concat|substr|regexp_substr常用函数
【SQL开发实战技巧】系列(十二):三问(如何对字符串字母去重后按字母顺序排列字符串?如何识别哪些字符串中包含数字?如何将分隔数据转换为多值IN列表?)
【SQL开发实战技巧】系列(十三):讨论一下常用聚集函数&通过执行计划看sum()over()对员工工资进行累加
【SQL开发实战技巧】系列(十四):计算消费后的余额&计算银行流水累计和&计算各部门工资排名前三位的员工
【SQL开发实战技巧】系列(十五):查找最值所在行数据信息及快速计算总和百之max/min() keep() over()、fisrt_value、last_value、ratio_to_report
【SQL开发实战技巧】系列(十六):数据仓库中时间类型操作(初级)日、月、年、时、分、秒之差及时间间隔计算
【SQL开发实战技巧】系列(十七):数据仓库中时间类型操作(初级)确定两个日期之间的工作天数、计算—年中周内各日期出现次数、确定当前记录和下一条记录之间相差的天数
【SQL开发实战技巧】系列(十八):数据仓库中时间类型操作(进阶)INTERVAL、EXTRACT以及如何确定一年是否为闰年及周的计算
【SQL开发实战技巧】系列(十九):数据仓库中时间类型操作(进阶)如何一个SQL打印当月或一年的日历?如何确定某月内第一个和最后—个周内某天的日期?
【SQL开发实战技巧】系列(二十):数据仓库中时间类型操作(进阶)获取季度开始结束时间以及如何统计非连续性时间的数据
【SQL开发实战技巧】系列(二十一):数据仓库中时间类型操作(进阶)识别重叠的日期范围,按指定10分钟时间间隔汇总数据
【SQL开发实战技巧】系列(二十二):数仓报表场景☞ 从分析函数效率一定快吗聊一聊结果集分页和隔行抽样实现方式
【SQL开发实战技巧】系列(二十三):数仓报表场景☞ 如何对数据排列组合去重以及通过如何找到包含最大值和最小值的记录这个问题再次用执行计划给你证明分析函数性能不一定高
【SQL开发实战技巧】系列(二十四):数仓报表场景☞通过案例执行计划详解”行转列”,”列转行”是如何实现的
【SQL开发实战技巧】系列(二十五):数仓报表场景☞结果集中的重复数据只显示一次以及计算部门薪资差异高效的写法以及如何对数据进行快速分组
【SQL开发实战技巧】系列(二十六):数仓报表场景☞聊聊ROLLUP、UNION ALL是如何分别做分组合计的以及如何识别哪些行是做汇总的结果行


文章目录


前言

本篇文章讲解的主要内容是:从执行计划看NOT IN、NOT EXISTS 和 LEFT JOIN效率,还是那就话,别死记网上结论、在使用内外关联时,特别是简写方式时记住关联条件不要乱放!
【SQL开发实战技巧】这一系列博主当作复习旧知识来进行写作,毕竟SQL开发在数据分析场景非常重要且基础,面试也会经常问SQL开发和调优经验,相信当我写完这一系列文章,也能再有所收获,未来面对SQL面试也能游刃有余~。


一、从执行计划看NOT IN、NOT EXISTS 和 LEFT JOIN效率

有些单位的部门(如40)中一个员工也没有,只是设了一个部门名字,如下列语句:

select count(*) from dept where deptno=40;

如何通过关联查询把这些信息查出来?
同样有三种写法:NOT IN、NOT EXISTS 和LEFT JOIN
语句及PLAN如下(版本为11.2.0.4.0 )。
环境:

alter table dept add constraints pk_dept primary key (deptno); --如果你有就不用建了
  • NOT IN用法
EXPLAIN PLAN FOR select *
FROM dept
WHERE deptno NOT IN (SELECT emp.deptno FROM emp WHERE emp.deptno IS NOT NULL);
SELECT * FROM TABLE(dbms_xplan.display());

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
Plan hash value: 1353548327
--------------------------------------------------------------------------------
| Id  | Operation                    | Name    | Rows  | Bytes | Cost (%CPU)| Ti
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |         |     1 |    23 |     6  (17)| 00
|   1 |  MERGE JOIN ANTI             |         |     1 |    23 |     6  (17)| 00
|   2 |   TABLE ACCESS BY INDEX ROWID| DEPT    |     4 |    80 |     2   (0)| 00
|   3 |    INDEX FULL SCAN           | PK_DEPT |     4 |       |     1   (0)| 00
|*  4 |   SORT UNIQUE                |         |    14 |    42 |     4  (25)| 00
|*  5 |    TABLE ACCESS FULL         | EMP     |    14 |    42 |     3   (0)| 00
--------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   4 - access("DEPTNO"="EMP"."DEPTNO")
       filter("DEPTNO"="EMP"."DEPTNO")
   5 - filter("EMP"."DEPTNO" IS NOT NULL)

19 rows selected
  • NOT EXISTS 用法
EXPLAIN PLAN FOR SELECT*
FROM dept
WHERE NOT EXISTS ( SELECT NULL FROM emp WHERE emp.deptno  =  dept.deptno) ; 
SELECT * FROM TABLE(dbms_xplan.display());
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
Plan hash value: 1353548327
--------------------------------------------------------------------------------
| Id  | Operation                    | Name    | Rows  | Bytes | Cost (%CPU)| Ti
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |         |     1 |    23 |     6  (17)| 00
|   1 |  MERGE JOIN ANTI             |         |     1 |    23 |     6  (17)| 00
|   2 |   TABLE ACCESS BY INDEX ROWID| DEPT    |     4 |    80 |     2   (0)| 00
|   3 |    INDEX FULL SCAN           | PK_DEPT |     4 |       |     1   (0)| 00
|*  4 |   SORT UNIQUE                |         |    14 |    42 |     4  (25)| 00
|*  5 |    TABLE ACCESS FULL         | EMP     |    14 |    42 |     3   (0)| 00
--------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   4 - access("EMP"."DEPTNO"="DEPT"."DEPTNO")
       filter("EMP"."DEPTNO"="DEPT"."DEPTNO")
   5 - filter("EMP"."DEPTNO" IS NOT NULL)

19 rows selected
  • LEFT JOIN 用法

根据前面介绍过的左联知识,LEFT JOIN 取出的是左表中所有的数据,其中与右表不匹配的就表示左表NOT IN右表。
所以这里LEFT JOIN加上条件TS NULL,就是LEFT JOIN的写法:

EXPLAIN PLAN FOR
SELECT dept.*
FROM dept
LEFT JOIN emp ON emp.deptno = dept.deptno WHERE emp.deptno IS NULL;

SELECT * FROM TABLE(dbms_xplan.display());

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
Plan hash value: 1353548327
--------------------------------------------------------------------------------
| Id  | Operation                    | Name    | Rows  | Bytes | Cost (%CPU)| Ti
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |         |     1 |    23 |     6  (17)| 00
|   1 |  MERGE JOIN ANTI             |         |     1 |    23 |     6  (17)| 00
|   2 |   TABLE ACCESS BY INDEX ROWID| DEPT    |     4 |    80 |     2   (0)| 00
|   3 |    INDEX FULL SCAN           | PK_DEPT |     4 |       |     1   (0)| 00
|*  4 |   SORT UNIQUE                |         |    14 |    42 |     4  (25)| 00
|*  5 |    TABLE ACCESS FULL         | EMP     |    14 |    42 |     3   (0)| 00
--------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   4 - access("EMP"."DEPTNO"="DEPT"."DEPTNO")
       filter("EMP"."DEPTNO"="DEPT"."DEPTNO")
   5 - filter("EMP"."DEPTNO" IS NOT NULL)

19 rows selected

通过看上面的执行计划,三个SQL用的都是 MERGE JOIN ANTI, 说明这三种方法的效率一样。
如果想改写,就要对比改写前后的PLAN,根据PLAN来判断并测试哪种方法的效率高,一定要记住不能凭借某些结论来碰运气。

二、外连接中的条件不要乱放,建议大家使用join而非(+)

对于系列三博客介绍的左联语句,见下面的数据。

SELECT l.str AS left_str, r.str AS right_str,r.status FROM l
LEFT JOIN r	ON l.v = r.v
ORDER BY 1 , 2 ;
LEFT_STR RIGHT_STR     STATUS
-------- --------- ----------
left_1             
left_2             
left_3   right_3            1
left_4   right_4            0

那现在有这么一个需求:对于其中的L表,四条数据都返回。而对于R表,我们需要只显示其中的status=1的数据,也就是下面这样的结果:

LEFT_STR RIGHT_STR     STATUS
-------- --------- ----------
left_1             
left_2             
left_3   right_3            1
left_4             

对于这个需求,可能有些人会加一个where条件!然后结果就变成了下面这样了:
left join写法:

SELECT l.str AS left_str, r.str AS right_str,r.status 
 FROM l
LEFT JOIN r  ON (l.v = r.v)
where  r.status=1
ORDER BY 1 , 2;
LEFT_STR RIGHT_STR     STATUS
-------- --------- ----------
left_3   right_3            1

(+)写法:

SELECT l.str AS left_str, r.str AS right_str, r.status
  FROM l, r
 where l.v = r.v(+)
   and r.status = 1
 ORDER BY 1, 2;
LEFT_STR RIGHT_STR     STATUS
-------- --------- ----------
left_3   right_3            1

而此时的执行计划:

SQL> EXPLAIN PLAN FOR
  2  SELECT l.str AS left_str, r.str AS right_str,r.status
  3   FROM l
  4  LEFT JOIN r  ON (l.v = r.v)
  5  where  r.status=1
  6  ORDER BY 1 , 2;

Explained

SQL> SELECT * FROM TABLE(dbms_xplan.display());

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
Plan hash value: 688663707
----------------------------------------------------------------------------
| Id  | Operation           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |     2 |    42 |     7  (15)| 00:00:01 |
|   1 |  SORT ORDER BY      |      |     2 |    42 |     7  (15)| 00:00:01 |
|*  2 |   HASH JOIN         |      |     2 |    42 |     6   (0)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL| R    |     2 |    24 |     3   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL| L    |     4 |    36 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("L"."V"="R"."V")
   3 - filter("R"."STATUS"=1)

17 rows selected

很明显,结果以及执行计划(HASH JOIN)与我们期望得到的结果都不一致!!!这是很多人在写查询或更改查询时常遇到的一种错误。问题就在于所加条件的位置及写法,正确的写法分别如下:

SQL> SELECT l.str AS left_str, r.str AS right_str, r.status
  2    FROM l
  3    LEFT JOIN r
  4      ON (l.v = r.v and r.status = 1)
  5   ORDER BY 1, 2;

LEFT_STR RIGHT_STR     STATUS
-------- --------- ----------
left_1             
left_2             
left_3   right_3            1
left_4             



SQL> SELECT l.str AS left_str, r.str AS right_str, r.status
  2    FROM l, r
  3   where l.v = r.v(+)
  4     and r.status(+) = 1
  5   ORDER BY 1, 2;

LEFT_STR RIGHT_STR     STATUS
-------- --------- ----------
left_1             
left_2             
left_3   right_3            1
left_4             

看一下这时候的执行计划:

SQL> EXPLAIN PLAN FOR
  2  SELECT l.str AS left_str, r.str AS right_str, r.status
  3    FROM l
  4    LEFT JOIN r
  5      ON (l.v = r.v and r.status = 1)
  6   ORDER BY 1, 2;

Explained


SQL> SELECT * FROM TABLE(dbms_xplan.display());

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
Plan hash value: 2310059642
----------------------------------------------------------------------------
| Id  | Operation           | Name | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |     4 |    84 |     7  (15)| 00:00:01 |
|   1 |  SORT ORDER BY      |      |     4 |    84 |     7  (15)| 00:00:01 |
|*  2 |   HASH JOIN OUTER   |      |     4 |    84 |     6   (0)| 00:00:01 |
|   3 |    TABLE ACCESS FULL| L    |     4 |    36 |     3   (0)| 00:00:01 |
|*  4 |    TABLE ACCESS FULL| R    |     2 |    24 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("L"."V"="R"."V"(+))
   4 - filter("R"."STATUS"(+)=1)

17 rows selected

以上两种写法结果均正确,且根据执行计划HASH JOIN OUTER明确走的是外连接。而且根据上面查询我们能够看出来JOIN的方式明显更容易辨别,这也是我反复建议使用JOIN的原因。
对于上面SQL我们还可以使用先过滤再关联的方式,即R表先过滤:

(select * from r where status=1) r

总结

同上一篇博客所说,在使用in exists或则NOT IN、NOT EXISTS 和 LEFT JOIN时候,不要想当然的认为in和not in效率极其低下,在本章案例中通过执行计划能够直观的看到,三者效率竟然一致了!!所以,读万卷书不如行万里路,网上别人做的总结再好,也不如自己实践一把来的真实。还有就是,在使用关联查询时候,关联条件和过滤条件一定要想好放哪里,不然你会想当然的错了!

以上是关于SQL开发实战技巧系列:从执行计划看INEXISTS 和 INNER JOIN效率,我们要分场景不要死记网上结论的主要内容,如果未能解决你的问题,请参考以下文章

SQL开发实战技巧系列(十三):讨论一下常用聚集函数&通过执行计划看sum()over()对员工工资进行累加

SQL开发实战技巧系列:从执行计划讨论UNION ALL与空字符串&UNION与OR的使用注意事项

SQL开发实战技巧系列:SQL排序的那些事

SQL开发实战技巧系列:关于SQL不得不说的那些事

SQL开发实战技巧系列(三十四):数仓报表场景☞如何对数据分级并行转为列

SQL开发实战技巧系列(二十二):数仓报表场景☞ 从分析函数效率一定快吗聊一聊结果集分页和隔行抽样实现方式