关系代数与逻辑优化规则 (一): 定义

时间:2024-05-08 22:01:47

作者: zhuwenzhuang, 2024.05.08.

阅读前假设读者熟悉数据库使用,了解 SQL 的语法和关系算子的大概含义, 能通过 EXPLAIN 命令查看数据库执行计划.

0 前言

数据库优化器的 查询优化(Query Optimization) 指在查询等价的前提下, 将代价更高的查询转化为代价更低的查询的过程.
查询优化可以分为 基于规则的优化(RBO)基于代价的优化(CBO).
在一个典型的优化器中RBO和CBO可以不严格的对应到优化器的逻辑优化和物理优化阶段.

\[\rm{ SQL \xrightarrow[\text{Compiler}]{\text{Parse}} Initial\ Plan \xrightarrow[\text{RBO}]{\text{Logical Optimize}} Logical\ Plan \xrightarrow[\text{CBO}]{\text{Physical Optimize}} Physical\ Plan \xrightarrow[\text{Runtime}]{\text{Execute}} Result\ Data \hspace{100cm} } \]

逻辑优化主要是用启发式规则将逻辑查询计划变化为最优形式, 支持消除不必要的计算, 如谓词下推.
物理优化主要是指基于代价模型在多个物理执行计划中选出最优的物理查询计划, 其包含了最优的计算路径, 比如 join reorder 倾向减少中间产生数据最小的执行计划.
逻辑优化中的 逻辑优化规则(Logical Rule) 关注在关系代数这一表示层上, 对查询进行等价变换优化, 相对少的关注物理执行上的选择.

内容关注逻辑优化规则的关系代数表示(Logical rules in relational algebra representation), 目标是作为一个相对完备的逻辑优化规则参考, 也可以作为理解优化器逻辑优化的一个资料.主要讨论定义, 规则, 以尽可能简化+精确的符号表达, 以逐步定义加推理的方式梳理规则, 最终在可视的范围内表示规则.

使用关系代数符号避免了需要深入具体数据库优化器的代码才能理解规则. 在初步了解时符号可能会增加识别的难度, 但在更为熟悉后, 符号表示相对伪代码表示, 以及画图说明都有优势, 可以作为一个描述优化器规则设计的建模语言. 最终产出基于符号表达的规则梳理结果, 可以用于确认已有规则完整性, 以及发现缺失的规则.

主要内容来源于 Guido Moerkotte 所著 Building Query Compiler, 后文用 BQC 指代.

1 基本定义

Value, Type and Domain

每个 类型(Type) 对应到一个 值域(Domain).值(Value) 是在一个类型下值域中的一个点.值域是由有限的离散的值组成, 一个值域中的值之间可以用\(=\)比较, 判断是否相等.如果值域中的值之间都可以用\(<\) 比较, 则是个有偏序关系的值域.

比如:
INT 类型的值域为 [-2^31, 2^31-1] 中每个整数 以及 NULL. 1 是 INT 类型的一个合法值.
STRING 类型的值域为任意长度的字符串 以及 NULL. 'Bob' 是 STRING 类型的一个合法值.
DOUBLE 类型的值域为 [-Inf, Inf] 中每个可精确表示的双精度浮点数 以及 NULL 和 NaN, 0.0 是 DOUBLE 类型的一个合法值.
INT/DOUBLE/ STRING 类型的值都是可排序的, BINARY 无法用 '<' 比较.
实际系统中往往 STRING/BINARY 有最大长度限制, 所以也可以看做一个有限离散的值域.

NULL 是一个特殊的值, 在每个类型的值域中存在, 用 \(\perp\) 表示.

Tuple

元组是属性名到值的映射.
*('属性名'后续简称为'属性')
元组使用[]表示,包含逐项属性和值. 如:
\(t_1:= [name: \text{"张有志"},\ age: 30]\)
\(t_2:= [job: \text{"Software\ Engneer"}]\)

用表格形式表示

\[t_1 =\def\arraystretch{1.5} \begin{array}{c:c} name & age \\ \hline \rm{张有志} & 30 \end{array} \hspace{100cm} \]

\[t_2 = \def\arraystretch{1.5} \begin{array}{c} job \\ \hline \rm{Software\ Engneer} \\ \end{array} \hspace{100cm} \]

\(t_1\)属性(Attribute) \(A\) 为:
\(A := \mathcal{A}(t_1) = \{name, age\}\)
\(\mathcal{A}(t)\)获取属性函数,表示从元组中获取属性集.

使用 \(\circ\) 表示元组的连接(concat):
\(t_1\circ t_2 = [name: \text{"张有志"},\ age: 30,\ job: \rm{"Software\ Engneer"}]\)

用表格形式表示连接结果,

\[t_3 = t_1\circ t_2 = \def\arraystretch{1.5} \begin{array}{c:c:c} name & age & job \\ \hline \rm{张有志} & 30 & \rm{Software\ Engneer} \\ \end{array} \hspace{100cm} \]

使用 \(t.A\) 表示选择 \(t\) 的一些属性后剩余的元组.
\(A_1=\{age, job\}\), 则 \(t_3.A_1 = [age:30, job:\text{Software Engneer}]\)

使用 \(.\) 加属性名获取单个属性对应值\(t_1.age = 30\)

属性可以看作数据库表的列名.

Bag

是不去重的元组多重集(multiset).表示上使用下标b区分于一般的集合符号.\(bag:=\{t_1, t_2, ...\}_b\)

后续一般讨论的包中, 所有元组都有相同的属性集合, 同一属性对应值的类型在多个元组中一致.

Sequence

