查询优化概述
数据库优化器的输入是一个关系代数表达式,经过查询优化后,输出一个查询执行计划,并且使输出的执行计划的代价尽可能小
查询优化的步骤可以分为三步:
- 产生一些逻辑上与输入表达式等价的关系代数表达式
- 将所产生的表达式转换成执行计划(一个表达式可能对应多个执行计划)
- 对产生的每个执行计划,估计其执行代价,选择一个代价最小的执行计划输出
代价估计
我们已经知道应该用磁盘传输的块数和寻道次数来估计一个执行计划的代价,而磁盘传输块数和寻道次数一般和关系的块数有关,所以我们只要能估计出进行运算的每个关系的块数,就能估计出整个执行计划的代价
统计信息
如果不借助额外的统计信息,我们是不可能估算出每个关系的块数的,我们对每个关系都额外统计了一些信息,包括关系的元组数、关系的块数、单个元组的字节数、一个块中可以容纳的元组数等
此外,还有一些关于属性的统计信息,设 V(A,r) 表示关系 r 中属性 A 的不同取值个数,对于关系 r 的所有属性 A1,A2,⋯,An ,我们记录所有的 V(A1,r),V(A2,r),⋯,V(An,r) ,根据需要,还可能统计某些属性集的不同取值个数
仅统计属性的不同取值个数,有时还不够精确,因此对于关系上的属性,可能还会保存一张记录其取值分布的直方图

