参考资料:
本系列博客主要参考资料有CUUG冉乃纲老师数据库教学笔记,《SQL优化核心思想》(罗炳森,黄超,钟侥著),《PostgreSQL技术内幕:查询优化深度探索》(张树杰著),排名不分先后。
3 连接顺序专题
3.1 外连接嵌套循环驱动表问题
网络上经常说外连接走嵌套循环,驱动表顺序不能变,那走HASH驱动表为什么可以换?
首先,说明外连接走嵌套循环为什么驱动表不能换,外连接嵌套循环是外表给内表传值,内表依附于外表,所以连接顺序不可以变。
3.2 半连接嵌套循环表之间的关系
半连接可以改写为被驱动表去重复后和驱动表内连接,那么问题来了,既然是内连接,那么为什么走嵌套循环半连接时为什么不可以改驱动表呢,就是关键字,去重复,因为半连接匹配子查询后只要一条成功后,只返回驱动表数据,也就意味着子查询的连接列有重复值时最后返回的数据也只是主表的数据,不会翻倍,我们要以嵌套循环的方式,并且要改变驱动顺序,只能给子表去重复,而不能直接改变驱动顺序。
3.3 反连接嵌套循环表之间的关系
反连接可以改写为左连接+where 内表连接条件 is null,那么,在走嵌套循环时和外连接一样,驱动表也不可以换。
但是不同于半连接,为什么内表不要去重复呢?
首先,我们想想半连接改内连接去重复的原因是什么,防止子表连接列值重复产生数据翻倍。但是反连接呢,是要求内表连接条件 is null,是要求返回主表没有匹配上的数据,既然前提是没有匹配上,也就不存在数据翻倍问题,就不用去重复了。
3.4 HASH连接驱动表问题
HASH连接的驱动表是可以换的,不管是外连接,内连接,还有反连接,原因很简单,HASH连接需要连接的表都进行全扫描,全部匹配,并且只匹配一次,那么连接顺序就可以变化了。
3.5 优化器为什么喜欢内连接
本节参考《PostgreSQL技术内幕:查询优化深度探索》3.8 外连接消除和4.3 谓词下推
我们在<半连接和内连接转换>中知道了,优化器自动将半连接转换成了内连接,有时候还会产生性能问题,当时,我们也知道了内连接的优点,就是内连接驱动表可以灵活选在,可以降低优化复杂性。
除此之外,外连接有时候还会阻止谓词下推
SQL优化中有谓词推入,也就是说将筛选条件或者连接条件推入到视图中,实现提前过滤或让嵌套循环充分利用索引,实现优化;谓词下推是另一个概念。
SQL的约束条件,分为连接条件和过滤条件,过滤条件肯定不会增加数据返回量,而连接条件可能会使数据翻倍,如果将连接条件推到过滤条件,也就是谓词下推,那么对于SQL是性能提升操作。
那我们通过案例说明。
首先,我们准备数据
create table tests1 as select * from dba_objects;
create table tests2 as select * from dba_objects;
create index ix_tests1_id on tests1(object_id);
create index ix_tests2_id on tests2(object_id);
create index ix_tests1_ownerid on tests1(owner,object_id);
create index ix_tests2_ownerid on tests2(owner,object_id);
※本案例应该不用建索引,但是实验时我忘记删除了,就那么留着吧,对本示例影响不大。
SELECT COUNT(T1.OWNER) ,COUNT(T2.OBJECT_ID)
FROM TESTS1 T1 INNER JOIN
TESTS2 T2 ON T1.OWNER='SCOTT';
SELECT COUNT(T1.OWNER) ,COUNT(T2.OBJECT_ID)
FROM TESTS1 T1 LEFT JOIN
TESTS2 T2 ON T1.OWNER='SCOTT';
内连接时,连接列下推到驱动表TESTS1上,实现提前过滤,左联接时,这个条件无法下推,本案例中两条语句语义不同,但是通过谓词下推,可以理解优化器为什么独爱内连接。
反连接与左联接相同,也无法实现谓词下推;但是半连接可以实现谓词下推,优化器之所以有时候将半连接改为内连接,还是驱动表的灵活性问题。
本专题是理论性说明,关于半连接和反连接,就不再准备案例验证了。
3.6 谓词下推优化
通过<连接顺序专题-优化器为什么喜欢内连接>,我们了解了谓词下推问题。下面,我们构建一个谓词下推的优化案例。但是注意一个问题,我们这里的改写是非等价改写,只是为了演示一下谓词下推问题。
3.6.1 环境准备
create table tests1 as select * from dba_objects;
create table tests2 as select * from tests1;
create table tests3 as select * from tests1;
INSERT INTO tests2 SELECT * FROM TESTS2;
INSERT INTO tests2 SELECT * FROM TESTS2;
INSERT INTO tests2 SELECT * FROM TESTS2;
INSERT INTO tests2 SELECT * FROM TESTS2;
create index ix_tests2_id on tests2(object_id);
commit;
3.6.1谓词下推
下面先看第一条SQL,因为过滤条件T2.OBJECT_ID <1000在和TESTS3左连之后,也就是说这个过滤条件无法推到TESTS2上。
SELECT COUNT(*)
FROM TESTS1 T1
LEFT JOIN TESTS2 T2
ON T1.OBJECT_ID=T2.OBJECT_ID
LEFT JOIN TESTS3 T3
ON T2.OBJECT_ID=T3.OBJECT_ID
AND T2.OBJECT_ID <1000;
这执行计划狠,连物理读都出来了。基本上是废了。
但是我们把T2.OBJECT_ID <1000换到TESTS2左连之后,这样,过滤条件可以下推到TESTS2上,提前过滤,减少IO成本。
SELECT COUNT(*)
FROM TESTS1 T1
LEFT JOIN TESTS2 T2
ON T1.OBJECT_ID=T2.OBJECT_ID
AND T2.OBJECT_ID <1000
LEFT JOIN TESTS3 T3
ON T2.OBJECT_ID=T3.OBJECT_ID;
比较一下,是不是质的飞跃。
上面改写之前之所会造成谓词无法下推,全是外连接惹的祸,假设我们改成内连接呢。
SELECT COUNT(*)
FROM TESTS1 T1
LEFT JOIN TESTS2 T2
ON T1.OBJECT_ID=T2.OBJECT_ID
INNER JOIN TESTS3 T3
ON T2.OBJECT_ID=T3.OBJECT_ID
AND T2.OBJECT_ID <1000;
因为最后是inner join,所以过滤条件不止是推到TESTS2,还一路扩张,推到了TESTS1和TESTS3上。
通过此案例,更深一层理解了,为什么优化器独爱内连接了,简单说就是驱动表选择灵活+谓词下推灵活。