序列使用\(⟨t_1, t_2, ...⟩\) 表示, 用于表示有序的数据

Relation

关系是一个有相同属性元组的包, 常用R或者A,B等大写字母表示.
关系的schema是包的属性集,用\(sch(R)\)表示.
\(\mathcal{A}(R)\) 表示获取一个关系的属性集, 两者含义等价.
\(\mathcal{A}(R)= sch(R)\)

包的表示

bag 是个无序多重集, 所以对于相同元组可以使用多重度 m 来简化表示
关系/包的表示示例如下.

基本表示:
\(R = \{[a:1,b:1],[a:1,b:1],[a:2,b:2]\}_b\)

\[\begin{array}{cc} A & B \\ \hline 1 & 1 \\ 1 & 1 \\ 2 & 2 \\ \end{array} \hspace{100cm} \]

多重度表示, \(\mathfrak{m}\) 表示 multiplicity:
\(R = \{[a:1,b:1,\mathfrak{m}:2],[a:2,b:2, \mathfrak{m}:1]\}_b\)

\[\begin{array}{c|cc} \mathfrak{m} & A & B \\ \hline 2 & 1 & 1 \\ 1 & 2 & 2 \\ \end{array} \hspace{100cm} \]

元组 id 表示, 可以表示元组的顺序. i 表示元组序号:
\(R = ⟨[a:1,b:1]^{1,3},[a:2,b:2]^2⟩_b\)

\[\begin{array}{c|cc} i & A & B \\ \hline 1 & 1 & 1 \\ 2 & 2 & 2 \\ 3 & 1 & 1 \\ \end{array} \hspace{100cm} \]

注意在数据库中列名大小写不敏感, 所以 A 和 a 作为属性名是一样的.

Algebra Expression

代数表达式分为标量表达式和关系表达式, 都使用e表示.标量表达式和关系表达式除了返回值不同外,没有更多的区分.

Operator

算子(又称运算符)分为关系算子(Relational Operator)和标量算子(Scalar Operator).
运算返回标量的算子是标量算子. 运算返回关系的是关系算子.

Scalar Operator

标量算子是标量表达式中最小粒度的基本计算函数.标量算子对应到是数据库的一般标量函数, 聚合函数, 窗口函数等. 比如 +,-,*,/, TO_DATE, LIKE, MAX, COUNT, SUM, RANK等返回标量的函数都是标量函数.

比较算子(comparison operator) 是一类返回布尔值的二元标量算子, 用 \(θ\) 表示:
\(aθb, θ ∈ \{=,≤,<,≥,>,\ne \}\)
在基本的比较算子之外, 其他的算子也可以构造出偏序关系和等价类, 同样可以应用和比较算子类似的规则, 这些不在讨论范围内.

Relational Operator

关系算子是关系表达式中最小粒度的计算后返回关系的基本计算函数.关系算子对应到是数据库的表值函数(Table Valued Function), 如 EXPLODE, TRANS_ARRAY.

使用关系算子组成关系表达式时,关系算子可以包含一些标量表达式,如
\(\sigma_p(R)\) 中的 \(p\) 为返回布尔类型值的标量表达式, 表示按照 \(p\) 计算结果过滤.

Scalar Expression

标量表达式指返回一个标量值的表达式.一般由标量算子,*变量和常量组成, 可以包含关系表达式在内部.

谓词(Predicate) 是指一个返回 true,false 或 unknown 的标量表达式. 常用 p 表示,从表达式的角度看也可以用 e 表示

标量表达式可以包含关系算子和关系
\(\sigma_{e_1(t) \neq \emptyset_b, t∈e_2}(e_2)\) 中 predicate 部分是一个返回标量结果的子查询.
不含关系算子和关系数据的标量表达式称为纯标量表达式.
比如 a+b 中 + 只接受数值类型, 所以 a+b是一个纯标量表达式.

Relational Expression

关系表达式指返回一个'关系'(一组元组)的表达式.一般由关系算子,*变量和常量组成.

逻辑算子 Filter 定义
\(\sigma_p(R) := \{r|r ∈ R, p(r)\}\)
Filter 运算过程: 遍历关系 R 中每一行(元组) r, p(r)的结果为true则保留.

通过*变量与绑定的概念, 关系表达式可以消费输入, 产生输出, 嵌套成更复杂的关系表达式.

*变量(Free variable)
指表达式中需要从输入中获取绑定值的属性
以 谓词 \(p\)\(name='Bob'\) 为例,其中 name 是一个*变量

获取*变量函数 \(\mathcal{F}(e)\)
\(\mathcal{F}(e)\) 表示获取关系表达式(或标量表达式) \(e\) 中的*变量集,如:
\(\mathcal{F}(\sigma_{name='Bob'\ ∧\ age > 18}) = \mathcal{F}(name='Bob'\ ∧\ age > 18)= \{name,age\}\)

按照属性绑定*变量
对于一个表达式 e 有*变量集
\(\mathcal{F}(e) = \{a_1,a_2, ...,a_n\}\)\(\mathcal{F}(e) ⊆ \mathcal{A}(t)\) ,
定义
\(e(t) := e[a_1 \leftarrow t.a_1, ..., a_n \leftarrow t.a_n]\)
表示将元组 t 按照属性绑定在 e 的*变量上,从而完成求值.
同样,定义可以扩展到包上以及表达式的复合上:
\(e_2(e_1) = e_2(R') = e_2(t_1',t_2',...,t_n')\)

