openGauss源码解析(92)

openGauss源码解析:SQL引擎源解析(7)

6.3 查询优化

openGauss数据库的查询优化过程功能比较明晰,从源代码组织的角度来看,相关代码分布在不同的目录下,如表6-6所示。

表6-6 查询优化模块说明

模块

目录

说明

查询重写

src/gausskernel/optimizer/prep

主要包括子查询优化、谓词化简及正则化、谓词传递闭包等查询重写优化技术

统计信息

src/gausskernel/optimizer/commands/analyze.cpp

生成各种类型的统计信息,供选择率估算、行数估算、代价估算使用

代价估算

src/common/backend/utils/adt/selfuncs.cpp

src/gausskernel/optimizer/path/costsize.cpp

进行选择率估算、行数估算、代价估算

物理路径

src/gausskernel/optimizer/path

生成物理路径

动态规划

src/gausskernel/optimizer/plan

通过动态规划方法对物理路径进行搜索

遗传算法

src/gausskernel/optimizer/geqo

通过遗传算法对物理路径进行搜索

6.3.1 查询重写

SQL语言是丰富多样的,非常的灵活,不同的开发人员依据经验的不同,手写的SQL语句也是各式各样,另外还可以通过工具自动生成。SQL语言是一种描述性语言,数据库的使用者只是描述了想要的结果,而不关心数据的具体获取方式,输入数据库的SQL语言很难做到是以最优形式表示的,往往隐含了一些冗余信息,这些信息可以被挖掘用来生成更加高效的SQL语句。查询重写就是把用户输入的SQL语句转换为更高效的等价SQL,查询重写遵循两个基本原则。

(1) 等价性:原语句和重写后的语句,输出结果相同。
(2) 高效性:重写后的语句,比原语句在执行时间和资源使用上更高效。

查询重写主要是基于关系代数式的等价变换,关系代数的变换通常满足交换律、结合律、分配率、串接率等,如表6-7所示。

表6-7 关系代数等价变换

等价变换

内容

交换律

A × B == B × A

A ⨝B == B ⨝ A

A ⨝F B == B ⨝F A ……其中F是连接条件

Π p(σF (B)) == σF (Π p(B)) ……其中F∈p

结合律

(A × B) × C==A × (B × C)

(A ⨝ B) ⨝ C==A ⨝ (B ⨝ C)

(A ⨝F1 B) ⨝F2 C==A ⨝F1 (B ⨝F2 C) …… F1和F2是连接条件

分配律

σF(A × B) == σF(A) × B …… 其中F ∈ A

σF(A × B) == σF1(A) × σF2(B)

…… 其中F = F1 ∪ F2,F1∈A, F2 ∈B

σF(A × B) == σFX (σF1(A) × σF2(B))

…… 其中F = F1∪F2∪FX,F1∈A, F2 ∈B

Π p,q(A × B) == Π p(A) × Π q(B) …… 其中p∈A,q∈B

σF(A × B) == σF1(A) × σF2(B)

…… 其中F = F1 ∪ F2,F1∈A, F2 ∈B

σF(A × B) == σFx (σF1(A) × σF2(B))

…… 其中F = F1∪F2∪Fx,F1∈A, F2 ∈B

串接律

Π P=p1,p2,…pn(Π Q=q1,q2,…qn(A)) == Π P=p1,p2,…pn(A)……其中P ⊆ Q

σF1(σF2(A)) == σF1∧F2(A)

查询重写优化既可以基于关系代数的理论进行优化,例如谓词下推、子查询优化等,也可以基于启发式规则进行优化,例如Outer Join消除、表连接消除等。另外还有一些基于特定的优化规则和实际执行过程相关的优化,例如在并行扫描的基础上,可以考虑对Aggregation算子分阶段进行,通过将Aggregation划分成不同的阶段,可以提升执行的效率。

从另一个角度来看,查询重写是基于优化规则的等价变换,属于逻辑优化,也可以称为基于规则的优化,那么怎么衡量对一个SQL语句进行查询重写之后,它的性能一定是提升的呢?这时基于代价对查询重写进行评估就非常重要了,因此查询重写不只是基于经验的查询重写,还可以是基于代价的查询重写。

以谓词传递闭包和谓词下推为例,谓词的下推能够极大的降低上层算子的计算量,从而达到优化的效果,如果谓词条件有存在等值操作,那么还可以借助等值操作的特性来实现等价推理,从而获得新的选择条件。

