Oracle外连接中on和where的区别
Oracle外连接中on和where的区别
前言
慢慢接触SQL优化,必然会涉及到CBO的内容以及SQL改写的东西。
这不,最近遇到左连接中on和where条件的问题,花了点时间搞清楚了。
首先,网上百度是有答案的,但是只能让你记住结论,无法明白理解透彻,所以还是要自己做实验,把每个有疑问的点尽量都搞清楚就差不多了。
而且,对于百度出来的内容,还是有相当一部分是存在问题的。
本文除了最终搞明白on和where的区别外,还是引出其他一些现象加深理解或者作为拓展,顿序渐进。
构造环境
数据库版本是11.2.0.4.0的,表和数据就随便构造就行了。
create table a (id number,name varchar2(20)); create table b (id number,name varchar2(20)); insert into a values(1,'a'); insert into a values(2,'b'); insert into a values(3,null); insert into a values(4,'d'); insert into a values(5,null); insert into a values(6,'a'); insert into b values(1,'a'); insert into b values(2,'b'); insert into b values(3,'c'); insert into b values(4,null); insert into b values(5,null); insert into b values(6,'a'); commit; create index idx_a_id on a(id); create index idx_a_name on a(name); create index idx_b_id on b(id); create index idx_b_name on b(name); exec dbms_stats.gather_table_stats(ownname => 'ZKM',tabname => 'A',estimate_percent => 100,method_opt => 'FOR ALL COLUMNS SIZE REPEAT',degree => 1 ,no_invalidate => false); exec dbms_stats.gather_table_stats(ownname => 'ZKM',tabname => 'B',estimate_percent => 100,method_opt => 'FOR ALL COLUMNS SIZE REPEAT',degree => 1 ,no_invalidate => false);
查询结果如下:
22:17:31 ZKM@test(733)> select * from a; ID NAME ---------- ------------------------------------------------------------ 1 a 2 b 3 4 d 5 6 a 6 rows selected. Elapsed: 00:00:00.00 22:35:25 ZKM@test(733)> select * from b; ID NAME ---------- ------------------------------------------------------------ 1 a 2 b 3 c 4 5 6 a 6 rows selected. Elapsed: 00:00:00.00
奇怪的现象1
先来看看一个奇怪的现象,以左连接为例子,如下查询结果为什么加了条件a.name='a'之后,还会出现a.name除了'a'之外的所有记录
22:42:19 ZKM@test(733)> select a.*,b.* from a left join b on a.name=b.name and a.name='a'; ID NAME ID NAME --- ---- --- ---- 6 a 1 a 1 a 1 a 6 a 6 a 1 a 6 a 5 3 2 b 4 d 8 rows selected. Elapsed: 00:00:00.02
外连接概念
想解释这个奇怪的现象1,这里还是必须说一下外连接的概念的:
外连接(Outer Join)是对内连接的一种拓展,它是指表连接的连接结果除了包含那些完全满足连接条件的记录之外还会包含驱动表中所有不满足该连接条件的记录。
以左连接为例子,左连接的语法(之一)为:
目标表1 left outer join 目标表2 on (连接条件)
“目标表1 left outer join 目标表2 on (连接条件)”的含义为目标表1和目标表2按括号中的连接条件来做表连接,位于关键字“left outer join”左边的目标表1会作为该表连接的驱动表(关键字“left outer”即表明位置处于left的表就是outer table,这里的outer table就是指驱动表)。
此时的连接结果除了包含目标表1和目标表2中所有满足该连接条件的记录外,还会包含驱动表(即目标表1)中的所有不满足该连接条件的记录,同时,驱动表中所有不满足该连接条件的记录所对应的被驱动表(即目标表2)中的查询列均会以NULL值来填充。
选自《基于Oracle的SQL优化》,P37,有删减但不影响原意。
解释现象1
SQL语句为:
select a.*,b.* from a left join b on a.name=b.name and a.name='a';
按照外连接的概念,结果集中一定会有满足on后边连接条件的记录(a.name=b.name and a.name='a'),这一部分的记录为:
ID NAME ID NAME --- ---- --- ---- 6 a 1 a 1 a 1 a 6 a 6 a 1 a 6 a
同时,还要包括驱动表,就是a表的不满足连接条件(a.name=b.name and a.name='a')的记录:
ID NAME ID NAME --- ---- --- ---- 5 3 2 b 4 d
合并起来刚刚好就是8条记录。
这就是为什么加了条件a.name='a'之后,还会出现a.name除了'a'之外的所有记录。
换句话说,on后边的条件限制不了a表的记录,但却是可以限制只有a.name='a'的记录和b表进行关联(因为并没有出现"2 b 2 b"这条记录,而是出现了"2 b ")。
所以说,on的连接条件只会影响结果,而不会影响结果的数量。
因此,在这个案例中a表一定是全表扫描,有索引的话就算是hint强制使用索引也无法生效(如下示例)。
但是因为b表不需要全部数据,所以是理论上可以用上idx_b_name这个索引的。
你只想取a.name='a'的记录的话,把a.name='a'写在on后边是无法实现的。
09:35:59 ZKM@test(402)> select /*+ index(a idx_a_name) use_nl(b) */ a.*,b.* from a left join b on a.name=b.name and a.name='a'; 8 rows selected. Elapsed: 00:00:00.00 Execution Plan ---------------------------------------------------------- Plan hash value: 3004495171 ------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 6 | 60 | 7 (0)| 00:00:01 | | 1 | NESTED LOOPS OUTER | | 6 | 60 | 7 (0)| 00:00:01 | | 2 | TABLE ACCESS FULL | A | 6 | 30 | 3 (0)| 00:00:01 | | 3 | TABLE ACCESS BY INDEX ROWID| B | 1 | 5 | 1 (0)| 00:00:01 | |* 4 | INDEX RANGE SCAN | IDX_B_NAME | 1 | | 0 (0)| 00:00:01 | ------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 4 - access("A"."NAME"="B"."NAME"(+)) filter("B"."NAME"(+) IS NOT NULL AND "A"."NAME"=CASE WHEN ("B"."NAME"(+) IS NOT NULL) THEN 'a' ELSE 'a' END ) Statistics ---------------------------------------------------------- 1 recursive calls 0 db block gets 12 consistent gets 0 physical reads 0 redo size 847 bytes sent via SQL*Net to client 524 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 8 rows processed
where和on的区别
将上边的语句SQL 1
select a.*,b.* from a left join b on a.name=b.name and a.name='a';
改为如下SQL 2:
select a.*,b.* from a left join b on a.name=b.name where a.name='a';
只针对外连接而言,以左连接为例子,上述的SQL 1和SQL 2有什么区别?
先看看以下的现象。
奇怪的现象2及解释
查看执行结果:
08:43:39 ZKM@test(733)> select a.*,b.* from a left join b on a.name=b.name where a.name='a'; ID NAME ID NAME --- ---- --- ---- 1 a 1 a 1 a 6 a 6 a 1 a 6 a 6 a Elapsed: 00:00:00.00
那么到底是为什么?具体来说就是这样:
对于该限制条件a.name='a',它在SQL 1的文本中位于LEFT JOIN ON后边,这表示该限制条件会在表a和表b做连接之前就别应用在表a上,即在SQL 1上,参与左连接的a表的数据是那些满足条件"a.name='a'"的记录;
而该限制条件在SQL 2的SQL文本中位于where之后,这表示该限制条件在表a和表b做完左连接后,才会被应用在表a和表b的连接结果集上,即在SQL 2中,参与左连接的是表a的所有数据。
简单描述就是先进行on的外连接,然后对on之后的结果集做where过滤。
改自《基于Oracle的SQL优化》,P42。
奇怪的现象3
按照上边的解释,在SQL 2中,参与左连接的是表a的所有数据,那也就是a表也一定是全表扫描?
10:29:53 ZKM@test(402)> select /*+ use_nl(b) */ a.*,b.* from a left join b on a.name=b.name where a.name='a'; ID NAME ID NAME --- ---- --- ---- 1 a 1 a 1 a 6 a 6 a 1 a 6 a 6 a Elapsed: 00:00:00.00 Execution Plan ---------------------------------------------------------- Plan hash value: 906933200 ------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 2 | 20 | 3 (0)| 00:00:01 | | 1 | NESTED LOOPS OUTER | | 2 | 20 | 3 (0)| 00:00:01 | | 2 | TABLE ACCESS BY INDEX ROWID| A | 1 | 5 | 2 (0)| 00:00:01 | |* 3 | INDEX RANGE SCAN | IDX_A_NAME | 1 | | 1 (0)| 00:00:01 | | 4 | TABLE ACCESS BY INDEX ROWID| B | 1 | 5 | 1 (0)| 00:00:01 | |* 5 | INDEX RANGE SCAN | IDX_B_NAME | 1 | | 0 (0)| 00:00:01 | ------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - access("A"."NAME"='a') 5 - access("B"."NAME"(+)='a') Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 9 consistent gets 0 physical reads 0 redo size 803 bytes sent via SQL*Net to client 524 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 4 rows processed
从执行计划看,a表显然使用了索引idx_a_name将a.name='a'的数据过滤出来再进行外连接,这似乎与先on在where存在矛盾。
其实就SQL 2从结果而言,按照先on然后最后where过滤后,只会留下a.name='a'的数据。
因此,其实CBO在处理SQL时,是将SQL 2等价改写为如下语句了
select a_new.*,b.* from (select * from a where a.name='a') a_new left join b on a_new.name=b.name;
这样的话其实就比较好理解了。
由于被CBO等价改写之后,先进行where,而后当成新表在去left join on,这就必然和概念上的先on后where冲突(SQL都不同了冲突也正常)。
但是从性能上看,等价改写后不需要对a表全表扫描并且有能够使用索引的前提,显然要更优。
NOTE!!!
需要注意的一点,
select /*+ use_nl(b) */ a.*,b.* from a left join b on a.name=b.name where b.name='a';
并不等价于
select /*+ use_nl(b) */ a.*,b.* from a left join (select * from b where b.name='a') b on a.name=b.name;
而是会被CBO等价改写为内连接,即:
select /*+ use_nl(b) */ a.*,b.* from a , b where a.name=b.name and b.name='a';
因此,是否等价改写是需要看最终的结果的。
关于使用索引规律总结
有如下几条SQL及其对应的执行计划部分:
SQL 1: select a.*,b.* from a left join b on a.name=b.name and a.name='a'; SQL 2: select a.*,b.* from a left join b on a.name=b.name and b.name='a'; SQL 3: select a.*,b.* from a left join b on a.name=b.name where a.name='a'; SQL 4: select a.*,b.* from a left join b on a.name=b.name where b.name='a';
添加hint后的执行计划信息:
SQL 1(针对索引 idx_a_name): select /*+ index(a idx_a_name) */ a.*,b.* from a left join b on a.name=b.name and a.name='a'; ------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 6 | 60 | 5 (0)| 00:00:01 | |* 1 | HASH JOIN OUTER | | 6 | 60 | 5 (0)| 00:00:01 | | 2 | TABLE ACCESS FULL | A | 6 | 30 | 3 (0)| 00:00:01 | | 3 | TABLE ACCESS BY INDEX ROWID| B | 4 | 20 | 2 (0)| 00:00:01 | |* 4 | INDEX FULL SCAN | IDX_B_NAME | 4 | | 1 (0)| 00:00:01 | ------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - access("A"."NAME"="B"."NAME"(+) AND "A"."NAME"=CASE WHEN ("B"."NAME"(+) IS NOT NULL) THEN 'a' ELSE 'a' END ) 4 - filter("B"."NAME"(+) IS NOT NULL) SQL 2(针对索引 idx_b_name): select /*+ index(b idx_b_name) */ a.*,b.* from a left join b on a.name=b.name and b.name='a'; Plan hash value: 3915125 ------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 6 | 60 | 5 (0)| 00:00:01 | |* 1 | HASH JOIN OUTER | | 6 | 60 | 5 (0)| 00:00:01 | | 2 | TABLE ACCESS FULL | A | 6 | 30 | 3 (0)| 00:00:01 | | 3 | TABLE ACCESS BY INDEX ROWID| B | 1 | 5 | 2 (0)| 00:00:01 | |* 4 | INDEX RANGE SCAN | IDX_B_NAME | 1 | | 1 (0)| 00:00:01 | ------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - access("A"."NAME"="B"."NAME"(+)) 4 - access("B"."NAME"(+)='a') SQL 3(针对索引 idx_a_name): select /*+ index(a idx_a_name) */ a.*,b.* from a left join b on a.name=b.name where a.name='a'; -------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | -------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 2 | 20 | 3 (0)| 00:00:01 | | 1 | MERGE JOIN OUTER | | 2 | 20 | 3 (0)| 00:00:01 | | 2 | TABLE ACCESS BY INDEX ROWID | A | 1 | 5 | 2 (0)| 00:00:01 | |* 3 | INDEX RANGE SCAN | IDX_A_NAME | 1 | | 1 (0)| 00:00:01 | | 4 | BUFFER SORT | | 1 | 5 | 1 (0)| 00:00:01 | | 5 | TABLE ACCESS BY INDEX ROWID| B | 1 | 5 | 1 (0)| 00:00:01 | |* 6 | INDEX RANGE SCAN | IDX_B_NAME | 1 | | 0 (0)| 00:00:01 | -------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - access("A"."NAME"='a') 6 - access("B"."NAME"(+)='a') SQL 4(针对索引 idx_b_name): select /*+ index(b idx_b_name) use_hash(b) */ a.*,b.* from a left join b on a.name=b.name where b.name='a'; -------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | -------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 2 | 20 | 3 (0)| 00:00:01 | | 1 | MERGE JOIN CARTESIAN | | 2 | 20 | 3 (0)| 00:00:01 | | 2 | TABLE ACCESS BY INDEX ROWID | A | 1 | 5 | 2 (0)| 00:00:01 | |* 3 | INDEX RANGE SCAN | IDX_A_NAME | 1 | | 1 (0)| 00:00:01 | | 4 | BUFFER SORT | | 1 | 5 | 1 (0)| 00:00:01 | | 5 | TABLE ACCESS BY INDEX ROWID| B | 1 | 5 | 1 (0)| 00:00:01 | |* 6 | INDEX RANGE SCAN | IDX_B_NAME | 1 | | 0 (0)| 00:00:01 | -------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - access("A"."NAME"='a') 6 - access("B"."NAME"='a')
其中,只有SQL1由于表a最后的数据一定是全部需要的因此一定会对a表进行全表扫描导致hint失效。
同理,SQL2中条件b.name='a'是相对被驱动表b而言,不一定会需要b表的全部数据,因此有使用索引的前提条件。
SQL 2 其实也相当于
select /*+ index(b idx_b_name) */ a.*,b.* from a left join (select * from b where b.name='a') b on a.name=b.name;
而SQL 3和SQL 4其实由于被等价改写了,因此也使用了索引。
(SQL 4被等价改写为内连接了发现没有)。
留下一个问题:
什么时候外连接能够被改成内连接。
参考链接/书籍
https://blog.csdn.net/spw55381155/article/details/89638558
https://blog.csdn.net/weixin_39827145/article/details/105556520
《基于Oracle的SQL优化》
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?