阅读更多
1 Overview
优化器涉及到的优化点包括:
Limit
合并Limit
下推- 子查询重写
- 各种表达式的重写和化简
- 列裁剪
- 谓词下推
- 聚合合并
- 等价谓词推导(常量传播)
Outer Join
转Inner Join
- 常量折叠
- 公共表达式复用(CTE)
- 子查询重写
Lateral Join
化简- 分区分桶裁剪
Empty Node
优化Empty Union, Intersect, Except
裁剪Intersect Reorder
Count Distinct
相关聚合函数重写GroupBy Reordering
2 Subquery Rewrite
子查询分类
- 按子查询所在的位置进行分类
WHERE
子句SELECT
子句GROUP BY
子句ORDER BY
子句HAVING
子句
- 按是否相关进行分类
Correlated Subquery
,即相关子查询Non-correlated Subquery
,即非相关子查询
- 按产生的数据特征来分类
Scalar Subquery
,即标量子查询- 聚合子查询
- 非聚合子查询
Existential Test Subquery
,存在性检测子查询,如EXISTS
子查询Quantified Comparation Subquery
,集合比较子查询,如ANY/SOME/ALL
子查询x = SOME(statement) -> IN (statement)
x <> ALL(statement) -> NOT IN (statement)
上述不同分类均可自由组合
下面的讨论都基于如下的数据集:
1 | DROP TABLE IF EXISTS `S`; |
2.1 Scalar Subquery
对于在WHERE Clause
中的Scalar Subquery
,一般用Outer Join
来进行转换。下面用一个例子来说明,原SQL
如下,其含义是,针对S
表中的每一行,在R
表中找出满足S.s_id = R.r_id
的那个R.r3
(只能有一行,否则会报错),并作为S
表的谓词
1 | SELECT S.s1, S.s2 |
重写后的SQL
如下:
1 | SELECT tmp.s1, tmp.s2 FROM ( |
对于在SELECT Clause
中的Scalar Subquery
,同样用Outer Join
来进行转换。下面用一个例子来说明,原SQL
如下,其含义是,针对S
表中的每一行,在R
表中找出满足S.s_id = R.r_id
的那个R.r3
(只能有一行,否则会报错),并作为S
表的表达式
1 | SELECT S.s1, S.s2, S.s3 = ( |
重写后的SQL
如下:
1 | SELECT tmp.s1, tmp.s2, tmp.s3 = tmp.r3 FROM ( |
2.2 Exists Subquery
2.2.1 Implement with Semi Join(Where Clause)
对于在WHERE Clause
中的Exists Subquery
,一般用Semi Join
来进行转换。下面用一个例子来说明,原SQL
如下,其含义是,针对S
表中的每一行,在R
表中找出满足S.s3 = R.r3
的所有行,并提取出R.r2
作为结果集A
,看结果集A
是否存在非空元素,并以此作为过滤条件,过滤S
的数据
1 | SELECT S.s1, S.s2 |
重写后的SQL
如下:
1 | SELECT S.s1, S.s2 |
2.2.2 Implement with Outer Join(Select Clause)
对于在SELECT Clause
中的Exists Subquery
,一般用Outer Join
来进行转换。下面用一个例子来说明,原SQL
如下,其含义是,针对S
表中的每一行,在R
表中找出满足S.s3 = R.r3
的所有行,并提取出R.r2
作为结果集A
,看这个结果集A
是否存在非空元素
1 | SELECT S.s1, EXISTS ( |
重写后的SQL
如下:
1 | SELECT S.s1, R_GroupBy.r3_Row IS NOT NULL FROM |
2.3 In Subquery
2.3.1 Implement with Semi Join(Where Clause)
对于在WHERE Clause
中的In Subquery
,一般用Semi Join
来进行转换。下面用一个例子来说明,原SQL
如下,其含义是,针对S
表中的每一行,在R
表中找出满足S.s3 = R.r3
的所有行,并提取出R.r2
作为结果集A
,看S.s2
是否在这个结果集A
中,并以此作为过滤条件,过滤S
的数据
1 | SELECT S.s1, S.s2 |
重写后的SQL
如下:
1 | SELECT S.s1, S.s2 |
2.3.2 Implement with Outer Join(Select Clause)
对于在SELECT Clause
中的In Subquery
,一般用Outer Join
来进行转换。下面用一个例子来说明,原SQL
如下,其含义是,针对S
表中的每一行,在R
表中找出满足S.s3 = R.r3
的所有行,并提取出R.r2
作为结果集A
,看S.s2
是否在这个结果集A
中
1 | SELECT S.s1, S.s2 IN |
- 若
S.s2
为NULL
,那么返回NULL
- 若结果集
A
为空,那么返回false
- 对于当前行(固定
S.s3
的值),若R
表中不存在满足R.r3 = S.s3
的行,那么返回false
重写后的SQL
如下:
1 | WITH R_CTE AS (SELECT R.r2, R.r3 FROM R) |
R_GroupBy
:通过Left Outer Join
将谓词IN
调整谓词==
R_CountRow
:用于统计R
表中各分组(by r3
)下的总行数以及R.r2
非NULL
的行数CASE WHEN
语句分析:CASE WHEN 1
:若R_CountRow.R_Rows IS NULL
,我们知道count(*)
是不会产生空值的,因此该空值一定是Left Outer Join
产生的,意味着对于当前行(固定S.s3
的值),R
表中不存在满足R.r3 = S.s3
的行,即集合A
为空。因此根据ANY_OR_NULL IN (empty) -> false
,谓词IN
的结果就是false
- 否则,意味着对于当前行(固定
S.s3
的值),R
表中存在满足R.r3 = S.s3
的行,即集合A
不为空 CASE WHEN 2
:若S.s2 IS NULL
。因此根据NULL IN (ANY_OR_NULL...) -> NULL
,谓词IN
的结果就是NULL
- 否则,意味着
S.s2 IS NOT NULL
CASE WHEN 3
:若R_GroupBy.r2 IS NOT NULL
,意味着对于当前行(固定S.s3
的值),集合A
中至少存在一个元素满足S.s2 = R.r2
,因此根据X IN (X, [ANY_OR_NULL...]) -> true
,谓词IN
的结果就是true
- 否则,意味着
R_GroupBy.r2 IS NULL
CASE WHEN 4
:若R_CountRow.r2_NotNulls < R_CountRow.R_Rows
,意味着对于当前行(固定S.s3
的值),集合A
中一定存在NULL
元素。因此根据X IN (NULL, [NOT_X_OR_NULL...]) -> NULL
,谓词IN
的结果就是NULL
- 否则,意味着
R_CountRow.r2_NotNulls = R_CountRow.R_Rows
,则集合A
中不存在NULL
元素,因此根据X IN (NOT_X...) -> false
,谓词IN
的结果就是false
2.4 Generic Decorrelation Algorithm
下面用一个例子来解释该算法:
Dept
:部门信息,其中building
字段表示该部门的主要办公地点(意味着,该部门下的员工也可以在其他地点办公)Emp
:员工信息
下面这个SQL
的业务含义是,找出预算小于10000
,且主要办公地点无法容纳该部门所有员工的部门
1 | SELECT D.name |
2.4.1 FEED Stage
请结合论文中的Figure 2
看如下的分析
2.4.1.1 step a
初始状态。显然,Child_1
与CurBox
存在关联
1 | -- CurBox |
2.4.1.2 step b
将谓词Q1.budget < 10000
下推,构造出Supp
替换原来的Dept
1 | -- CurBox |
2.4.1.3 step c
接着,从Supp
中构造出Magic_1
1 | -- CurBox |
2.4.1.4 step d
这一步比较复杂,引入DCO(Decorrelated Output) Box
以及CI(Correlated Input) Box
-
CiBox_1
与CurBox
存在关联,但这个相关性可以通过Eq Join
解除- 对于
CiBox_1
,每个building
只有一条数据
1
2
3
4SELECT Q1.name
FROM Supp Q1 JOIN CiBox_1 Q2
ON Q1.building = Q2.building
WHERE Q1.num_emps > Q2.$1; - 对于
-
Child_1
与CurBox
存在关联,但由于Magic_1
的存在,Child_1
与DcoBox_1
也存在关联。因此,关联关系就被下推了 -
于是
CurBox
的相关性就被解除了
1 | -- CurBox |
2.4.2 ABSORB Stage
ABSORB
阶段的目标是将相关性整个除去。紧接着FEEE Stage
的step d
继续流程,此时CurBox
指向的是Temp_2
2.4.2.1 non-SPJ Box
情况1,即CurBox
指向的不是一个SPJ Box
,比如是一个Aggregate Box
2.4.2.1.1 step a
该状态等同于FEEE Stage
的step d
,只不过CurBox
下移了
2.4.2.1.2 step b
此时,可以进一步对CurBox
的父节点使用FEEE Stage
的流程
1 | -- DcoBox_1 如下 |
2.4.2.1.3 step c
此时,DcoBox_1
与CiBox_2
存在相关性,但是CiBox_2
可以从其孩子节点中获取相关性,因此DcoBox_1
与CiBox_2
的相关性是冗余的,可以通过LOJ
直接移除
1 | -- DcoBox_1 如下 |
2.4.2.1.4 step d
这一步比较简单,移除冗余的CiBox_2
即可
1 | -- DcoBox_1 如下 |
2.4.2.2 SPJ Box
此时CurBox
继续下移,指向了step d
中的Temp_3
2.4.2.2.1 step a
1 | -- DcoBox_2 如下 |
2.4.2.2.2 step b
Temp_3
将Magic_2
添加到其From Clause
中,并改写Join On Predicate
,于是DcoBox_2
与Temp_3
的相关性就被移除了
1 | -- DcoBox_2 如下 |
2.4.2.2.3 step c
Temp_3
增加输出列Q10.building
1 | -- DcoBox_2 如下 |
2.4.2.2.4 step d
移除冗余的DcoBox_2
1 | -- Magic_2 如下 |
2.5 Subquery Elimination by Window Function
[Enhancement] Subquery elimination by window function
2.6 参考
3 GroupBy Reordering
Orthogonal Optimization of Subqueries and Aggregation
用$G_{A,F}(R)$表示GroupBy
,其中A
表示GroupBy
列,F
表示输出列
3.1 Filter
3.1.1 push down
$$\sigma_{p}G_{A,F}(R) \rightarrow G_{A,F}(\sigma_{p}R)$$上述转换成立的条件如下:
Filter
中用到的列全部来自于GroupBy
的输入列。换言之,不对GroupBy
的输出列进行过滤
3.2 Join
3.2.1 push down
$$G_{A,F}(S \Join_{p} R) \rightarrow S \Join_{p} G_{A \cup columns(p)-columns(S),F}(R)$$上述转换成立的条件如下:
Join Predicate p
中与R
有关的列必须存在于GroupBy
列中S
的主键必须存在于GroupBy
列中- 聚合操作仅用到了
R
中的列
3.2.2 pull above
$$S \Join_{p} G_{A,F}(R) \rightarrow G_{A \cup columns(S),F}(S \Join_{p} R)$$上述转换成立的条件如下:
Join
的表有主键Join
没有用到聚合函数的结果列
3.3 Outer Join
3.3.1 push down
$$G_{A,F}(S ⟕_{p} R) \rightarrow \pi_{c}(S ⟕_{p} G_{A - columns(S),F}(R))$$上述转换成立的条件如下(同Join push down
):
Join Predicate p
中与R
有关的列必须存在于GroupBy
列中S
的主键必须存在于GroupBy
列中- 聚合操作仅用到了
R
中的列
可以发现,Outer Join
与Join
的差异是,多了一个$\pi_{c}$
- 当
GroupBy
列为NULL
时,若聚合函数针对该NULL
分组可以生成NULL
时,比如sum
,无需引入额外的$\pi_{c}$ - 当
GroupBy
列为NULL
时,若聚合函数针对该NULL
分组无法生成NULL
时,比如count
,则需引入额外的$\pi_{c}$,来产生相应的NULL
值
下面用一个例子来说明
1 | DROP TABLE IF EXISTS `S`; |
1 | -- Q1.1 |
可以发现:
Q1.1
和Q1.2
可以产生相同的输出,因为聚合函数sum
对于NULL
分组可以正常产生NULL
值Q2.1
和Q2.2
无法产生相同的输出,因为聚合函数count
对于NULL
分组会输出0
或1
。因此这里我们通过case when
语句来对输出列进行改写Q2.3
,以达到纠正的目的
4 Table Prune
[Feature] Pruning tables in cardinality-preserving joins
For cardinality-preserving joins:
- A inner join B on A.fk = B.pk: each row in A matches B exactly once;
- A left join B on A.fk = B.pk: each row in A matches B at most once;
So the number of this join’s output rows is equivalent to A’s. if B’s columns is unused or it can be substituted by A’s column that equivalent to B’s column, then B can be pruned from this join.
5 Data Skew
- [Enhancement] Add GroupByCountDistinctDataSkewEliminateRule
- [Enhancement] optimize distinct agg
- [Enhancement] Support [skew] hints in group-by-multi-column-count-distinct-one-column
Given the following case, where column v1
has very limited cardinality, and column v2
has very large cardinality and severely skewed. For example, for v1 = 'x'
, a great number of distinct values of v2
within this group, but for other values of v1
, the number of v2
’s distinct value is much more smaller, this kind of data distribution will lead to poor execution performance.
1 | SELECT v1, COUNT(DISTINCT v2) FROM t0 GROUP BY v1 |
This query can be optimized, as down below, by introducing another group by dimension(hash of v2
) to make sure the processing of count distinct can be fully paralleled.
1 | SELECT v1, SUM(CNT_IN_BUCKET) |
6 Range Join
6.1 Reference
- How to speed up range joins joins in Snowflake by 300x
- What Is a Range Join and Why Is It So Fast?
- Range Joins in DuckDB
7 Property
7.1 HashProperty
Shuffle by v1
可以满足Shuffle by v1, v2
,只不过到了每个节点后,还需要再对v2
进行一次shuffle