概述
当查询语句中存在相关子查询时,可以考虑将其进行改写成两表连接的形式,从而可以避免使用嵌套的方式进行执行。当关联条件涉及的是同一张表时,那么这种改写方式仍然避免不了对该表的多次访问。为此,Oceanbase中定义了winmagic优化规则,能够将满足条件的查询语句转化为窗口函数语句进行执行,提升查询性能。
基本原理
考虑如下情况:
SELECT t1.c1, t2.c2 FROM t1, t2 WHERE t1.c1 = t2.c1
AND t1.c2 > (SELECT sum(c2) FROM t1 v0 WHERE v0.c1 = t2.c1)
对于上述查询,子查询中使用的表同样出现在父查询中,且子查询中的关联条件也出现在父查询的where条件中,可以将查询语句改写为使用窗口函数,如下所示:
SELECT c1, c2 FROM
(
SELECT t1.c1, t2.c2, sum(v0.c2) AS sum_c2 OVER (PARTITION BY c1)
FROM t1, t2 WHERE t1.c1 = t2.c1
) v1
WHERE c2 > sum_c2
代码解析
winmagic优化规则的入口为ObTransformWinMagic::transform_one_stmt,该函数会遍历查询语句中的子查询,然后按照如下流程进行改写:
调用check_subquery_validity函数判断当前子查询是否可以被改写。
调用do_transform函数对满足的语句执行改写。
调用accept_transform函数对改写后的执行开销进行评估,以此判断是否应该接受改写。
check_subquery_validity函数会对子查询的基本属性和where条件进行分析,然后根据如下条件判断其是否可以被改写:
子查询表达式必须位于where条件中,且不能为exist或not_exist子查询。
子查询语句中不包含join表和窗口函数,以及group by、having、order by、limit子句。
子查询中select列表中只包含一个类型为min/max/count/sum之一的聚合函数列。
子查询中的where条件必须都包含在父查询的where条件中,其中相关条件必须为equal条件,且需要同时存在于父查询中或者为同一列的自相关条件;不相关条件则需要同时存在于父查询中或者为lossless_join条件(如主外键)。
子查询中的from表需要同时存在于父查询中,或者为lossless_join表(如外键依赖的表)。
进行判断的同时,该函数还会建立子查询与父查询间where条件和表的映射关系,供改写阶段使用。
do_transform函数负责对满足的语句执行改写,执行流程如下:
调用compute_push_down_items函数计算需要下推到子查询中的表和条件表达式。
调用transform_child_stmt函数对子查询语句进行改写。
调用transform_upper_stmt函数对父查询语句执行改写。
compute_push_down_items函数会根据检查阶段获得的映射关系,将其中的表偏移和条件偏移加入下推集合中。如果子查询的相关条件中包含映射中不存在的表,且该表为基表时,则该表的偏移也会作为相关表被加入下推表集合中。如果父查询中的where条件中存在相关表的条件,且该条件不包含子查询,则该条件的偏移也会被加入到下推条件集合中。
transform_child_stmt函数会根据下推集合对子查询语句进行改写,执行流程如下:
调用transform_aggr_to_winfunc函数在子查询语句中创建窗口函数。
将父查询下推的表达式合并到子查询的where条件中,映射中存在的子查询条件会被父查询条件替代。映射中不存在的相关表达式(即自相关表达式)由于已经被被转换成了窗口函数的分区表达式,因此会被忽略。
将父查询下推的from表合并到子查询的from表中,映射中存在的表会被父查询表替代。
将父查询下推的表信息合并到子查询的表信息中,同时会将所属的列表达式和其他信息(表的分区表达式,分区hint等)合并到子查询中。
transform_aggr_to_winfunc函数负责创建窗口函数表达式,执行流程如下:
遍历子查询条件中的相关条件表达式,将内外表列表达式分别收集到表达式集合中(下称inner_exprs和outer_exprs)。如果内表达式的结果类型被外表达式兼容,则将外表达式加入单独的join表达式集合中(下称type_safe_join_expr)。
遍历父查询中的表信息,对于下推到子查询中的表,如果其不在表映射中(说明不是自表关联),则进一步判断type_safe_join_expr对于该表是否满足唯一性。如果不满足,说明join过程可能会产生数据重复,因此不能使用对应的外表列作为窗口函数的分区列。该函数会将表的id记录到单独的集合中(下称tables_with_pk),然后将表的唯一键添加到窗口函数的分区表达式集合中(下称partition_exprs)。
遍历outer_exprs,对于每条外表列表达式,如果其表id包含于tables_with_pk则略过该项(前面已经将唯一键加入了分区表达式),否则将对应的内表列表达式加入partition_exprs。如果外表列可能为空,则还需要将对应的内表列表达式存储到单独的表达式集合中(下称nullable_part_exprs)。
使用前面得到的分区表达式和子查询语句的聚合函数列创建窗口函数,并添加到查询语句中。如果nullable_part_exprs不为空,则会将聚合函数表达式转化为case when语句构成的表达式,类似如下:
case when c1 is not null and c2 is not null then count(1) else null end. (c1和c2为nullable_part_exprs中的表达式)
transform_upper_stmt函数负责对父查询进行改写,执行流程如下:
从父查询的where条件、from表和表信息中移除下推到子查询中的部分,为下推的子查询中的列在子查询中增加对应的select列。
从父查询语句中移除子查询,同时为子查询语句创建视图表并导出视图表的列表达式。
使用视图表导出的列表达式替换原父查询中select表达式(原表达式因为下推已失效),同时更新半连接信息中对应的左表为视图表。