例如,假设有两个表t1、t2分别包含[1,2,3,..100]共100行数据,那么查询语句SELECT t1.c1, t2.c1 FROM t1 JOIN t2 ON t1.c1=t2.c1 WHERE t1.c1=1则可以通过选择下推和等价推理进行优化,如图6-6所示。

图6-6 查询重写前后对比图

如图6-6-(1)所示,t1、t2表都需要全表扫描100行数据,然后再做join,生成100行数据的中间结果,最后再做选择操作,最终结果只有1行数据。如果利用等价推理,可以得到{t1.c1, t2.c1, 1}的是互相等价的,从而推导出新的t2.c1=1的选择条件,并把这个条件下推到t2上,从而得到图6-6-(4)重写之后的逻辑计划。可以看到,重写之后的逻辑计划,只需要从基表上面获取1条数据即可,join时内、外表的数据也只有1条,同时省去了在最终结果上的过滤条件,性能大幅提升。

在代码层面,查询重写的架构大致如图6-7所示。

图6-7 查询重写的架构

(1) 提升子查询:子查询出现在RangeTableEntry中,它存储的是一个子查询树,若子查询不被提升,则经过查询优化之后形成一个子执行计划,上层执行计划和子查询计划做嵌套循环得到最终结果。在该过程中,查询优化模块对这个子查询所能做的优化选择较少。若该子查询被提升,转换成与上层的join,由查询优化模块常数替换等式:由于常数引用速度更快,故将可以求值的变量求出来,并用求得的常数替换它,实现函数为preprocess_const_params。
(2) 子查询替换CTE:理论上CTE(common table expression,通用表达式)与子查询性能相同,但对子查询可以进行进一步的提升重写优化,故尝试用子查询替换CTE,实现函数为substitute_ctes_with_subqueries。
(3) multi count(distinct)替换为多条子查询:如果出现该类查询,则将多个count(distinct)查询分别替换为多条子查询,其中每条子查询中包含一个count(distinct)表达式,实现函数为convert_multi_count_distinct。
(4) 提升子链接:子链接出现在WHERE/ON等约束条件中,通常伴随着ANY/ALL/IN/EXISTS/SOME等谓词同时出现。虽然子链接从语句的逻辑层次上是清晰的,但是效率有高有低,比如相关子链接,其执行结果和父查询相关,即父查询的每一条元组都对应着子链接的重新求值,此情况下可通过提升子链接提高效率。在该部分数据库主要针对ANY和EXISTS两种类型的子链接尝试进行提升,提升为Semi Join或者Anti-SemiJoin,实现函数为pull_up_sublinks。
(5) 减少ORDER BY:由于在父查询中可能需要对数据库的记录进行重新排序,故减少子查询中的ORDER BY语句以进行链接可提高效率,实现函数为reduce_orderby。
(6) 删除NotNullTest:即删除相关的非NULL Test以提高效率,实现函数为removeNotNullTest。
(7) Lazy Agg重写:顾名思义,即“懒聚集”,目的在于减少聚集次数,实现函数为lazyagg_main。
(8) 对连接操作的优化做了很多工作,可能获得更好的执行计划,实现函数为pull_up_subqueries。
(9) UNION ALL优化:对顶层的UNION ALL进行处理,目的是将UNION ALL这种集合操作的形式转换为AppendRelInfo的形式,实现函数为flatten_simple_union_all。
(10) 展开继承表:如果在查询语句执行的过程中使用了继承表,那么继承表是以父表的形式存在的,需要将父表展开成为多个继承表,实现函数为expand_inherited_tables。,实现函数为expand_inherited_tables。
(11)预处理表达式:该模块是对查询树中的表达式进行规范整理的过程,包括对链接产生的别名Var进行替换、对常量表达式求值、对约束条件进行拉平、为子链接生成执行计划等,实现函数为preprocess_expression。
(12) 处理HAVING子句:在Having子句中,有些约束条件是可以转变为过滤条件的(对应WHERE),这里对Having子句中的约束条件进行拆分,以提高效率。
(13) 外连接消除:目的在于将外连接转换为内连接,以简化查询优化过程,实现函数为reduce_outer_join函数。
(14) 全连接full join重写:对全连接函数进行重写,以完善其功能。比如对于语句SELECT * FROM t1 FULL JOIN t2 ON TRUE可以将其转换为: SELECT * FROM t1 LEFT JOIN t2 ON TRUE UNION ALL (SELECT * FROM t1 RIGHT ANTI FULL JOIN t2 ON TRUE),实现函数为reduce_inequality_fulljoins。

