Greenplum查询优化器如何消除外连接揭秘
Greenplum经过多年打磨,以性能卓越,速度快胜任不同类型的查询场景。Greenplum之所以查询跑得块,不仅是因为拥有极致优化的执行引擎和节点间网际传输,更依赖于查询处理的大脑中枢:查询优化器。Greenplum查询优化器功能丰富,结构庞杂,优化的点和用到的技术非常多。本章限于篇幅,只对查询优化器其中一小块消除外连接进行简单介绍,本周五的直播活动《Greenplum内核揭秘之查询优化》将对查询优化器进行更为详细的解说,欢迎大家的参与!
连接类型
讲解外连接之前,不得不先讲讲什么是内连接(INNER JOIN),所谓内连接,就是将满足连接条件的两个表中的元组拼接在一起作为结果输出,不满足连接条件的元组不会输出到结果集合中。
下面两张表作为示例说明内连接的逻辑。
student(表示学生信息)
enrolled(表示成绩信息)
通过内连接操作student INNER JOIN enrolled on student.sid = enrolled.sid得到的结果如下。
没有满足连接条件的元组不会输出到结果集中。
Greenplum中的外连接包含三种类型:
- LEFT [OUTER] JOIN:除了输出满足连接条件的输出元组外,同时输出左表中不满条件的元组,同时右表补充NULL
- RIGHT [OUTER] JOIN:除了输出满足连接条件的输出元组外,同时输出右表中不满条件的元组,同时左表补充NULL
- FULL [OUTER] JOIN:除了输出满足连接条件的输出元组外,同时输出左表和右表不满足条件的元组,同时另外一半补充上NULL
除此之外,Greenplum还内部引入了三种特殊的连接类型,由提升子链接时生成使用,用于特殊操作(关于提升子链接,本文不做遨述)
- SEMI JOIN:对于左表中的每一行元组,如果右表至少存在一个元组满足连接条件,则输出左表元组,否则不输出。
- ANTI JOIN:对于对于左表中的每一行元组,如果右表任何元组都不满足连接条件,则输出左表元组,否则不输出。ANTI JOIN可以看成是LEFT JOIN的结果去除INNER JOIN的结果之后得到的结果。
- JOIN_LASJ_NOTIN:主要是考虑到NOT IN语义的不同,用于NOT IN子链接提取的支持。
相比于外连接,SEMI JOIN和ANTI JOIN语义不同,输出的结果数据集也会较少。并且在对连接运算的结合律的支持上,和INNER JOIN非常类似,更便于查询优化器处理利用。
为什么需要消除外连接
相比于外连接,内连接主要具有如下优点:
- 输出结果集更小,因为不需要输出不匹配的元组
- 更便于优化器做优化,比如下面的内连接操作等式两边总是相等
(A INNER JOIN B ON A.v = B.v) INNER JOIN C ON C.v=1
=
A INNER JOIN (B INNER JOIN C ON C.v=1) ON A.v = B.v
而对于左连接,两边不同连接顺序所得到的结果并不总是相等
(A LEFT JOIN B ON A.v = B.v) LEFT JOIN C ON C.v=1
?
A LEFT JOIN (B LEFT JOIN C ON C.v=1) ON A.v = B.v
比如:
test=# SELECT * FROM A;
v
---
1
2
(2 rows)
test=# SELECT * FROM B;
v
---
1
(1 row)
test=# SELECT * FROM C;
v
---
1
(1 row)
test=# select * from (A left join B on A.v = B.v) left join C on C.v=1;
v | v | v
---+---+---
2 | | 1
1 | 1 | 1
(2 rows)
test=# select * from A left join (B left join C on C.v = 1) on A.v = B.v;
v | v | v
---+---+---
2 | |
1 | 1 | 1
(2 rows)
所以,对于非内连接的情况,不能随便交换连接顺序。如果能够将外连接转换为内连接,可以让优化器在查询优化的后续步骤更好的基于代价选择合适的连接顺序。
如何消除外连接
消除外连接其实是试图完成如下多种可能的转换:
- 试图将半外连接(LEFT JOIN, RIGHT JOIN)往内连接INNER JOIN转换
- 试图将全外连接(FULL JOIN)往半外连接或内连接转换
- 试图将LEFT JOIN转换成ANTI JOIN
将外连接半外连接转换成内连接或半外连接
首先,一个启发是,对于下面的查询类型,可以将外连接直接转换为内连接。
A LEFT JOIN B ON A.id = B.id WHERE B.id > 0
如果A表和B表元组满足连接条件,可以直接输出元组,这和内连接结果一致。
如果A表和B表元组不满足连接条件,连接结果会将B的值填充NULL,对于B.v为NULL,过滤条件(WHERE)表达式B.v > 0求值后也为NULL,所以过滤条件会过滤掉不满足连接条件的元组,即LEFT JOIN生成的B表填充NULL的结果,所以上述连接等同于下面的内连接。
A INNER JOIN B ON A.v = B.v WHERE B.v > 0
为了方便处理,Greenplum会针对过滤条件中出现的表达式计算出满足下面条件的表的集合,该集合称为nonnullable_rels:
- 如果当前表的所有属性都为NULL时会导致过滤表达式不为真(假或者NULL)如果通过表达式的计算nonnullable_rels,Greenplum采用分类和递归的方式实现,整个各个不同的表达式类型分别处理,比如对于函数表达式,布尔表达式,NULL测试等。
然后,计算得到nonnullable_rels后,进行如下判断:
- 对于LEFT JOIN,如果nonnullable_rels包含右边的部分或全部表,则可以将LEFT JOIN转换为INNER JOIN
- 对于RIGHT JOIN,如果nonnullable_rels包含左边的部分或全部表,则可以将RIGHT JOIN转换为INNER JOIN
- 对于FULL JOIN
- 如果nonnullable_rels即包含左边部分或全部表也包含右边部分或全部表,则可以转换为INNER JOIN
- 否则,如果nonnullable_rels包含左边部分或全部表,则可将FULL JOIN转换为LEFT JOIN,因为会过滤掉左边填充NULL即RIGHT JOIN的结果
- 否则,如果nonnullable_rels包含右边部分或全部表,则可将FULL JOIN转换为RIGHT JOIN,因为会过滤掉右边填充NULL即LEFT JOIN的结果
- 否则,不做转换
对于nonnullable_rels的计算,需要考虑递归的情况,将上一层计算到的nonnullable_rels往下一层传递。比如对于连接操作。
A LEFT JOIN (B RIGHT JOIN C ON B.v = C.v) ON A.id = C.id WHERE B.v > 10
当处理其中的子连接(B RIGHT JOIN C ON B.v = C.v)时,nonnullable_rels = {B},其来自于上一层的过滤条件。
所以连接最后会全转换为内连接
A INNER JOIN (B INNER JOIN C ON B.v = C.v) ON A.id = C.id WHERE B.v > 10
之后,Greenplum会将RIGHT JOIN统一转换为LEFT JOIN,因为LEFT JOIN与RIGHT JOIN为对称关系,这样可以减少代码处理时的分支数,同时也简化了接下来将LEFT JOIN转换为ANTI JOIN的处理逻辑,不需要处理RIGHT JOIN的情况。
将LEFT JOIN连接转换成ANTI JOIN
一个可以得到启发的例子是
A LEFT JOIN B ON A.id = B.id AND B.v > 10 WHERE B.v IS NULL
过滤条件要求B.v只能是NULL,而连接条件对于B.v为NULL不成立,B的元组在连接结果里会填充NULL返回,这样就可以在结果集中排除LEFT JOIN结果中因为满足连接条件生成的INNER JOIN的部分,所以可以将上述查询转换为ANTI JOIN。
实现上,Greenplum通过过滤条件维护了列的集合forced_null_vars = {B.v},表示通过过滤条件获得的值必须为NULL的列,forced_null_vars要求其中每个列必须为NULL。同时通过LEFT JOIN的连接条件得到不能为NULL的列的集合nonnullable_vars={B.v, A.id, B.id},如果任何一个列为NULL会导致不满足连接条件。如果发现集合forced_null_vars与集合nonnullable_vars交集不为空,并且交集中有来自LEFT JOIN右表的列,就可以将LEFT JOIN转换成ANTI JOIN。
值得一提的是,Greenplum中目前还没有的另外一个优化是,计算nonnullable_vars集合时,可以参考LEFT JOIN右表列上的Not Null约束条件,即上例中的B表,将B表中指定有Not Null约束条件的列也加入到nonnullable_vars集合中。
实现时,Greenplum基于两阶段消除外连接,第一阶段通过函数reduce_outer_joins_pass1来完成标记,搜集所有的连接的信息并标记所有的外连接,第二阶段通过函数reduce_outer_joins_pass2实现真正的消除外连接操作,需要递归分别处理FromExpr子句和JoinExpr子句。
结语
外连接消除实现的基本思路就介绍到这里。消除外连接对Greenplum查询优化器非常重要,不仅可以减少结果数据量,而且让优化器能够更好的基于代价调整连接顺序。本文对Greenplum查询优化器消除外连接的逻辑进行了简单介绍,希望对消除外连接进一步了解的读者可以参照Greenplum内核函数reduce_outer_joins。
文章转自公众号:Greenplum中文社区