*变量与属性绑定示例
比如令关系 \(R=\{\{year:2000,month:12\},\{id:2050,month: 1\}\}_b\) ,
想过滤出 year > 2008 的月份数据时, 令含*变量的表达式 \(e_1 = \sigma_{year>2008}, e_2=\Pi_{month}\),
则完成*变量按属性绑定后的表达式的结果为:
\(R'' = e_2(e_1(R))= \sigma_{year>2008}(\Pi_{month}(R)) = \{\{month:1\}\}_b\)

含*变量表达式等值的含义
当后续讨论两个含*变量表达式等价时(如 \(e_1 \equiv e_2\)), 则表示两个表达式*变量部分一致.
也就是说, 等价意味着在两个表达式分别绑定相同的任意输入时, 产生相同的计算结果.
另外, 为了讨论方便, 后续也会用符号 \(=\) 表示与符号 \(\equiv\) 同样的含义, 即表达式等价.

逻辑关系算子(Logical Relational Operator)简称逻辑算子(Logical Operator),主要指区别于物理算子的关系算子, 不关注物理属性, 只关注在数据在包表示上的变化.

比如 Join 是个逻辑算子, 其物理算子有HashJoin/SortMergeJoin 等,这些物理算子不会影响在包语义下计算的结果.
物理算子会影响排序属性, 从而序列(Sequence)以及分区序列(Partitioned Sequence)可以用于描述物理属性(排序,分布等)上的规则, 不过这不在这主要的讨论范围内.

2 布尔表达式

标量表达式的优化一般包括常量折叠, 表达式化简, 公共子表达式提取, 输入范围(谓词)提取等.
布尔表达式是一类化简规则明确的标量表达式.

二值逻辑

二值逻辑可以看做由

  • 逻辑算子 \(∧, ∨, ¬\)
  • 量词 \(∃,∀\)
  • \(true,false\), 以及一些在比较算子作用下的其他类型值
  • 比较算子 \(<, >, \le, \ge, =, \ne\), 两两为相反符号
  • 替代符号 \(p, \theta, t\)
  • 表达两个表达式相等的全等符号 \(\equiv\)

等等符号组成的经典布尔代数系统.有很多基于真值表推理的已知的等价规则, 这里省略不作完整列举.

比如消去非的规则
\(¬(¬(p)) ≡ p\)
\(¬t_1\theta t_2 ≡ t_1\bar\theta t_2\)

用比较符号的取反消去非实例, 如
\(¬t_1 = t_2 ≡ t_1 \ne t_2\)

另外, 量词在 SQL 中直接关注较少, 可以认为在子查询展开部分已经解决, 后续讨论也不包含.

二值逻辑的已有规则各种等价规则,在已知数据中没有 null 时, 可以应用于表达式等价变换(化简,拆分等), 否则应使用三值逻辑的等价规则.

三值逻辑

一般关系代数中使用的是三值逻辑(Three-Valued Logic), 因为需要处理 null 值.

null 在逻辑表达式中又称为 unknown, 从后续真值表可以看出将null当做unknown语义来处理.
SQL 中没有蕴含(entail)算子, 所以不用讨论各种三值逻辑定义.

三值逻辑真值表

由于有了 null 的存在, 在二值逻辑也需要定义 null 在逻辑算子下的行为.
比如对于非, null 的非也是null.

当扩展二值逻辑的真值表后, 对于 \(\neg, ∧, ∨\) 算子, 真值表为

\[\def\arraystretch{1.5} \begin{array}{c|ccc} \neg & true & false & \perp \\ \hline & false & true & \perp \\ \end{array} \hspace{100cm} \]

\[\def\arraystretch{1.5} \begin{array}{c|ccc} \vee & true & false & \perp \\ \hline true & true & true & true \\ false & true & false & \perp \\ \perp & true & \perp & \perp \\ \end{array}\hspace{100cm} \]

\[\def\arraystretch{1.5} \begin{array}{c|ccc} \wedge & true & false & \perp \\ \hline true & true & false & \perp \\ false & false & false & false \\ \perp & \perp & false & \perp \\ \end{array} \hspace{100cm} \]

梳理真值表时, 可以发现:
如果让
\(true := 1, false := -1, \perp :=0\)\(\neg := -, ∨ := max, ∧ := min\)
则可以得到一模一样的乘法表, 也就是说可以用min/max/- 和 0,1,-1 编码对应到三值逻辑.
比如对于 ∨ := max 的乘法表

\[\begin{array}{c|ccc} \text{max} & 1 & -1 & 0 \\ \hline 1 & 1 & 1 & 1 \\ -1 & 1 & -1 & 0 \\ 0 & 1 & 0 & 0 \\ \end{array} \hspace{100cm} \]

这说明, 对于 min/max/- 的等价规则和对于三值逻辑的等价规则是一样的.

基于真值表可以得到一些等价规则, 如