下面以子链接提升为例,介绍openGauss中一种最重要的子查询优化。所谓子链接(SubLink)是子查询的一种特殊情况,由于子链接出现在WHERE/ON等约束条件中,因此经常伴随ANY/EXISTS/ALL/IN/SOME等谓词出现,openGauss数据库为不同的谓词设置了不同的SUBLINK类型。代码如下:

Typedef enum SubLinkType {

EXISTS_SUBLINK,

ALL_SUBLINK,

ANY_SUBLINK,

ROWCOMPARE_SUBLINK,

EXPR_SUBLINK,

ARRAY_SUBLINK,

CTE_SUBLINK

} SubLinkType;

openGauss数据库为子链接定义了单独的结构体——SubLink结构体,其中主要描述了子链接的类型、子链接的操作符等信息。代码如下:

Typedef struct SubLink {

Expr xpr;

SubLinkType subLinkType;

Node* testexpr;

List* operName;

Node* subselect;

Int location;

} SubLink;

子链接提升相关接口函数如图6-8所示。

图6-8 子链接相关接口函数

子链接提升的主要过程是在pull_up_sublinks函数中实现,pull_up_sublinks函数又调用pull_up_sublinks_jointree_recurse递归处理Query->jointree中的节点,函数输入参数如表6-8所示。

表6-8 函数输入参数说明

参数名

参数类型

说明

root

PlannerInfo*

输入参数,查询优化模块的上下文信息

jnode

Node*

输入参数,需要递归处理的节点,可能是RangeTblRef、FromExpr或JoinExpr

relids

Relids*

输出参数,jnode参数中涉及的表的集合

返回值

Node*

经过子链接提升处理之后的node节点

jnode分为三种类型:RangeTblRef、FromExpr、JoinExpr。针对这三种类型pull_up_sublinks_jointree_recurse函数分别进行了处理。

1) RangeTblRef

RangeTblRef是Query->jointree的叶子节点,所以是该函数递归结束的条件,程序走到该分支,一般有两种情况。

(1) 当前语句是单表查询而且不存在连接操作,这种情况递归处理直到结束后,再去查看子链接是否满足其他提升条件。
(2) 查询语句存在连接关系,在对From->fromlist、JoinExpr->larg或者JoinExpr->rarg递归处理的过程中,当遍历到了RangeTblRef叶子节点时,需要把RangeTblRef节点的relids(表的集合)返回给上一层。主要用于判断该子链接是否能提升。
2) FromExpr
(1) 递归遍历From->fromlist中的节点,之后对每个节点递归调用pull_up_sublinks_jointree_recurse函数,直到处理到叶子节点RangeTblRef才结束。
(2) 调用pull_up_sublinks_qual_recurse函数处理From->qual,对其中可能出现的ANY_SUBLINK或EXISTS_SUBLINK进行处理。
3) JoinExpr
(1) 调用pull_up_sublinks_jointree_recurse函数递归处理JoinExpr->larg和JoinExpr->rarg,直到处理到叶子节点RangeTblRef才结束。另外还需要根据连接操作的类型区分子链接是否能够被提升。
(2) 调用pull_up_sublinks_qual_recurse函数处理JoinExpr->quals,对其中可能出现的ANY_SUBLINK或EXISTS_SUBLINK做处理。如果连接类型不同,pull_up_sublinks_qual_recurse函数的available_rels1参数的输入值是不同的。

pull_up_sublinks_qual_recurse函数除了对ANY_SUBLINK和EXISTS_SUBLINK做处理,还对OR子句和EXPR类型子链接做了查询重写优化。其中Expr类型的子链接提升代码逻辑如下。

(1) 通过safe_convert_EXPR函数判断sublink是否可以提升。代码如下:

//判断当前SQL语句是否满足sublink提升条件

