SQL 子查询优化详解( 三 )


SQL 子查询优化详解

文章插图
 

SQL 子查询优化详解

文章插图
 
注意这些规则仅处理 Cross Apply 这一种情况 。其他 3 种 Apply 的变体,理论上都可以转换成 Cross Apply,暂时我们只要知道这个事实就可以了 。
你可能会问:通常我们都是尽可能把 Filter、Project 往下推,为什么这里会反其道而行呢?关键在于:Filter、Project 里面原本包含了带有关联变量的表达式,但是把它提到 Apply 上方之后,关联变量就变成普通变量了! 这正是我们想要的 。
我们稍后就会看到这样做的巨大收益:当 Apply 被推最下面时,就可以应用第一组规则,直接把 Apply 变成 Join ,也就完成了子查询去关联化的优化过程 。
下面是对 Query 2 应用规则 (3) 的例子 。之后再应用规则 (1),就完成了去关联化过程 。
SQL 子查询优化详解

文章插图
 
Aggregate 的去关联化第三组规则描述如何处理子查询中的 Aggregate(即 Group By) 。和上一组一样,我们的指导思想仍然是:尽可能把 Apply 往下推、把 Apply 下面的算子向上提  。
下面等式中,GA,FGA,F 表示带有 Group By 分组的聚合(Group Agg),其中 AA 表示分组的列,FF 表示聚合函数的列;G1FGF1 表示不带有分组的聚合(Scalar Agg) 。
SQL 子查询优化详解

文章插图
 
这一组规则不像之前那么简单直白,我们先看一个例子找找感觉 。下面是对 Query 1 运用规则 (9) 的结果:
SQL 子查询优化详解

文章插图
 
规则 (9) 在下推 Apply 的同时,还将 ScalarAgg 变成了 GroupAgg,其中,分组列就是 R 的 key ,在这里也就是 CUSTOMER 的主键 c_custkey 。
如果 R 没有主键或唯一键,理论上,我们可以在 Scan 时生成一个 。
为什么变换前后是等价的呢?变换前,我们是给每个 R 的行做了一次 ScalarAgg 聚合计算,然后再把聚合的结果合并起来;变换后,我们先是将所有要聚合的数据准备好(这被称为 augment),然后使用 GroupAgg 一次性地做完所有聚合 。
这也解释了为什么我们要用 ALOJALOJ 而不是原本的 A×A× :原来的 ScalarAgg 上,即使输入是空集,也会输出一个 NULL 。如果我们这里用 ALOJALOJ,恰好也会得到一样的行为(*);反之,如果用 A×A× 就有问题了——没有对应 ORDERS 的客户在结果中消失了!
规则 (8) 处理的是 GroupAgg,道理也是一样的,只不过原来的分组列也要留着 。
ScalarAgg 转换中的细节*
细心的读者可能注意到,规则 (9) 右边产生的聚合函数是 F′F′,多了一个单引号,这暗示它和原来的聚合函数 FF 可能是有些不同的 。那什么情况下会不同呢?这个话题比较深入了,不感兴趣的同学可以跳过 。
首先我们思考下,GroupAgg 以及 ALOJALOJ 的行为真的和变换前一模一样吗?其实不然 。举个反例:
SELECT c_custkey, (    SELECT COUNT(*)    FROM ORDERS    WHERE o_custkey = c_custkey) AS count_ordersFROM CUSTOMER设想一下:客户 Eric 没有任何订单,那么这个查询应当返回一个 ['Eric', 0] 的行 。但是,当我们应用了规则 (9) 做变换之后,却得到了一个 ['Eric', 1] 的值,结果出错了!
为何会这样呢?变换之后,我们是先用 LeftOuterJoin 准备好中间数据(augment),然后用 GroupAgg 做聚合 。LeftOuterJoin 为客户 Eric 生成了一个 ['Eric', NULL, NULL, ...] 的行;之后的 GroupAgg 中,聚合函数 COUNT(*) 认为 Eric 这个分组有 1 行数据,所以输出了 ['Eric', 1] 。
下面是个更复杂的例子,也有类似的问题:
SELECT c_custkeyFROM CUSTOMERWHERE 200000 < (    SELECT MAX(IF_NULL(o_totalprice, 42)) -- o_totalprice may be NULL    FROM ORDERS    WHERE o_custkey = c_custkey)作为总结,问题的根源在于:F(∅)≠F({NULL})F(∅)≠F({NULL}),这样的聚合函数 FF 都有这个问题 。
变换后的 GroupAgg 无法区分它看到的 NULL 数据到底是 OuterJoin 产生的,还是原本就存在的 ,有时候,这两种情形在变换前的 ScalarAgg 中会产生不同的结果 。


推荐阅读