\[\begin{align*} true ∨ p ≡& true \\ \perp ∨ p ≡& \perp \qquad //当 p 不为 true \\ false ∨ p ≡& p \end{align*} \hspace{100cm} \]

对应到以下几个表达式

\[\begin{align*} true ∨ \perp ∨ false ≡& true \\ \perp ∨ false ∨ \perp ≡& \perp \\ false ∨ false ≡& false \\ \end{align*} \hspace{100cm} \]

对于比较算子, 当两个值中存在 null 时结果也为 null
扩展的等值算子 \(\doteq\) (加点等号) 将 null 视为一个单独的量, 用\(\doteq\)做等值判断不会返回 null.

\[\begin{array}{c|cc} x = y & x \text{ is null} & x \text{ not null} \\ \hline y \text{ is null} & \perp & \perp \\ y \text{ not null} & \perp & x = y \\ \end{array} \hspace{100cm} \]

\[\begin{array}{c|cc} x \doteq y & x \text{ is null} & x \text{ not null} \\ \hline y \text{ is null} & true & false \\ y \text{ not null} & false & x \doteq y \\ \end{array} \hspace{100cm} \]

偏序比较算子在用于排序(order by)时, 需要显式指明 null 的顺序应该作为 first 还是 last
\(\doteq\) 常用于 group by 与 distinct, 也可以用SQL语句 IS NOT DISTINCT FROM 表示

null 转换为 true/false

在表达式如 x = 1 < null , x 的求值结果不是true或者false, 而是 null.
比如 select 1 < null; 的结果为 null.
当该表达式在where子句中时, null 结果会被视作 false.
比如 select 1 from t where 1 < null; 最后结果为0行.
对于这类转换null 为true或false的行为, 称为 null as true (true-interpreted) 或 null as false (false-interpreted).类似于取顶和取底.
\(⌈·⌉,⌊·⌋\) 表示, 对应真值表为:

\[\begin{array}{c|cc} x & \lceil x \rceil_\perp & \lfloor x \rfloor_\perp \\ \hline true & true & true \\ false & false & false \\ \perp & true & false \\ \end{array} \hspace{100cm} \]

用SQL语句表达则是 IF(x is null then false else x) 或 IF(x is null then true else x)

将 null 转换成 true/false 的操作可以在嵌套的三值逻辑表达式中下推/上拉.
规则为随着非算子变换方向, 且可以分配到 \(∧, ∨\)

\[\begin{align*} ⌈¬x⌉_\perp ≡& ¬⌊x⌋_\perp \\ ⌊¬x⌋_\perp ≡& ¬⌈x⌉_\perp \\ ⌈p_1 ∧ p_2⌉_\perp ≡& ⌈p_1⌉_\perp ∧ ⌈p_2⌉_\perp \\ ⌊p_1 ∧ p_2⌋_\perp ≡& ⌊p_1⌋_\perp ∧ ⌊p_2⌋_\perp \\ ⌈p_1 ∨ p_2⌉_\perp ≡& ⌈p_1⌉_\perp ∨ ⌈p_2⌉_\perp \\ ⌊p_1 ∨ p_2⌋_\perp ≡& ⌊p_1⌋_\perp ∨ ⌊p_2⌋_\perp \end{align*} \hspace{100cm} \]

当上层的 \(⌈·⌉,⌊·⌋\) 下推时, 上层算子不再需要处理可能为 null 的场景, 从而可以应用上各种二值逻辑的等价变换规则.

\(⌈·⌉,⌊·⌋\) 下推代码实例可以参考: Calcite UnknownAs

三值逻辑的等价规则

三值逻辑下二值逻辑成立的规则需要调整, 主要是在等价变换时需要保留返回 null 场景的不变, 以及上面提到的 null 在不同算子下转化的规则.

比如比较算子除了会返回 true, false 之外, 在任意操作数为 null 时返回 null. 所以取反时需要保持 null.
null 谓词 is null 和 is not null 用于判断一个值是否为 null, 一定不会返回 null.
and / or 对应到含null值域的求交和求并.
其他的布尔表达式算子按照同样的方式推理.

基于这个规则变换值域, 可化简一个嵌套的布尔表达式为最简形式.

三值逻辑化简代码实例: Calcite RexSimplify,Sarg

Conjunction Normal Form / Disjunction Normal Form 转化已有讨论较多, 略.

3 逻辑算子定义

逻辑算子分为五部分和一个扩展.
五部分: 输入输出, 数据重排, 投影过滤, 分组计算, 集合与连接.
一个扩展: 嵌套与展开可以看做逻辑算子的一个特例, 用于将包压缩成一个标量值, 或将一个标量值展开成一个包.

输入输出(Input/Output)

Sink

\(Sink(e)\)表示表的输出.

TableScan

\(Scan(T)\)表示表的输入. \(T\) 表示一个表数据的快照.

Values

\(\{t_1, t_2, ...\}_b\)\(Values(e)\)Values 用于表示一个包常量.

Values 的实用意义包括将常量值封装为一个算子, 在处理算子树时可以统一处理.

TableFunction

\(T_f(e) := \{e_2|e_2 = f(e)\}, f\) 为表值函数

TableFunction 严格来说不是一个输入算子, 需要一个关系作为输入, 和后续提到的映射(map)类似. 但其可以一次性消费整个关系上的元组,以及可以从空关系中生成任意多个元组, 超越了一般逐行映射的能力, 所以在处理上看做输入算子会更容易, 对这个算子的优化讨论较少.

数据重排(Enforcer)

数据重排一般不改变包语义下关系的逻辑属性, 只改变物理属性(排序, 分布).
Limit是一个例外, 其可以将包中元组按照 offset 选取最多 num 个.

\(Enforcer_{dist,coll,limit,offset}(e)\)

细化的 Enforcer:
\(Limit_{num,offset}(e)\)
\(Sort_{coll}(e)\)
\(Exchange_{dist}(e)\)

在后续暂不讨论数据重排相关算子, 因为包语义无法体现数据重排中的排序和分布属性.

投影与过滤(Project/Filter)

Filter 算子

\(\sigma_p(e) := \{x|x ∈ e, p(x)\}\)\(p\) 是个谓词(predicate),返回布尔值作为结果, 注意 \(p\) 中可以嵌套关系算子等.

计算过程为从 e 中取出逐行元组, 以谓词 p 求值结果过滤, 过滤剩余的多个元组构造为一个新的包.
\(p\) 可以嵌套子查询以用于exists等场景,如\(\sigma_{e_1(t) \neq \emptyset_b, t∈e_2}(e_2)\) 表示只保留 e_2 中满足 e1 表达式的元组

Project 类算子

扩展投影主要是指投影(Projection), 映射(Map) 两部分, 去重(Distinct) 和 重命名(Rename) 也与扩展投影相关, 所以统一归为 Project 类算子.

在一般的数据库教材中, 扩展投影是一个算子同时最投影和标量函数计算.
这里沿用BQC定义的两个算子, 分别表达投影和计算.

Project投影选择部分属性令 \(A = \{a_1, ... a_n\}\) 为属性集\(\Pi_A(e):=\{[a_1:x.a_1, ..., a_n:x.a_n] | x ∈ e\}_b\)

计算过程为从 e 逐行取出元组, 按照属性集选取值, 构造成元组, 并将新构造的多个元组构造为一个新的包.

取剩余属性的投影.
\(\Pi_{\overline A}(e) := \Pi_{\mathcal{A}(e)\backslash A}(e)\)

Distinct
\(\Pi^D\) 表示将包中所有元素的多重度 \(\mathfrak{m}\) 置为 1

带去重(Distinct)的投影, 去重后元组都是唯一的.
\(\Pi_A^D(e):= \Pi^D(\Pi_A(e))\)
\(\Pi^D_A\)的符号虽然是投影,在分析上可以看做一种只做去重聚合的Aggregate.

等价表示:
\(\Pi^D_A(e) = \Gamma_{A;()}(e)\)

计算示例:
\(\Pi^D(\{[a:1,b:1,\mathfrak{m}:2],[a:2,b:2, \mathfrak{m}:1]\}_b)=\{[a:1,b:1,\mathfrak{m}:1],[a:2,b:2, \mathfrak{m}:1]\}_b\)

\(\Pi^D_{\{a\}}(\{[a:1,b:1,\mathfrak{m}:2],[a:2,b:2, \mathfrak{m}:1]\}_b)=\{[a:1,\mathfrak{m}:1],[a:2, \mathfrak{m}:1]\}_b\)

Map映射是执行多个表达式, 并返回结果的算子.

有的地方将映射和投影表示合并, 作为拓展投影算子整体看待.
比如 Calcite 的 Project 也同时包含了投影,映射以及下面提到的重命名.
不过这里我们沿用经典的符号表示, 将两者分开.

执行表达式, 并将结果以属性 a 和原有元组连接:
\(\chi_{a:e_2}(e_1):=\{y\circ[a:e_2(y)]|y ∈ e_1 \}_b\)

执行表达式, 并将结果整体作为输出:
\(\chi_{e_2}(e_1):=\{e_2(y)|y ∈ e_1 \}_b\)

扩展到多个表达式:令含有多个标量函数的 属性赋值向量(attribute assignment vector):\(F=a_1:e_1,...a_k:e_k\)\(\chi_F(e)=\chi_{a_k:e_k}(...\chi_{a_1:e_1} (e)...) = \{x \circ [{a_1} : e_1(x)) ,..., a_k : e_k(x)) ] | x \in e\}\)相当于多个表达式逐个作用, 并指定结果元组的属性名.

