查询优化即求解给定查询语句的高效执行计划的过程。它既是关系数据库管理系统实现的关键技术,又是关系系统的优点所在。由DBMS进行查询优化的好处在于:查询优化的优点不仅在于用户不必考虑如何最好的表达查询以获得较高的效率,而且在于系统可以比用户程序的"优化"做得更好。
查询计划,从形式上看是一颗二叉树,树叶是每个单表对象,两个树叶的父节点是一个连接操作符连接后的中间结果(另外还有一些其他节点如排序等也可以作为中间结果),这个结果是一个临时关系,这样直至根节点。
从一个查询计划看,涉及的主要"关系节点"包括:
查询优化的总目标是选择有效的策略,求得给定关系表达式的值,使得查询代价较小。因为查询优化的搜索空间有时非常大,实际系统选择的策略不一定是最优的,而是较优的。
查询优化主要包括逻辑优化和物理优化。其中,逻辑优化又可包含语法级查询优化、基于规则的优化等;而物理优化主要指基于代价的优化。语法级优化是基于语法的等价转换;基于规则的优化(如依据关系代数的规则或依据经验的规则等)具有操作简单且能快速确定执行方式的优点,但这种方法只是排除了一部分不好的可能;基于代价的优化是在查询计划生成过程中,计算每条存取路径进行量化比较,从而得到开销最小的情况,但如果组合情况多则开销的判断时间就很多。查询优化器的实现,多是这两种优化策略的组合使用。
查询优化器在逻辑优化阶段主要解决的问题是:如何找出SQL语句的等价变换形式,使SQL执行更高效。
代数优化是基于关系代数等价变换规则的优化方法。
代数优化策略是通过对关系代数表达式的等价变换来提高查询效率。所谓关系代数表达式的等价是指用相同的关系代替两个表达式中相应的关系所得到的结果是相同的。两个关系表达式E1和E2是等价的。
常用的关系代数等价变换规则如下:
设E1和E2为关系代数表达式,F为连接运算条件,则有:
E1×E2 ≡ E2×E1
E1⋈E2 ≡ E2⋈E1
≡
对于连接和笛卡尔积运算,可以交换前后位置,其结果不变。例如,两表连接算法中有嵌套循环连接算法,对外表和内表有要求,外表尽可能小则有利于做"基于块的嵌套循环连接",所以通过交换律可以将元组少的表作为外表。
设E1、E2、E3为关系代数表达式,F1、F2为连接运算条件。则有:
(E1×E2)×E3 ≡ E1×(E2×E3)
(E1⋈E2)⋈E3 ≡ E1⋈(E2⋈E3)
≡
对于连接、笛卡尔积运算,如果新的结合有利于减少中间关系的大小,则可以优先处理。
设E为关系代数表达式,Ai(i=1,2,3,…,n),Bj(j=1,2,3,…,m)是属性名,且{A1,A2,…,An}为{B1,B2,…,Bm}的子集。则有:
∏A1,A2,…,An(∏B1,B2,…,Bm(E)) ≡ ∏A1,A2,…,An (E)
在同一个关系上,只需做一次投影运算,且一次投影时选择多列同时完成。所以许多数据库优化引擎会为一个关系收集齐该关系上的所有列,即目标列和WHERE、GROUP BY等子句中涉及到的所有该关系的列。
设E为关系代数表达式,F1、F2为选择条件。则有:
σF1(σF2(E)) ≡ σF1∧F2(E)
此变换规则对于优化的意义在于:选择条件可以合并,使得一次选择运算就可检查全部条件,而不必多次过滤元组,所以可以把同层的合取条件收集在一起,统一进行判断。
设E为关系代数表达式,F为选择条件,Ai(i=1,2,3,…,n)是属性名。选择条件F只涉及属性A1,A2,…,An。则有:
σF(∏A1,A2,…,An (E)) ≡∏A1,A2,…,An(σF(E))
此变换规则对于优化的意义在于:先投影后选择可以改为先选择后投影,这对于以行为单位来存储关系的主流数据库而言,很有优化意义。按照这种存储方式,系统总是先获取元组,然后才能解析得到其中的列。
设E为关系代数表达式,F为选择条件,Ai(i=1,2,3…,n)是属性名,选择条件F中有不属于A1,A2,…,An的属性B1,B2,…,Bn。则有:
∏A1,A2,…,An(σF(E)) ≡ ∏A1,A2,…,An(σF(∏A1,A2,…,An,B1,B2,…,Bm(E)))
此变换规则对于优化的意义在于:先选择后投影可以改为先做带有选择条件中的列的投影,然后选择,最后再完成最外层的投影。这样内层的选择和投影可以同时进行,不会增加过多的计算开销,但能减小中间结果集的规模。
设E1、E2为关系代数表达式,F为选择条件,F中涉及的属性都是E1中的属性,则有:
σF(E1×E2) ≡ σF(E1)×E2
如果F=F1∧F2,且F1只涉及E1中的属性,F2只涉及E2中的属性,则有:
σF(E1×E2) ≡ σF1(E1)×σF2(E2)
此变换规则对于优化的意义在于:条件下推到相关的关系上,先做选择后做笛卡尔积运算,这样可以减小中间结果的大小。
如果E1和E2有相同的属性名,且E= E1∪E2,则有:
σF(E1∪E2) ≡ σF(E1) ∪σF (E2)
此变换规则对于优化的意义在于:条件下推到相关的关系上,先选择后做并运算,可以减小每个关系输出结果的大小。
如果E1和E2有相同的属性名,则:
σF(E1-E2) ≡ σF(E1)-σF(E2)
此变换规则对于优化的意义在于:条件下推到相关的关系上,先选择后做差运算,可以减小每个关系输出结果的大小。
设A1,A2,…,An是E1的属性,B1,B2,…,Bm是E2的属性,则有:
∏A1,A2,…,An,B1,B2,…,Bm(E1×E2) ≡ ∏A1,A2,…,An(E1)×∏B1,B2,…,Bm(E2)
此变换规则对于优化的意义在于:先投影后做笛卡尔积,可减少做笛卡尔积前每个元组的长度,使得计算后得到的新元组的长度也变短。
如果E1和E2有相同的属性名,则有:
∏A1,A2,…,An (E1∪E2) ≡ ∏A1,A2,…,An (E1)∪∏A1,A2,…,An (E2)
此变换规则对于优化的意义在于:先投影后做并运算,可减少做并运算前每个元组的长度。
针对不同运算符的优化规则如表5-1~5-3所示。
运算符 | 子类型 | 根据特点可得到的优化规则 | 可优化的原因 |
---|---|---|---|
选择 | 对同一个表的同样选择条件,作一次即可。 | 单行文本输入框 | 幂等性:多次应用同一个选择有同样效果; 交换性:应用选择的次序在最终结果中没有影响 选择可有效减少在它的操作数中的元组数的运算(元组个数减少)。 |
分解有复杂条件的选择 | 合取,合并多个选择为更少的需要求值的选择,多个等式则可以合并①。 | 合取的选择等价于针对这些单独条件的一系列选择。 | |
析取,分解它们使得其成员选择可以被移动或单独优化②。 | 析取的选择等价于选择的并集。 | ||
选择和笛卡尔积 | 尽可能先做选择。 | 运算关系分别有N和M行,先做积运算将包含N×M行。先做选择运算减少N和M,则可避免不满足条件的元组参与积运算,节约时间同时减少结果集的大小。 | |
尽可能下推选择。 | 如果积运算后面没有跟随选择运算,可以尝试使用其它规则从表达式树更高层下推选择。 | ||
选择和集合运算 | 选择下推到的集合运算中,如表5-2中的3种情况。 | 选择在差集、交集和并集算子上满足分配律。 | |
选择和投影 | 在投影之前进行选择。 | 如果选择条件中引用的列是投影中的列的子集,则选择与投影满足交换性。 | |
投影 | 基本投影性质 | 尽可能先做投影 | 投影是幂等的;投影可以减少元组大小。 |
投影和集合运算 | 投影下推到集合的运算中,如表5-3中的情况。 | 投影在差集、交集和并集算子上满足分配律。 |
表5-2 选择下推到集合的运算
初始式 | |||
---|---|---|---|
等价表达式一 | 等价表达式二 | 等价表达式三 | |
σA(R-S) | σA(R)-σA(S) | σA(R)-S | |
σA(R∪S) | σA(R)∪σA(S) | ||
σA(R∩S) | σA(R)∩σA (S) | σA(R)∩S | R∩σA(S) |
表5-3 投影下推到集合的运算
初始式 | 优化后的等价表达式 |
---|---|
∏A1,A2,…,An(R-S) | ∏A1,A2,…,An(R)- ∏A1,A2,…,An(S) |
∏A1,A2,…,An(R∪S) | ∏A1,A2,…,An(R) ∪∏A1,A2,…,An(S) |
∏A1,A2,…,An(R∩S) | ∏A1,A2,…,An(R) ∩∏A1,A2,…,An(S) |
包括:
语法级优化要解决的主要问题是找出SQL语句的等价变换形式,使得SQL执行更高效,包括:
以下介绍几种常见的优化方法。
早期的查询优化器对子查询都采用嵌套执行的方式,即对父查询中的每一行都执行一次子查询,这样效率很低,因此对其进行优化很有必要。例如,将子查询转为连接操作之后,有如下好处:
(1) 子查询合并
在语义等价条件下,多个子查询可以合并成一个子查询,这样多次表扫描,多次连接减少为单次表扫描和单次连接。例如:
SELECT *
FROM t1
WHERE a1<10 AND (
EXISTS (SELECT a2 FROM t2 WHERE t2.a2<5 AND t2.b2=1) OR
EXISTS (SELECT a2 FROM t2 WHERE t2.a2<5 AND t2.b2=2)
);
可优化为:
SELECT *
FROM t1
WHERE a1<10 AND (
EXISTS (SELECT a2 FROM t2 WHERE t2.a2<5 AND (t2.b2=1 OR t2.b2=2)
);
此例中,两个EXISTS子查询合并为一个子查询,查询条件也进行了合并。
(2) 子查询展开
子查询展开又称子查询反嵌套,子查询上拉。实质是把某些子查询重写为等价的多表连接操作。带来好处是,有关的访问路径、连接方法和连接顺序可能被有效使用,使得查询语句的层次尽可能地减少。常见的IN / ANY / SOME / ALL / EXISTS依据情况转为半连接(SEMI JOIN)。例如:
SELECT *
FROM t1, (SELECT * FROM t2 WHERE t2.a2>10) v_t2
WHERE t1.a1<10 AND v_t2.a2<20;
可优化为:
SELECT *
FROM t1, t2
WHERE t1.a1<10 AND t2.a2<20 AND t2.a2>10;
此例中,原本的子查询变为了t1、t2表的连接操作,相当于把t2表从子查询中上拉了一层。
子查询展开是一种最常用的子查询优化技术,如果子查询是只包含选择、投影、连接操作的简单语句,没有聚集函数或者group子句,则可以上拉,前提是上拉后的结果不能带来多余元组,需遵循以下规则:
子查询展开的具体步骤如下:
(3) 聚集子查询消除
这种方法将聚集子查询的计算上推,使得子查询只需计算一次,并与父查询的部分或全表做左外连接。例如:
SELECT *
FROM t1
WHERE t1.a1 > (SELECT avg(t2.a2) FROM t2);
可优化为:
SELECT t1.*
FROM t1, (SELECT avg(t2.a2) FROM t2) as tm(avg_a2) )
WHERE t1.a1 ? tm.avg_a2;
(4) 其他
此外还有利用窗口函数消除子查询、子查询推进等技术,本文不再细述。
(1) IN类型子查询
IN类型有3种格式:
格式一:
outer_expr [not] in (select inner_expr from ... where subquery_where)
格式二:
outer_expr = any (select inner_expr from ... where subquery_where)
格式三:
(oe_1, ..., oe_N) [not] in (select ie_1, ..., ie_N from ... where subquery_where)
对于in类型子查询的优化,如表5-4所示。
情况一:outer_expr和inner_expr均为非NULL值。
优化后的表达式为:
exists (select 1 from ... where subquery_where and outer_expr=inner_expr)
子查询优化需要满足2个条件:
outer_expr和inner_expr不能为NULL;
不需要从结果为FALSE的子查询中区分NULL。
情况二:outer_expr是非空值。
优化后的表达式为:
exists (select 1 from ... where subquery_where and
(outer_expr=inner_expr or inner_expr IS NULL);
情况三:outer_expr为空值。
则原表达式等价为:
NULL in (select inner_expr FROM ... where subquery_where)
当outer_expr为空时,如果子查询结果为:
对上面的等价形式,还有2点需说明:
select col1 from t1 where col1 =ANY (select col1 from t2);
select col1 from t1 where col1 IN (select col1 from t2);
(2) ALL/ANY/SOME类型子查询
ALL/ANY/SOME子查询格式如下:
outer_expr operator ALL (subquery)
outer_expr operator ANY (subquery)
outer_expr operator SOME (subquery)
其中,operator是操作符,可以是>、>=、=、<、<=中任何一个。其中,
如果子查询中没有group by子句,也没有聚集函数,则以下表达式可以使用聚集函数MAX/MIN做等价转换:
val>=ALL (select ...)
等价变换为:val>= (select MAX...)
val<=ALL (select ...)
等价变换为:val<= (select MAX...)
val>=ANY (select ...)
等价变换为:val>= (select MIN...)
val>=ANY (select ...)
等价变换为:val>= (select MAX...)
(3) EXISTS类型子查询
存在谓词子查询格式为:[NOT] EXISTS (subquery)
需要注意几点:
所谓半连接(Semi Join),是一种特殊的连接类型。如果用"t1.x semi= t2.y"来表示表T1和表T2做半连接,则其含义是:只要在表T2中找到一条记录满足t1.x=t2.y,则马上停止搜索表T2,并直接返回表T1中满足条件t1.x=t2.y的记录,因此半连接的执行效率高于普通的内连接。
等价谓词重写包括:LIKE规则、BETWEEN-AND规则、IN转换OR规则、IN转换ANY规则、OR转换ANY规则、ALL/ANY转换集函数规则、NOT规则等,相关原理比较简单,有兴趣的同学可以自行查找相关查询重写规则。
WHERE、HAVING和ON条件由许多表达式组成,而这些表达式在某些时候彼此间存在一定的联系。利用等式和不等式性质,可将WHERE、HAVING和ON条件简化,但不同数据库的实现可能不完全相同。
将WHERE、HAVING和ON条件简化的方式通常包括如下几个:
去除表达式中冗余的括号:以减少语法分析时产生的AND和OR树的层次;
常量传递:对不同关系可使用条件分离后有效实施"选择下推",从而减小中间关系的规模。如:
col1=col2 AND col2=3
可化简为:col1=3 AND col2=3
操作符=、<、>、<=、>=、<>、LIKE中的任何一个,在col1<操作符>col2
条件中都会发生常量传递
消除死码。化简条件,将不必要的条件去除。如:
WHERE (0>1 AND s1=5)
, 0>1
使得AND
为恒假,去除即可。
不等式变换。化简条件,将不必要的重复条件去除。如:
a>10 AND b=6 AND a>2
可化简为:a>10 AND b=6
。
a>b AND b>2
可推导出a>2
,减少a、b比较元组数。代数优化改变查询语句中操作的次序和组合,但不涉及底层的存取路径。物理优化就是要选择高效合理的操作算法或存取路径,求得优化的查询计划,达到查询优化的目标。
查询优化器在物理优化阶段,主要解决的问题是:
选择的方法可以是:
启发式规则优化是定性的选择,比较粗糙,但是实现简单而且优化本身的代价较小,适合解释执行的系统。因为解释执行的系统,其优开销包含在查询总开销之中,在编译执行的系统中,一次编译优化,多次执行,查询优化和查询执行是分开的,因此,可以用精细复杂一些的基于代价的优化方法。
查询代价估算基于CPU代价和I/O代价,计算公式如下:
总代价 = I/O代价 + CPU代价
COST = P * a_page_cpu_time + W * T
其中:
P是计划运行时访问的页面数,a_page_cpu_time是每个页面读取的时间开销,其乘积反映了I/O开销。
T为访问的元组数,如果是索引扫描,还要考虑索引读取的开销,反映了数据读取到内存的CPU开销。
W为权重因子,表明I/O到CPU的相关性,又称选择率(selectivity),用于表示在关系R中,满足条件“A
选择率在代价估算模型中占有重要地位,其精确程度直接影响最优计划的选取。选择率计算常用方法如下:
由于其中I/O代价占比最大,通常以I/O代价为主来进行代价估算。
2. 索引扫描算法的代价估算公式
3.嵌套循环连接算法的代价估算公式
4.排序合并连接算法的代价估算公式
多表连接算法实现的是在查询路径生成的过程中,根据代价估算,从各种可能的候选路径中找出最优的路径。它需要解决两个问题:
多表间的连接顺序表示了查询计划树的基本形态。在1990年,Schneder等人在研究查询树模型时提出了左深树,右深树和紧密树3种形态,如图5-1所示。
即使是同一种树的生成方式,也有细节需要考虑。如图5-1-a中{A,B}和{B,A}两种连接方式开销可能不同。比如最终连接结果{A,B,C}则需要验证比较6种连接方式,找出最优的一种作为下次和其他表连接的依据。
多表连接搜索最优查询树,有很多算法,如启发式、分枝界定计划枚举、贪心、动态规划、爬山法、System R优化方法等。其中,常用算法如下。
动态规划
在数据库领域,动态规划算法主要解决多表连接的问题。它是自底向上进行的,即从叶子开始做第一层,然后开始对每层的关系做两两连接(如果满足内连接进行两两连接,不满足则不可对全部表进行两两连接),构造出上层,逐次递推到树根。以下介绍具体步骤:
初始状态:构造第一层关系,即叶子结点,每个叶子对应一个单表,为每一个待连接的关系计算最优路径(单表的最优路径就是单表的最佳访问方式,通过评估不同的单表的数据扫描方式代价,找出代价最小的作为每个单表的局部最优路径)
归纳:当第1层到第n-1层的关系已经生成,那么求解第n层的关系方法为:将第n-1层的关系与第一层中的每个关系连接,生成新的关系(对新关系的大小进行估算),放于第n层,且每一个新关系,均求解最优路径。每层路径的生成都是基于下层生成的最优路径,这满足最优化原理的要求。
还有的改进算法,在生成第n层的时候,除了通过第n-1层和第一层连接外,还可以通过第n-2层和第二层连接…。
PostgreSQL查询优化器求解多表连接时,采用了这种算法。
启发式算法
启发式算法是相对最优化算法提出的,是一个基于直观或者经验构造的算法,不能保证找到最好的查询计划。在数据库的查询优化器中,启发式一直贯穿于整个查询优化阶段,在逻辑查询优化阶段和物理查询优化阶段,都有一些启发式规则可用。PostgreSQL,MySQL,Oracle等数据库在实现查询优化器时,采用了启发式和其他方式相结合的方式。
物理查询优化阶段常用启发式规则如下:
贪心算法
贪心算法最后得到的是局部最优解,不一定全局最优,其实现步骤如下:
(1) 初始,算法选出的候选对象集合为空;
(2) 根据选择函数,从剩余候选对象中选出最有可能构成解的对象;
(3) 如果集合中加上该对象后不可行,那么该对象就被丢弃并不再考虑;
(4) 如果集合中加上该对象后可行,就加到集合里;
(5) 扩充集合,检查该集合是否构成解;
(6) 如果贪心算法正确工作,那么找到的第一个解通常都是最优的,可以终止算法;
(7) 继续执行第二步。
MySQL查询优化器求解多表连接时采用了这种算法。
System-R算法
对自底向上的动态规划算法进行了改进,主要思想是把子树的查询计划的最优查询计划和次优查询计划保留,用于上层的查询计划生成,以便使得查询计划总体上最优。
算法名称 | 特点与适用范围 | 缺点 |
---|---|---|
启发式算法 | 适用于任何范围,与其它算法结合,能有效提高整体效率 | 不知道得到的解是否最优 |
贪婪算法 | 非穷举类型的算法。适合解决较多关系的搜索 | 得到局部最优解 |
爬山法 | 适合查询中包含较多关系的搜索,基于贪婪算法 | 随机性强,得到局部最优解 |
遗传算法 | 非穷举类型的算法。适合解决较多关系的搜索 | 得到局部最优解 |
动态规划算法 | 穷举类型的算法。适合查询中包含较少关系的搜索,可得到全局最优解 | 搜索空间随关系个数增长呈指数增长 |
System R优化 | 基于自底向上的动态规划算法,为上层提供更多可能的备选路径,可得到全局最优解 | 搜索空间可能比动态规划算法更大一些 |
基于代价优化的一个缺点是优化本身的代价。因此,查询优化器使用启发式方法来减少优化代价。
1) 对于小关系,全表扫描;
2) 对于大关系:
(1) 若选择条件是主码,则可以选择主码索引,因为主码索引一般是被自动建立的;
(2) 若选择条件是非主属性的等职查询,并且选择列上有索引,如果选择比例较小(10%)可以使用索引扫描,否则全表扫描;
(3) 若选择条件是属性上的非等值查询或者范围查询,同上;
(4) 对于用and连接的合取选择条件,若有组合索引,优先用组合索引方法;如果某些属性上有一般索引,则用索引扫描,否则全表扫描;
(5) 对于用OR连接的析取选择条件,全表扫描。
1) 若两个表都已经按连接属性排序,则选用排序-合并算法;
2) 若一个表在连接属性上有索引,则使用索引连接方法;
3) 若其中一个表较小,则选用hash join;
4) 最后可以使用嵌套循环,小表作为外表。
还有嵌套子查询优化、物化视图等多种优化手段,这里不再展开。