对于直方图的一个取值区间,通常不仅仅保存区间中值的频度,还会保存区间中不同取值的个数,这是为了之后估计运算结果集的大小
对于有索引的关系,还会保存有关索引的统计信息,例如 B+ 树索引的高度、B+ 树索引叶节点数等
统计信息是需要维护的,但如果每次修改关系时,都去更新统计信息,带来的开销太大,并且我们也不需要统计信息时刻保持完全精确,较小的误差完全是可以忍受的,因为我们本来就只是拿统计信息来估算代价
因此一些数据库系统在关系被修改时,一般不立即更新统计信息,而是在某些时间点更新统计信息,例如关系的元组数目发生了显著变化时,利用当前统计信息估算的代价和实际运行的代价相差很大时,数据库系统负载较轻时
另一些数据库系统从不自动更新统计信息,而是由数据库管理员决定何时更新统计信息
此外,统计数据常常是基于整个关系所有元组的一个样本来计算的,这个样本是从所有元组中随机抽样得来的
运算结果集大小的估计
如果进行运算的关系都是数据库关系,那我们可以很容易地估计出这次运算的代价,因为我们有所有数据库关系的统计数据,自然也就知道这些关系的块数
但问题在于,进行运算的关系有可能是一个临时关系,这个临时关系是某个运算的结果,我们需要能够估计出临时关系的块数,由于临时关系的元组的字节数可以根据临时关系的模式计算出,因此我们只需要估计出临时关系的元组数,就能通过计算得出临时关系的块数,也就是说对于一个运算,我们要能估计出结果集的大小
选择运算
对选择运算结果集大小的估计依赖于选择条件
如果选择条件是等值条件,即 σA=a(r) ,我们可以假设关系 r 的属性 A 的取值均匀分布,因此我们可以估计结果集的大小为 V(A,r)nr ,其中 nr 为关系 r 的元组数,如果在关系 r 的属性 A 上有直方图,我们可以在直方图中找到 a 所在的区间,然后用区间的频度来代替 nr ,用区间中不同取值的个数来代替 V(A,r)
如果选择条件是范围条件,例如 σA≥a(r) ,我们还是可以假设取值均匀分布,可以估计结果集的大小为 nr⋅max(A,r)−min(A,r)a−min(A,r) ,其中 min(A,r) 和 max(A,r) 分别表示关系 r 的属性 A 取的最小值和最大值,通常会包含在统计信息中,如果在关系 r 的属性 A 上有直方图,我们可以在直方图中找到 a 所在的区间,可以估计区间中满足选择条件的元组数为 n⋅M−ma−m ,其中 n 表示区间的频度,m 和 M 分别表示区间的左右端点,其它区间中的元组要么全部满足选择条件,要么全部不满足选择条件
如果选择条件是多个条件的合取,即 σθ1∧θ2∧⋯∧θn(r) ,我们可以按之前的方法分别算出 σθ1(r),σθ2(r),⋯,σθn(r) 的结果集大小,记为 t1,t2,⋯,tn ,我们假设每个条件相互独立,可以估计满足合取条件的结果集大小为 nr⋅nrnt1∗t2∗⋯∗tn
如果选择条件是多个条件的析取,还是假设每个条件相互独立,可以估计满足析取条件的结果集大小为 nr−nr∗(1−nrt1)∗(1−nrt2)∗⋯∗(1−nrtn)
如果选择条件是某个条件取反,即 σ¬θ(r) ,我们可以按之前的方法计算出 σθ(r) 的结果集大小,记为 t ,可以估计满足取反条件的结果集大小为 nr−t
连接运算
笛卡尔积运算 r×s 的结果集大小为 nr⋅ns
估计自然连接运算 r⋈s 的结果集大小时要分情况讨论,设关系 r 的属性集为 R ,关系 s 的属性集为 S
若 R∩S 是 r 的码,则 s 的每个元组至多跟 r 的一个元组连接,可以估计结果集大小为 ns ,若 R∩S 是 s 参照 r 的外码,则结果集大小等于 ns
当 R∩S 既不是 r 的码,也不是 s 的码时,设 R∩S={A} ,我们假设 r 和 s 的属性 A 的取值均匀分布,从关系 r 的角度考虑,可以估计 r 的每个元组跟 s 的 V(A,s)ns 个元组连接,结果集大小为 V(A,s)nr⋅ns ,而从关系 s 的角度考虑,可以估计 s 的每个元组跟 r 的 V(A,r)nr 个元组连接,结果集大小为 V(A,r)nr⋅ns ,当 V(A,r)=V(A,s) 时,两种估计方法得到的结果不同,由于 V(A,r)=V(A,s) 代表着有一些元组不参与连接,因此取两个结果中较小的一个可能更准确一些
当 r 和 s 在属性 A 上都有直方图,且两个直方图都有相同的区间时,我们可以在每个区间中使用之前的方法,最后把不同区间产生的连接元组数相加,就能得到结果集大小
对于 θ 连接 r⋈θs ,我们可以将它写作 σθ(r×s) ,然后使用之前估计笛卡尔积运算和选择运算的结果集大小的方法
投影运算
由于投影运算去除了重复元组,因此对于投影运算 ΠA(r) ,其结果集大小为 V(A,r)
聚集运算
聚集运算例如 Gsum(A)(r) ,结果集为 V(A,r)
集合运算
若参与集合运算的两个集合是对同一个关系进行选择运算的两个结果集,我们可以将集合运算重写为选择运算,例如将 σθ1(r)∪σθ2(r) 重写为 σθ1∨θ2(r) ,将 σθ1(r)∩σθ2(r) 重写为 σθ1∧θ2(r) ,将 σθ1(r)−σθ2(r) 重写为 σθ1∧¬θ2(r) ,然后使用估计选择运算的结果集大小的方法
其它情况下,对 r∪s 的结果集大小估计为 nr+ns ,对 r∩s 的结果集大小估计为 nr−ns (假设 nr>ns),对 r−s 的结果集大小估计为 nr
外连接
要估计关系 r 和关系 s 外连接的结果集大小,先用之前的方法估计 r⋈s 的结果集大小,记为 n ,对 r ⟕ s 的结果集大小估计为 n+nr ,对 r ⟗ 的结果集大小估计为 n+nr+ns
不同取值个数的估计
现在我们已经会估计运算结果集的大小了,对于一些运算,我们需要知道参与运算的关系的某个属性中不同取值的个数,如果这个参与运算的关系是数据库关系,那么可以通过查看统计信息直接得到结果,但参与运算的关系还有可能是另一个运算的结果集,因此我们必须会估计运算结果集的属性中不同取值的个数
选择运算
计算 V(A,σθ(r)) 时,对于不同的选择条件 θ 要使用不同的方法
如果 θ 是一个等值条件,则 V(A,σθ(r))=1
如果 θ 是多个等值条件的析取,则 V(A,σθ(r)) 等于不同等值条件的个数
如果 θ 是一个不等条件,假设 σθ(r) 的结果集大小为 n ,则我们估计 V(A,σθ(r))=nrn⋅V(A,r)
对于其它情况,我们假设一个元组在属性 A 上的取值与该元组是否满足选择条件是独立的,这样就可以用概率论的知识推导出 V(A,σθ(r)) 的估计值,不过通常情况下我们简单地估计 V(A,σθ(r))=min(V(A,r),nσθ(r))
连接运算
计算 V(A,r⋈s) 时,假设 A 是一个属性集,根据 A 中的属性属于哪些关系,我们选择不同的方法
若 A 中所有属性都来自 r ,则我们估计 V(A,r⋈s)=min(V(A,r),nr⋈s)
若 A 中部分属性来自 r ,部分属性来自 s ,我们把来自 r 的属性集记为 A1 ,来自 s 的属性集记为 A2 ,注意可能有部分属性即在 A1 中,也在 A2 中,我们把只来自于 r 的属性集记为 A3 ,只来自于 s 的属性集记为 A4 ,我们估计 V(A,r⋈s)=min(V(A1,r)⋅V(A4,s),V(A2,s)⋅V(A3,r),nr⋈s)
投影运算
投影运算一般不会改变不同取值的个数,也就是说 V(A,ΠA(r))=V(A,r)
聚集运算
当聚集函数为 min(A) 或 max(A) 时,我们估计 V(min(A),BGmin(A)(r))=min(V(A,r),V(B,r)) ,其中 B 表示分组属性
其它情况下,我们假设聚集函数的值不重复,则我们估计 V(f(a),Gf(A)(r))=V(A,r)
等价规则
我们不能期待用户写出最有效率的查询语句,因此在用户提供的查询语句效率很低时,数据库系统要能够将其进行等价转换,也就是说要想进行查询优化,必须能够知道输入的关系代数表达式和哪些关系代数表达式等价
常见等价规则
交换律
选择运算满足交换律,σθ1(σθ2(r))=σθ2(σθ1(r))
θ 连接运算满足交换律,r⋈θs=s⋈θr
集合的交、并运算满足交换律,r∩s=s∩r ,r∪s=s∪r
结合律
θ 连接运算满足以下方式的结合律,(r⋈θ1s)⋈θ2∧θ3t=r⋈θ1∧θ3(s⋈θ2t) ,其中 θ1 只包含 r 和 s 的属性,θ2 只包含 s 和 t 的属性,θ3 包含 r,s,t 的属性
集合的交、并运算满足结合律,(r∩s)∩t=r∩(s∩t) ,(r∪s)∪t=r∪(s∪t)
分配律
在满足某些条件时,选择运算对 θ 连接运算满足分配律:
- 选择条件 θ1 使用的属性只涉及参与连接的两个关系之一时(例如 r),有 σθ1(r⋈θ2s)=σθ1(r)⋈θ2s
- 选择条件 θ1 只涉及 r 的属性,选择条件 θ2 只涉及 s 的属性时,有 σθ1∧θ2(r⋈θ3s)=σθ1(r)⋈θ3σθ2(s)
在满足某些条件时,投影运算对 θ 连接运算满足分配律:
- 设投影属性集为 L1∪L2 ,L1 中的属性来自关系 r ,L2 中的属性来自关系 s ,当连接条件 θ 只涉及 L1∪L2 中的属性时,有 ΠL1∪L2(r⋈θs)=ΠL1(r)⋈θΠL2(s)
- 设 L3 是关系 r 的出现在连接条件中的属性集,L4 是关系 s 的出现在连接条件中的属性集,有 ΠL1∪L2(r⋈θs)=ΠL1∪L2(ΠL1∪L3(r)⋈θΠL2∪L4(s))
选择运算对集合交、并、差运算有分配律,σθ(r∩s)=σθ(r)∩σθ(s) ,σθ(r∪s)=σθ(r)∪σθ(s) ,σθ(r−s)=σθ(r)−σθ(s)
投影运算对集合交、并、差运算有分配律,ΠL(r∩s)=ΠL(r)∩ΠL(s) ,ΠL(r∪s)=ΠL(r)∪ΠL(s) ,ΠL(r−s)=ΠL(r)−ΠL(s)
级联
选择运算的级联:σθ1(σθ2(r))=σθ1∧θ2(r)
投影运算的级联:ΠL1(ΠL2(r))=ΠL1(r)
其它规则
根据 θ 连接的定义,有 σθ(r×s)=r⋈θs ,σθ1(r⋈θ2s)=r⋈θ1∧θ2s
类似于选择运算对集合交、差运算的分配律,有 σθ(r∩s)=σθ(r)∩s ,σθ(r−s)=σθ(r)−s
查询优化器
有了前面的知识,我们就可以对关系代数表达式进行等价转换和对一个查询计划进行代价估计了,基于这些,就可以开始设计查询优化器了
枚举执行计划
最简单的查询优化器按以下方式工作:得到一个关系代数表达式后,查询优化器利用等价规则枚举出所有与输入表达式等价的关系代数表达式,然后为这些表达式生成所有可能的执行计划,同时对每个执行计划进行代价估计,最后选择估计代价最小的执行计划执行
显然,利用上述方式进行查询优化,效率很低,不过存在许多优化的手段,使得我们不用枚举所有的执行计划,就能得到代价最小的执行计划或一个近似代价最小的执行计划,最常用的方法是动态规划,此外,如果我们在查询优化的过程中,已经找到了一个代价为 c 的执行计划,那么在之后的查询优化过程中,若给某个子表达式找到了一个代价大于 c 的子执行计划,则直接对该子执行计划剪枝,而不是继续使用这个子执行计划来生成完整的执行计划
多表连接的优化
由于连接运算满足结合律,因此多个关系进行连接时,有很多种连接顺序,事实上,如果将 n 个关系的连接转化成一棵表达式树,每种连接顺序都对应一种完全二叉树,n 个叶节点的无标号完全二叉树有 n1(n−12(n−1)) 种,n 个叶节点的有标号完全二叉树的种类数在此基础上乘以 n! ,为 (n−1)!(2(n−1))! 种,因此 n 个关系进行连接时,有 (n−1)!(2(n−1))! 种连接顺序
由于在 n 增长时,(n−1)!(2(n−1))! 增长过快,通过枚举所有连接顺序从而找到代价最小的执行计划的效率太低
不过,其实我们不需要枚举所有连接顺序,就能得到代价最小的连接顺序,假设参与连接的关系集为 s ,对于 s 的每个子集 s′ ,只需要知道 s′ 的代价最小的连接顺序,之后就不需要考虑 s′ 的任何其它连接顺序了,这其实就是一个子集 DP 的过程,时间复杂度 O(3n) ,比起枚举全部连接顺序,子集 DP 在效率上已经有了很大的进步
此外,生成的中间结果集是否有序,也是在找代价最小的连接顺序时需要考虑的,因为一个中间结果集是否有序,可能影响这个结果集在之后连接时的效率(例如之后进行归并连接),因此在 DP 时,包含元组相同但顺序不同的两个结果集,应该视为两个不同的状态,通常情况下,一个结果集只有很少的排序方式有可能影响之后的连接效率,因此 DP 的状态数不会增加太多
启发式优化
启发式优化就是,利用一些启发式规则,在所有与输入表达式等价的关系代数表达式中排除一部分,只需要为剩下的关系代数表达式生成执行计划,找到代价最小的执行计划即可,启发式规则并不能保证一定将可生成最小执行代价的关系代数表达式保留下来,但通常情况下,我们可以认为在启发式规则保留下来的表达式中,至少能生成代价近似最小的执行计划
常见的启发式规则如下:
- 尽可能早地执行选择运算,这条规则在绝大多数情况下都有利于减少执行计划的代价,但依然存在例外,例如 σθ(r⋈s) ,其中 θ 只涉及 s 的属性,如果我们采用了这条启发式规则,就会将表达式转换为 r⋈σθ(s) ,大部分情况下转换后的表达式执行得更快,然而若 r 的元组数相对于 s 的元组数来说很少,s 在连接属性上有索引,并且 θ 是一个非常复杂的选择条件,这个时候我们先将 r 和 s 连接在一起,再执行选择可能会更快一些
- 尽可能早地执行投影运算,在既有选择运算又有投影运算时,建议先做选择运算,因为选择运算可能会大大减小关系的元组数
- 在考虑连接顺序时,只考虑左深连接顺序,也就是说连接运算的两个输入关系中,总有一个是数据库关系,而不进行两个中间结果集的连接
优化成本预算
查询优化的主要目的是减少查询执行的时间,但查询优化又要占用 CPU 时间,因此如何进行权衡就变得十分重要
查询优化器工作时,一般会有一个成本预算,当优化的时间超过成本预算时,停止搜索最优计划,直接返回当前找到的最优计划,预算的成本可以是动态的,如果当前已经找到了低开销的代价,就降低预算,避免优化花费过多时间,反而得不偿失,另一方面,如果当前找到的最优计划的代价仍然很大,就可以提升预算,增大找到小代价计划的概率
为了在优化的时间超过成本预算时,返回的计划的代价尽可能小,查询优化器通常实现了多种查询优化的算法,在查询优化开始时,优化器先使用复杂度低但不精确的算法找到一个较优的计划,再使用精度高、复杂度也高的算法继续优化,寻找更优的计划
嵌套子查询的优化
考虑一个嵌套子查询的例子:
select *
from r1
where exists
(select *
from r2
where r1.A=r2.A)
如果真的按字面意思执行查询,那么对于 r1 的每一个元组,都要至少读取 r2 的一块,因此会产生大量的随机 I/O ,而如果我们改写上述查询,将其改成:
select *
from r1,r2
where r1.A=r2.A
则我们只需要进行一次等值连接即可
然而大多数情况下,嵌套子查询不像例子中那么简单,没法直接像例子中那样改写,不过我们仍能通过建立临时关系,对嵌套子查询进行改写,假设我们有一个嵌套子查询:
select *
from r1
where P1 and exists
(select *
from r2
where P2 and P3)
其中 P2 只涉及 r2 中的属性,而 P3 涉及 r1 和 r2 的属性,我们可以将其改写为:
create table t as
select distinct L
from r2
where P2
select *
from r1,t
where P1 and P3
其中 L 包含了 r2 在嵌套子查询中使用的所有属性