后续使用 \(\chi_f(e)\) 来表示一般性的 map 算子.

Rename
重命名算子符号为:

\(A = \{a_1, ..., a_n\}, B=\{b_1,...b_n\}\) 为两个属性集\(\rho_{A \leftarrow B} :=\Pi_{\overline A}(\chi_{b_1:a_1,..., b_n:a_n}(e))\)

Rename 一般在生成逻辑计划之前就可以消除. 对于关系代数等价性的讨论不重要.

分组计算 (Aggregate/Window) 类

分组计算包含 Aggregate/Window 类的逻辑算子.

讨论聚合相关关系算子之前, 先定义聚合函数.

聚合函数(Aggregate Function)
聚合函数是一种标量算子, 典型的有 min/max/sum/count/avg 等, 可以包含 distinct 属性. 比如 sum 和 sum(distinct) 在 \(e\) 上作用的结果可能不同.

可分解性指聚合函数可以分解成多个聚合函数逐一计算, 比如 \(agg(e)=agg^2(agg^1(e))\)

本文使用Markdown格式编写, 在文字较多时, 使用 Markdown 原生表格, 符号较多时使用 \(\KaTeX\) 表格.

有以下聚合函数分解表(Decomposition of aggregate functions):

\(agg\) \(agg^1\) \(agg^2\)
min min min
max max max
count count sum0
avg sum,count sum,sum

注意 count 的特殊语义, 无 group key 时, 不返回空集而返回0,所以使用 sum0 表示这种语义.
比如以下计算, 其中c表示绑定*变量对应的属性名.
\(\Gamma_{();a:count(c)} (\emptyset_b) = \Gamma_{();a:sum0(c')} (\Gamma_{();c':count(c)} (\emptyset_b)) = \{[a:0]\}_b\)

聚合函数向量
表示一组聚合函数
\(F:=(b_1 :agg_1(a_1),...,b_k :agg_k(a_k)),\)

聚合函数向量中聚合函数逐个分解后,可以表示为:

\[\begin{align*} F^1:=&(b_1' :agg_1^1(a_1),...,b_k' :agg_k^1(a_k))\\ F^2:=&(b_1 :agg_1^2(b_1'),...,b_k :agg_k^2(b_k'))\\ F(e)=&F^2(F^1(e)) \end{align*} \hspace{100cm} \]

重复敏感
重复敏感指部分agg函数会因为数据是否做过去重而改变结果.
重复无关(duplicate agnostic) 函数:
min, max, sum(distinct), count(distinct), avg(distinct)
重复敏感(duplicate sensitive) 函数:
sum, count, avg