if (subQuery->cteList ||

subQuery->hasWindowFuncs ||

subQuery->hasModifyingCTE ||

subQuery->havingQual ||

subQuery->groupingSets ||

subQuery->groupClause ||

subQuery->limitOffset ||

subQuery->rowMarks ||

subQuery->distinctClause ||

subQuery->windowClause) {

ereport(DEBUG2,

(errmodule(MOD_OPT_REWRITE),

(errmsg("[Expr sublink pull up failure reason]: Subquery includes cte, windowFun, havingQual, group, "

"limitoffset, distinct or rowMark."))));

return false;

}

(2) 通过push_down_qual函数提取子链接中相关条件。代码如下:

Static Node* push_down_qual(PlannerInfo* root, Node* all_quals, List* pullUpEqualExpr)

{

If (all_quals== NULL) {

Return NULL;

}

List* pullUpExprList = (List*)copyObject(pullUpEqualExpr);

Node* all_quals_list = (Node*)copyObject(all_quals);

set_varno_attno(root->parse, (Node*)pullUpExprList, true);

set_varno_attno(root->parse, (Node*)all_quals_list, false);

Relids varnos = pull_varnos((Node*)pullUpExprList, 1);

push_qual_context qual_list;

SubLink* any_sublink = NULL;

Node* push_quals = NULL;

Int attnum = 0;

While ((attnum = bms_first_member(varnos)) >= 0) {

RangeTblEntry* r_table = (RangeTblEntry*)rt_fetch(attnum, root->parse->rtable);

//这张表必须是基表,否则不能处理

If (r_table->rtekind == RTE_RELATION) {

qual_list.varno = attnum;

qual_list.qual_list = NIL;

//获得包含特殊varno的条件

get_varnode_qual(all_quals_list, &qual_list);

If (qual_list.qual_list != NIL && !contain_volatile_functions((Node*)qual_list.qual_list)) {

any_sublink = build_any_sublink(root, qual_list.qual_list, attnum,pullUpExprList);

push_quals = make_and_qual(push_quals, (Node*)any_sublink);

}

list_free_ext(qual_list.qual_list);

}

}

list_free_deep(pullUpExprList);

pfree_ext(all_quals_list);

return push_quals;

}

(3) 通过transform_equal_expr函数构造需要提升的SubQuery(增加GROUP BY子句,删除相关条件)。代码如下:

//为SubQuery增加GROUP BY和windowClasues

if (isLimit) {

append_target_and_windowClause(root,subQuery,(Node*)copyObject(node), false);

} else {

append_target_and_group(root, subQuery, (Node*)copyObject(node));

}

//删除相关条件

subQuery->jointree = (FromExpr*)replace_node_clause((Node*)subQuery->jointree,

(Node*)pullUpEqualExpr,

(Node*)constList,

RNC_RECURSE_AGGREF | RNC_COPY_NON_LEAF_NODES);

(4) 构造需要提升的条件。代码如下:

//构造需要提升的条件

joinQual = make_and_qual((Node*)joinQual, (Node*)pullUpExpr);

Return joinQual;

(5) 生成join表达式。代码如下:

//生成join表达式

if (IsA(*currJoinLink, JoinExpr)) {

((JoinExpr*)*currJoinLink)->quals = replace_node_clause(((JoinExpr*)*currJoinLink)->quals,

tmpExprQual,

makeBoolConst(true, false),

RNC_RECURSE_AGGREF | RNC_COPY_NON_LEAF_NODES);

} else if (IsA(*currJoinLink, FromExpr)) {

((FromExpr*)*currJoinLink)->quals = replace_node_clause(((FromExpr*)*currJoinLink)->quals,

tmpExprQual,

makeBoolConst(true, false),

RNC_RECURSE_AGGREF | RNC_COPY_NON_LEAF_NODES);

}

rtr = (RangeTblRef *) makeNode(RangeTblRef);

rtr->rtindex = list_length(root->parse->rtable);

// 构造左连接的JoinExpr

JoinExpr *result = NULL;

result = (JoinExpr *) makeNode(JoinExpr);

result->jointype = JOIN_LEFT;

result->quals = joinQual;

result->larg = *currJoinLink;

result->rarg = (Node *) rtr;

// 在rangetableentry中添加JoinExpr。在后续处理中,左外连接可转换为内连接

rte = addRangeTableEntryForJoin(NULL,

NIL,

result->jointype,

NIL,

result->alias,

true);

root->parse->rtable = lappend(root->parse->rtable, r

posted @ 2024-04-30 10:11  openGauss-bot  阅读(22)  评论(0)    收藏  举报