重复无关的函数加distinct场景, 显然可以消去distinct, 如以下SQL变换在 agg 是重复无关函数时成立.
\(\text{select agg(distinct col) ...} \Rightarrow \text{ select agg(col) ...}\)

Aggregate

承载一般标量计算的是 Map 算子, 而承载聚合函数计算的是 Aggregate 算子.

Aggregate (单聚合函数场景)定义\(f\) 为一个聚合函数, \(g\) 为聚合函数结果的属性名, \(G\) 为 group keys 列表.\(\Gamma_{G;g:f}(e):=\{y\circ[g:x]|y\in\Pi_G^D(e), x=f(\{z | z \in e, z.G \dot{=} y.G\}_b)\}_s\)

注意 \(\Gamma_{G;g:f}(e)\) 返回的包中元素是无重复的, 所以是集合.

\(\Gamma_{G;g:f}\) 表达式的求值过程1.获取去重聚合元组

首先对 e 按照 G 进行去重投影,结果元组逐一枚举为 y

2.以每个去重聚合元组为键, 划分e为多个组

对于每个y, 从e中再次枚举与y在聚合键G上相等(\(\dot{=}\))的元组 z, 构造为一个包(对应到一个唯一的y).

3.每组内做聚合计算

对每个 y 构造出来的包逐个作为聚合函数 f 的输入, 计算出聚合结果 x, 和属性名 g 组成聚合结果元组 [g:x]
含有distinct的聚合函数, 内部实现需要先计算 \(\Pi^D_{\mathcal{F}(f)}(e)\)结果 (将包按照聚合函数distinct指定列做去重), 再进行逐个元组流式计算. 无distinct的聚合函数可以直接枚举元组流式计算.

4.聚合计算结果与group key元组连接

最终连接 y 和其对应聚合出来的元组 [g:x] 作为结果元组, 多个元组组成一个包.因为聚合键是无重复的,所以也可以看做一个元组集合(比包约束更强).

等值的语义问题
\(\dot{=}\) 是把 null 视作常量比较的等于符号.
\(\perp\dot{=}\perp\ is\ true, 1\dot{=}\perp\ is\ false,\)
一般的等号在三值逻辑中遇到null会返回 unknown. grouping 分组所使用的等值比较是二值逻辑\(\dot{=}\),从而可以正确的将 null 独立分组.

Aggregate (多聚合函数场景)完整定义
基于 group key 列表 \(G\) 和 属性赋值聚合函数列表 \(F\)聚合算子表示:

\[\begin{align*} \Gamma_{G;F}(e) :=& \Gamma_{G;b_1:f_1,...,b_k:f_k}(e) \\ :=& \{y\circ[b_1:x_1, ..., b_k:x_k]|y\in\Pi_G^D(e), x_i=f_i(\{z | z \in e, z.G \dot{=} y.G\}_b)\}_s \end{align*} \hspace{100cm} \]

与单聚合函数求值过程的区别为: 对于分组后的\(z\)对应元组包, \(F\) 中多个聚合函数会逐一计算并concat到结果中.
\(\Gamma_{G;g;F}(e)\) 这种表示法在后续也会用到, 其中 \(g\) 表示 \(F\) 的*变量列表.

预聚合函数列表的符号表示
分裂成预聚合函数后, 对同一个函数有多种表示方式, 分别对应聚合的不同阶段.
比如一种可能的划分方法, 将聚合函数划分成 4 种执行阶段:
(\(F\)上标表示所处阶段和子阶段)

\(type\ name\) \(common\ name\) \(input\) \(output\) \(symbol\)
Partial 预聚合 Raw Data Intermediate Results \(F^{1,1}\)\(F^1\)
Intermediate 中间聚合 Intermediate Results Intermediate Results \(F^{1,2}\)
Final 最终聚合 Intermediate Results Final Results \(F^2\)
Single 一次性完全聚合 Raw Data Final Results \(F\)

Group Sets定义
group sets 是一种多组 group keys 集合分别进行聚合运算, 并把结果 UNION ALL 在一起的语法糖. 符号表示上增加一个帽子作为区分(\(\widehat{G}, \widehat{\Gamma}\)).

\(\widehat{G} =\{G_1, ... G_n\}\) 为 n 个 分组键集合列表 的集合, 有:
\(\widehat{\Gamma}_{\widehat{G};F}(e) := \cup(\Gamma_{G_1;F}(e), ...,\Gamma_{G_n;F}(e))\)

Window

\(W_{w;F}\)
表示一个窗口算子, \(F\) 表示窗口函数的属性赋值向量. \(w\) 表示窗口的定义, 包含 partition by, sort by, frame. frame 表示窗口帧的定义, 包含窗口帧模式, 窗口范围等.

Window 算子不会改变关系的行数, 也不会删除原有的属性, 会增加一些聚合计算结果到关系的属性中, 和 \(\chi_f\) 行为很像. 区别在于窗口函数处理的范围不是单行而是一个窗口帧.定义窗口帧涉及到序列和分片, 不是包语义能描述的. 所以后续默认不详细讨论窗口算子.

了解窗口算子计算过程以及更多说明, 可以参考:
Efficient Processing of Window Functions in Analytical SQL Queries

嵌套与展开(nest/unnest) 类

嵌套是一个特殊的聚合算子, 展开类算子属于一类表值函数.

嵌套与展开是对偶的操作.
对嵌套和展开进行专门的符号定义, 主要目的是帮助到后续的等价性推理过程.

Nest

嵌套指将一个多属性元组压缩到一个属性上.

\(G ⊂ \mathcal{A}(e), \overline{G} = \mathcal{A}(e) \backslash G\)
\(\nu_{G;g}(e):=Γ_{G;g:Π_{\overline{G}}} (e)\)

嵌套是一种特殊的聚合, 将数据按照分组键分组后, 将非分组键范围内的多个元组整体作为属性的值.
如以 {a} 为 G, g为属性名对示例数据做嵌套:

\([a:1,g:[[b:2,c:3],[b:3, c:\perp]]] = \nu_{a:g}([a:1,b:2,c:3], [a:1, b:3, c:\perp])\)

可以看出, a 作为分组键没有受影响, 同一分组键下 b,c 对应的包整体作为属性 g 的值. 信息是没有损失的, 只是a被去重, b,c 被压缩进一个属性中, 应该可以通过展开将数据还原.

Unnest 类算子

展开(Unnest)
展开是一种特殊的表值函数, 执行过程为枚举逐行, 把选定部分元组解嵌套,并逐个和非展开部分连接.
下面第一个表达式处理嵌套的包, 带有属性名, 类似于 SQL 中的 array<struct>, struct 成员在展开后成为属性.
第二个表达式处理类似 array<int> 这种纯粹值的集合, 赋予a作为属性名.

\(\mu_g(e):=\{y.[\mathcal{A}(y) \backslash\{g\}] \circ x | y \in e, x \in y.g\}_b\)\(\mu_{a:g}(e):=\{y.[\mathcal{A}(y) \backslash\{g\}] \circ[a: x] | y \in e, x \in y. g\}_b\)

\([\mathcal{A}(y) \backslash\{g\}]\)表示的是从 y 中属性去除 g 剩余的属性集.
上面表达式执行过程先从 e 中取出每个元组 y, 再从 y.g 包中取出每个元组 x, 最后将 x 和 y 中非 g 部分进行连接.

算子 \(\mu\)\(\nu\) 互为逆运算,
\(令\ G ⊂ \mathcal{A}(e), 则 R=\mu_g(\nu_{G:g}(R))\)
符号可表示为 \(\mu = \nu^{-1}\).

一个具体示例:
\([a:1,b:2,c:3], [a:1, b:3, c:\perp] = \mu_{g}([a:1,g:[[b:2,c:3],[b:3, c:\perp]]])\)

展开映射(Unnest Map)
展开映射的输入来源于映射算子的表达式 \(e_2\), 后续的投影表示丢弃展开的中间结果, 也就是说\(\Upsilon\)是个从\(e_1\)作为数据源计算\(e_2\)同时并展开的简化符号表示.

\(\Upsilon_{e_2}(e_1) :=\Pi_{\bar{g}}(\mu_g(\chi_{g: e_2}(e_1)))\)\(\Upsilon_{a: e_2}(e_1) :=\Pi_{\bar{g}}(\mu_{a: g}(\chi_{g: e_2}(e_1)))\)

展开映射后续使用较少, 不做进一步讨论

集合与连接 (Set/Join) 类

union all 与 join 是仅有的可以有多个输入的算子.join 是二元输入的算子, union all可以看做二元输入算子,也可以看做多元输入算子.

只关注 union all 与 join
集合相关的很多操作,如并集,交集,差集,补集 以及一些 in/not in 的语法对应的实现算子, 在数据库系统实现的实践上,都可以在编译阶段Parser生成关系算子树之前比较好的转换为 union all 和 join 等算子的组合操作,简化实现的同时也不损失性能.
所以不用分析 union all 与 join 之外的其他二元/多元算子.

Union All

\(\cup(e_1, e_2, ....)\)

为union符号增加下标b更准确的区分于集合语义的并, 如 \(\cup_b\).
因为大多数情况下不会讨论集合相关的代数, 所以默认\(\cup\)作用于包上表示的是 union all 而非 union.

Join 类

符号与对应 join 名称

\[\begin{aligned} & × & cross\\ & ⋈ & inner\\ & ⟕ & left\\ & ⟖ & right\\ & ⟗ & full\\ & ⋉ & semi\\ & \vartriangleright & anti \\ \end{aligned} \hspace{100cm} \]

定义

\[\begin{aligned} e_1 × e_2:= & \{y \circ x|y \in e_1, x \in e_2\}_b, \\ e_1 ⋈_p e_2:= & \{y \circ x|y \in e_1, x \in e_2, p(y, x)\}_b, \\ e_1 ⋉_p e_2:= & \{y|y \in e_1, \exists x \in e_2, p(y, x)\}_b, \\ e_1 \vartriangleright_p e_2:= & \{y|y \in e_1, \neg \exists x \in e_2, p(y, x)\}_b, \\ e_1 ⟕_p e_2:= & (e_1 ⋈_p e_2) \cup((e_1 \vartriangleright_p e_2) ×\{\perp_{\mathcal{A}(e_2)}\}), \\ e_1 ⟗_p e_2:= & (e_1 ⋈_p e_2) \\ & \cup((e_1 \vartriangleright_p e_2) ×\{\perp_{\mathcal{A}(e_2)}\}) \\ & \cup(\{\perp_{\mathcal{A}(e_1)}\} ×(e_2 \vartriangleright_p e_1)) . \end{aligned} \hspace{100cm} \]

下标 \(p\) 表示谓词.
类似 \(\cup((e_1 \vartriangleright_p e_2) ×\{\perp_{\mathcal{A}(e_2)}\})\) 的表达式,表示外连接的补 null 步骤.

内连接, 叉积, 半连接, 反连接的在 \(\sigma\) 下的变换

叉积和内连接的转换仅在于谓词绑定的位置不同.
因为 \(\sigma_p(e):= \{x| x \in e, p(x)\}_b\) 所以:

\(\sigma_p(e_1 \times e_2) \equiv e_1 \bowtie_p e_2\)

半连接和反连接的计算是在对主表做过滤, 所以可以转化为 filter 算子

\(e_1 \ltimes_p e_2 = \sigma_{\sigma_p(e_2) \neq \emptyset}(e_1) \\ e_1 \vartriangleright_p e_2 = \sigma_{\sigma_p(e_2) = \emptyset}(e_1)\)

谓词 \((\sigma_p(e_2) \neq \emptyset)(x)\) 作用于 x 时,计算过程为将输入 x 绑定到 \(\sigma_p(e_2)\)\(p\) 的*变量, 得到结果, 再判断是否为空.

定义部分总结

以下给出之前提到的概念拓展关系总结, 也就是如何从基本定义出发推理出这些新增的概念, 也方便将概念串接起来.

从值的扩张出发.

\[\rm{ \begin{aligned} &值 \xrightarrow{\rm{枚举所有值的集合与null}} 值域 \xrightarrow{\rm{将值域分类}}\ 类型 \\ &值 \xrightarrow{属性} 元组 \xrightarrow{多重集} 包\xrightarrow{共享属性} 关系 \\ &值 \xrightarrow{计算} 标量函数 \xrightarrow{逻辑相关部分} 二值逻辑 \xrightarrow{\rm{null}} 三值逻辑 \\ &关系 \xrightarrow{计算} 关系算子\\ \end{aligned} } \hspace{100cm} \]

从数据分析需求出发.

\[\rm{ \begin{aligned} &数据 \xrightarrow{数据逻辑独立性, 约束数据质量, 简明} 关系(表格) \xrightarrow{空值处理} \rm{null} \\ &分析 \xrightarrow{在表格上} 关系算子 \xrightarrow{列选,计算,过滤} \Pi,\sigma \xrightarrow{集合处理能力,含合并, 拆分等} 集合算子, \cup_b \\ &关系算子\xrightarrow{分组聚合统计} \Gamma \xrightarrow{排序, 行选} Sort, Limit \xrightarrow{有序分组聚合统计} Window\\ &关系算子\xrightarrow{多表数据关联分析} \times, \ltimes, \bowtie,\vartriangleright, ... \\ &关系算子\xrightarrow{管理数据生命周期} Sink,TableScan, ... \end{aligned} } \hspace{100cm} \]

从提供价值的需求出发.
\(数据\xrightarrow{预测与改进数据需要} 计算 \xrightarrow{预测与改进计算需要} 优化器 \xrightarrow{改进优化器需要} 更多优化规则, 包含逻辑优化规则 \xrightarrow{发现和改进逻辑优化规则}定义和分析逻辑算子等价变换\)

算子定义汇总

\[\begin{align*} \sigma_p(e) :=& \{x|x \in e, p(x)\}_b \\ \Pi_A(e) := & \{[a_1:x.a_1, ..., a_n:x.a_n]| x \in e\}_b \qquad //A = \{a_1, ..., a_n\}_b \\ \Pi^D(e) := & _{\mathfrak m=1}(e)\\ \chi_{a:f}(e) := & \{x \circ [a:f(x)] | x \in e\}_b\\ \chi_{e_2}(e) :=& \{e_2(x) | x \in e\}_b \\ \chi_{F}(e) := & \{x \circ [a_1 : f_1(x) ,..., a_n : f_n(x)) ] | x \in e\}_b \qquad //F = a_1:f_1, ..., a_n:f_n \\ \Gamma_{G;g:f}(e) :=& \{y \circ [g:x] | y \in \Pi_G^D, x = f(\{z|z \in e, z.G \dot= y.G\}) \}_b \\ \Gamma_{G:F}(e):=&\{y\circ [a_1:x_1, ..., a_n:x_n] | y \in \Pi^D_G, x_i = f_i(\{z|z \in e, z.G \dot= y.G \}))\}_b \qquad //F = a_1:f_1, ..., a_n:f_n\\ \nu_{G;g}(e) := & \Gamma_{G;g:\Pi_{\overline{G}}}(e)\\ \mu_g(e):=& \{y.[\mathcal{A}(e)/g] \circ x | y \in e, x \in y.g\}_b\\ \mu_{a:g}(e):=& \{y.[\mathcal{A}(e)/g] \circ [a:g] | y \in e, x \in y.g\}_b\\ \\ e_1\cup_b e_2 :=& _{\mathfrak {m=m_1 + m_2}}(e_1, e_2)\\ e_1 × e_2:= & \{y \circ x|y \in e_1, x \in e_2\}_b, \\ e_1 ⋈_p e_2:= & \{y \circ x|y \in e_1, x \in e_2, p(y, x)\}_b, \\ e_1 ⋉_p e_2:= & \{y|y \in e_1, \exists x \in e_2, p(y, x)\}_b, \\ e_1 \vartriangleright_p e_2:= & \{y|y \in e_1, \neg \exists x \in e_2, p(y, x)\}_b, \\ e_1 ⟕_p e_2:= & (e_1 ⋈_p e_2) \cup((e_1 \vartriangleright_p e_2) ×\{\perp_{\mathcal{A}(e_2)}\}), \\ e_1 ⟗_p e_2:= & (e_1 ⋈_p e_2) \\ & \cup((e_1 \vartriangleright_p e_2) ×\{\perp_{\mathcal{A}(e_2)}\}) \\ & \cup(\{\perp_{\mathcal{A}(e_1)}\} ×(e_2 \vartriangleright_p e_1)) . \end{align*} \hspace{100cm} \]