openGauss源码解析(4)
openGauss源码解析:openGauss简介(4)
1.4.2 SQL引擎
数据库的SQL引擎是数据库重要的子系统之一,它对上负责承接应用程序发送过来的SQL语句,对下则负责指挥执行器运行执行计划。其中优化器作为SQL引擎中最重要、最复杂的模块,被称为数据库的“大脑”,优化器产生的执行计划的优劣直接决定数据库的性能。
本节从SQL语句发送到数据库服务器开始,对SQL引擎的各个模块进行全面的介绍与源码解析,以实现对SQL语句执行的逻辑与源码更深入的理解。其响应流程如图1-2所示。
图1-2 openGauss数据库SQL查询响应流程
1. 查询解析——parser
SQL解析对输入的SQL语句进行词法分析、语法分析、语义分析,获得查询解析树或者逻辑计划。SQL查询语句解析的解析器(parser)阶段包括如下。
(1) 词法分析:从查询语句中识别出系统支持的关键字、标识符、操作符、终结符等,每个词确定自己固有的词性。
(2) 语法分析:根据SQL语言的标准定义语法规则,使用词法分析中产生的词去匹配语法规则,如果一个SQL语句能够匹配一个语法规则,则生成对应的语法树(abstract synatax tree,AST)。
(3) 语义分析:对语法树进行检查与分析,检查语法树中对应的表、列、函数、表达式是否有对应的元数据(指数据库中定义有关数据特征的数据,用来检索数据库信息)描述,基于分析结果对语法树进行扩充,输出查询树(Query)。主要检查的内容包括:
➀ 检查关系的使用:FROM子句中出现的关系必须是该查询对应模式中的关系或视图。
➁ 检查与解析属性的使用:在SELECT语句中或者WHERE子句中出现的各个属性必须是FROM子句中某个关系或视图的属性。
➂ 检查数据类型:所有属性的数据类型必须是匹配的。
词法和语法分析代码基于gram.y和scan.l中定义的规则,使用UNIX工具bison和flex构建产生。其中,词法分析器在文件scan.l中定义,它负责识别标识符、SQL关键字等。对于找到的每个关键字或标识符,都会生成一个标记并将其传递给解析器。语法解析器在文件gram.y中定义,由一组语法规则和每当触发规则时执行的动作组成,基于这些动作代码架构并输出语法树。在解析过程中,如果语法正确,则进入语义分析阶段并建立查询树返回,否则将返回错误,终止解析过程。
解析器在词法和语法分析阶段仅使用有关SQL语法结构的固定规则来创建语法树。它不会在系统目录中进行任何查找,因此无法理解所请求操作的详细语义。
语法解析完成后,语义分析过程将解析器返回的语法树作为输入,并进行语义分析以了解查询所引用的表、函数和运算符。用来表示此信息的数据结构称为查询树。解析器解析过程分为原始解析与语义分析,分开的原因是,系统目录查找只能在事务内完成,并且不希望在收到查询字符串后立即启动事务。原始解析阶段足以识别事务控制命令(BEGIN,ROLLBACK等),然后可以正确执行这些命令而无须任何进一步分析。一旦知道正在处理的实际查询(例如SELECT或UPDATE),就可以开始事务,这时才调用语义分析过程。
1) parser源码组织
parser源码目录为:/src/common/backend/parser。parser源码文件如表1-3所示。
表1-3 parser源码文件
模块 | 源码文件 | 功能 |
---|---|---|
parser | parser.cpp | 解析主程序 |
scan.l | 词法分析,分解查询成token(令牌) | |
scansup.cpp | 处理查询语句转义符 | |
kwlookup.cpp | 将关键词转换为具体的token | |
keywords.cpp | 标准关键词列表 | |
analyze.cpp | 语义分析 | |
gram.y | 语法分析,解析查询token并产生原始解析树 | |
parse_agg.cpp | 处理聚集操作,比如SUM(col1)、AVG(col2) | |
parse_clause.cpp | 处理子句,比如WHERE、ORDER BY | |
parse_compatibility.cpp | 处理数据库兼容语法和特性支持 | |
parse_coerce.cpp | 处理表达式数据类型强制转换 | |
parse_collate.cpp | 对完成表达式添加校对信息 | |
parse_cte.cpp | 处理公共表格表达式(WITH 子句) | |
parse_expr.cpp | 处理表达式,比如col、col+3、x=3 | |
parse_func.cpp | 处理函数,table.column和列标识符 | |
parse_node.cpp | 对各种结构创建解析节点 | |
parse_oper.cpp | 处理表达式中的操作符 | |
parse_param.cpp | 处理参数 | |
parse_relation.cpp | 支持表和列的关系处理程序 | |
parse_target.cpp | 处理查询解析的结果列表 | |
parse_type.cpp | 处理数据类型 | |
parse_utilcmd.cpp | 处理实用命令的解析分析 |
2) parser主流程
parser主流程代码如下:
/* parser.cpp */
...
/* 原始解析器,输入查询字符串,做词法和语法分析,返回原始语法解析树列表*/
List* raw_parser(const char* str, List** query_string_locationlist)
{
...
/* 初始化 flex scanner */
yyscanner = scanner_init(str, &yyextra.core_yy_extra, ScanKeywords, NumScanKeywords);
...
/* 初始化 bison parser */
parser_init(&yyextra);
/* 解析! */
yyresult = base_yyparse(yyscanner);
/* 清理释放内存*/
scanner_finish(yyscanner);
...
return yyextra.parsetree;
}
/* analyze.cpp */
...
/* 分析原始语法解析树,做语义分析并输出查询树 */
Query* parse_analyze(
Node* parseTree, const char* sourceText, Oid* paramTypes, int numParams, bool isFirstNode, bool isCreateView)
{
/* 初始化解析状态和查询树 */
ParseState* pstate = make_parsestate(NULL);
Query* query = NULL;
...
/* 将解析树转换为查询树 */
query = transformTopLevelStmt(pstate, parseTree, isFirstNode, isCreateView);
...
/* 释放解析状态 */
free_parsestate(pstate);
...
return query;
}
2. SQL查询分流——traffic cop
traffic cop模块负责查询的分流,它负责区分简单和复杂的查询指令。事务控制命令(例如BEGIN和ROLLBACK)非常简单,因此不需要其他处理,而其他命令(例如SELECT和JOIN)则传递给重写器(参考第6章)。这种区分通过对简单命令执行最少的优化,并将更多的时间投入复杂的命令上,从而减少了处理时间。简单和复杂查询指令也对应如下两类解析。
(1) 软解析(简单,旧查询):当openGauss共享缓冲区中存在已提交SQL语句的已解析表示形式时,可以重复利用缓存内容执行语法和语义检查,避免查询优化相对昂贵的操作。
(2) 硬解析(复杂,新查询):如果无缓存语句可重用,或者第一次将SQL语句加载到openGauss共享缓冲区中,则会导致硬解析。同样,当一条语句在共享缓冲区中老化时,再次重新加载该语句时,还会导致另一次硬解析。因此,共享缓冲区的大小也会影响解析调用的数量。
可以查询gs_prepared_statements查看缓存了什么,以区分软/硬解析(它仅对当前会话可见)。此外,gs_buffercache模块提供了一种实时检查共享缓冲区高速缓存内容的方法,它甚至可以分辨出有多少数据块来自磁盘,有多少数据来自共享缓冲区。
1) traffic cop(tcop)源码组织
traffic cop(tcop)源码目录为:/src/common/backend/tcop。traffic cop(tcop)源码文件如表1-4所示。
表1-4 traffic cop(tcop)源码文件
模块 | 源码文件 | 功能 |
---|---|---|
tcop | auditfuncs.cpp | 记录数据库操作审计信息 |
autonomous.cpp | 创建可被用来执行SQL查询的自动会话 | |
dest.cpp | 与查询结果被发往的终点通信 | |
utility.cpp | 数据库通用指令控制函数 | |
fastpath.cpp | 在事务期间缓存操作函数和类型等信息 | |
postgres.cpp | 后端服务器主程序 | |
pquery.cpp | 查询处理指令 | |
stmt_retry.cpp | 执行SQL语句失败时,分析返回的错误码,决定是否进行重试 |
2) traffic cop主流程
traffic cop主流程代码如下:
...
/*原始解析器,输入查询字符串,做词法和语法分析,返回原始解析树列表*/
int PostgresMain(int argc, char* argv[], const char* dbname, const char* username)
{
...
/* 整体初始化*/
t_thrd.proc_cxt.PostInit->SetDatabaseAndUser(dbname, InvalidOid, username);
...
/* 事务的自动错误处理 */
if (sigsetjmp(local_sigjmp_buf, 1) != 0) { ... }
...
/* 错误语句的重新尝试阶段 */
if (IsStmtRetryEnabled() && StmtRetryController->IsQueryRetrying())
{ ... }
/* 无错误查询指令循环处理*/
for (;;) {
...
/* 按命令类型执行处理流程*/
switch(firstchar){
...
case: 'Q': ... /* 简单查询 */
case: 'P': ... /* 解析 */
case: 'E': ... /* 执行 */
}
...
}
...
}
3. 查询重写——rewriter
查询重写利用已有语句特征和关系代数运算来生成更高效的等价语句,在数据库优化器中扮演关键角色;尤其在复杂查询中,能够在性能上带来数量级的提升,可谓是“立竿见影”的“黑科技”。SQL语言是丰富多样的,非常灵活,不同的开发人员依据经验的不同,手写的SQL语句也是各式各样,另外还可以通过工具自动生成。同时SQL语言是一种描述性语言,数据库的使用者只是描述了想要的结果,而不关心数据的具体获取方式,输入数据库的SQL语言很难做到以最优形式表示,往往隐含了一些冗余信息,这些信息可以被挖掘生成更加高效的SQL语句。查询重写就是把用户输入的SQL语句转换为更高效的等价SQL。查询重写遵循两个基本原则。
(1) 等价性:原语句和重写后的语句,输出结果相同。
(2) 高效性:重写后的语句,比原语句在执行时间和资源使用上更高效。
介绍如下几个openGauss数据库关键的查询重写技术。
(1) 常量表达式化简:常量表达式,即用户输入SQL语句中包含运算结果为常量的表达式,分为算数表达式、逻辑运算表达式、函数表达式。查询重写可以对常量表达式预先计算以提升效率。例如“SELECT * FROM table WHERE a=1+1;”语句被重写为“SELECT * FROM table WHERE a=2”语句。
(2) 子查询提升:由于子查询表示的结构更清晰,符合人的阅读理解习惯,用户输入的SQL语句往往包含了大量的子查询,但是相关子查询往往需要使用嵌套循环的方法来实现,执行效率较低,因此将子查询优化为semi join的形式可以在优化规划时选择其他的执行方法,或能提高执行效率。例如“SELECT * FROM t1 WHERE t1.a in (SELECT t2.a FROM t2);”语句可重写为“SELECT * FROM t1 LEFT SEMI JOIN t2 ON t1.a=t2.a”语句。
(3) 谓词下推:谓词(Predicate),通常为SQL语句中的条件,例如“SELECT * FROM t1 WHERE t1.a=1;”语句中的“t1.a=1”即为谓词。等价类(equivalent-class)是指等价的属性、实体等对象的集合,例如“WHERE t1.a=t2.a”语句中,t1.a和t2.a互相等价,组成一个等价类{t1.a,t2.a}。利用等价类推理(又称作传递闭包),可以生成新的谓词条件,从而达到减小数据量和最大化利用索引的目的。举一个形象的例子来说明谓词下推的威力,假设有两个表t1、t2,它们分别包含[1,2,3,…,100]共100行数据,那么查询语句“SELECT * FROM t1 JOIN t2 ON t1.a=t2.a WHERE t1.a=1”的逻辑计划在经过查询重写前后的对比,如图1-3所示。
图1-3 查询重写前后对比
查询重写的主要工作在优化器中实现,源代码目录主要在/src/gausskernel/optimizer/prep,源码文件如表1-5所示。
表1-5 查询重写源代码文件
模块 | 源码文件 | 功能 |
prep | prepqual.cpp | 对谓词进行正则化 |
preptlist.cpp | 对投影进行重写 | |
prepunion.cpp | 处理查询中的集合操作 | |
preprownum.cpp | 对表达式中的rownum进行预处理 | |
prepjointree.cpp | 化简表达式、子查询 | |
prepnonjointree.cpp | Lazy Aggregation优化 |
除此之外,openGauss还提供了基于规则的rewrite接口,用户可以通过创建替换规则的方法对逻辑执行计划进行改写。例如视图展开功能,即通过rewrite模块中的规则进行替换,而视图展开的规则是在创建视图的过程中默认创建的。
1) rewriter源码组织
rewriter源码目录为:/src/gausskernel/optimizer/rewrite。源码文件如表1-6所示。
表1-6 rewriter源码文件
模块 | 源码文件 | 功能 |
rewrite | rewriteDefine.cpp | 定义重写规则 |
rewriteHandler.cpp | 重写主模块 | |
rewriteManip.cpp | 重写操作函数 | |
rewriteRemove.cpp | 重写规则移除函数 | |
rewriteRlsPolicy.cpp | 重写行粒度安全策略 | |
rewriteSupport.cpp | 重写辅助函数 |
2) rewriter主流程
rewriter主流程代码如下:
/* rewrite.cpp */
...
/* 查询重写主函数 */
List* QueryRewrite(Query* parsetree)
{
...
/* 应用所有non-SELECT规则获取改写查询列表 */
querylist = RewriteQuery(parsetree, NIL);
/* 对每个改写查询应用RIR规则 */
results = NIL;
foreach (l, querylist) {
Query* query = (Query*)lfirst(l);
query = fireRIRrules(query, NIL, false);
query->queryId = input_query_id;
results = lappend(results, query);
}
/* 从重写列表确定一个重写结果 */
origCmdType = parsetree->commandType;
foundOriginalQuery = false;
lastInstead = NULL;
foreach (l, results) {...}
...
return results;
}
4. 查询优化——optimizer
优化器(optimizer)的任务是创建最佳执行计划。一个给定的SQL查询以及一个查询树实际上可以以多种不同的方式执行,每种方式都会产生相同的结果集。如果在计算上可行,则查询优化器将检查这些可能的执行计划中的每一个,最终选择预期运行速度最快的执行计划。
在某些情况下,检查执行查询的每种可能方式都会占用大量时间和内存空间,特别是在执行涉及大量连接操作(join)的查询时。为了在合理的时间内确定合理的(不一定是最佳的)查询计划,当查询连接数超过阈值时,openGauss使用遗传查询优化器(genetic query optimizer),通过遗传算法来做执行计划的枚举。
优化器的查询计划(plan)搜索过程实际上与称为路径(path)的数据结构一起使用,该路径只是计划的简化表示,其中仅包含确定计划所需的关键信息。确定代价最低的路径后,将构建完整的计划树以传递给执行器。这足够详细地表示了所需的执行计划,供执行者运行。在本节的其余部分,将忽略路径和计划之间的区别。
1) 生成查询计划
首先,优化器会生成查询中使用的每个单独关系(表)的计划。候选计划由每个关系上的可用索引确定。对关系的顺序扫描是查询最基本的方法,因此总是会创建顺序扫描计划。假设在关系上定义了索引(例如B树索引),并且查询属性恰好与B树索引的键匹配,则使用B树索引创建另一个基于索引的查询计划。如果还存在其他索引并且查询中的限制恰好与索引的关键字匹配,则将考虑其他计划生成更多计划。
其次,如果查询需要连接两个或多个关系,则在找到所有可行的扫描单个关系的计划之后,将考虑连接关系的计划。连接关系有3种可用的连接策略:
(1) 嵌套循环连接:对在左关系中找到的每一行,都会扫描一次右关系。此策略易于实施,但非常耗时。(但是如果可以使用索引扫描来扫描右关系,这可能是一个不错的策略。可以将左关系的当前行中的值用作右索引扫描的键。)
(2)合并连接:在开始连接之前,对进行连接的每个关系的连接属性进行排序。然后,并行扫描进行连接的这两个关系,并组合匹配的行以形成连接行。这种连接更具吸引力,因为每个关系只需扫描一次。所需的排序可以通过明确的排序步骤来实现,也可以通过使用连接键上的索引以正确的顺序扫描关系来实现。
(3) 哈希连接:首先将正确的关系扫描并使用其连接属性作为哈希键加载到哈希表(hash table,也叫散列表)中。接下来,扫描左关系,并将找到的每一行的适当值用作哈希键,以在表中找到匹配的行。
当查询涉及两个以上的关系时,最终结果必须由构建连接树来确定。优化器检查不同的可能连接顺序以找到代价最低的连接顺序。
如果查询所使用的关系数目较少(少于启动启发式搜索阈值),那么将进行近乎穷举的搜索以找到最佳连接顺序。优化器优先考虑存在WHERE限定条件子句中的两个关系之间的连接(即存在诸如rel1.attr1 = rel2.attr2之类的限制),最后才考虑不具有join子句的连接对。优化器会对每个连接操作生成所有可能的执行计划,然后选择(估计)代价最低的那个。当连接表数目超过geqo_threshold时,所考虑的连接顺序由基因查询优化(Genetic Query Optimization,GEQO)启发式方法确定。
完成的计划树包括对基础关系的顺序或索引扫描,以及根据需要的嵌套循环、合并、哈希连接节点和其他辅助步骤,例如排序节点或聚合函数计算节点。这些计划节点类型中的大多数具有执行选择(丢弃不满足指定布尔条件的行)和投影(基于给定列值计算派生列集,即执行标量表达式的运算)的附加功能。优化器的职责之一是将WHERE子句中的选择条件附加起来,并将所需的输出表达式安排到计划树的最适当节点上。
2) 查询计划代价估计
openGauss的优化器是基于代价的优化器,对每条SQL语句生成的多个候选的计划,优化器会计算一个执行代价,最后选择代价最小的计划。
通过统计信息,代价估算系统就可以了解一个表有多少行数据、用了多少个数据页面、某个值出现的频率等,以确定约束条件过滤出的数据占总数据量的比例,即选择率。当一个约束条件确定了选择率之后,就可以确定每个计划路径所需要处理的行数,并根据行数可以推算出所需要处理的页面数。当计划路径处理页面的时候,会产生I/O代价。而当计划路径处理元组的时候(例如针对元组做表达式计算),会产生CPU代价。由于openGauss是单机数据库,无服务器节点间传输数据(元组)会产生通信的代价,因此一个计划的总体代价可以表示为:
总代价 = I/O代价 + CPU代价
openGauss把所有顺序扫描一个页面的代价定义为单位1,所有其他算子的代价都归一化到这个单位1上。比如把随机扫描一个页面的代价定义为4,即认为随机扫描一个页面所需代价是顺序扫描一个页面所需代价的4倍。又比如,把CPU处理一条元组的代价为0.01,即认为CPU处理一条元组所需代价为顺序扫描一个页面所需代价的1/100。
从另一个角度来看,openGauss将代价又分成了启动代价和执行代价,其中:
总代价 = 启动代价 + 执行代价
(1) 启动代价。
从SQL语句开始执行到此算子输出第一条元组为止,所需要的代价称为启动代价。有的算子启动代价很小,比如基表上的扫描算子,一旦开始读取数据页,就可以输出元组,因此启动代价为0。有的算子的启动代价相对较大,比如排序算子,它需要把所有下层算子的输出全部读取到,并且把这些元组排序之后,才能输出第一条元组,因此它的启动代价比较大。
(2) 执行代价。
从输出第一条算子开始,至查询结束,所需要的代价,称为执行代价。这个代价中又可以包含CPU代价、I/O代价,执行代价的大小与算子需要处理的数据量有关,也与每个算子完成的功能有关。处理的数据量越大、算子需要完成的任务越重,则执行代价越大。
(3) 总代价。
如图1-4所示示例,查询中包含两张表,分别命名为t1、t2。t1与t2进行join操作,并且对c1列做聚集。
图1-4 代价计算示例
示例中涉及的代价包括:
(1) 扫描t1的启动代价为0,总代价为13.13。13.13的意思是“总代价相当于顺序扫描13.13个页面”,t2表的扫描同理。
(2) 此计划的join方式为hash join,使用hash join时,必须先对一个子节点的所有数据建立哈希表,再依次使用另一个子节点的每一条元组尝试与hash join中的元组进行匹配。因此hash join的启动代价包括了建立哈希表的代价。
此计划中hash join的启动代价为13.29,对某个结果集建立哈希表时,必须拿到此结果集的所有数据,因此13.29比下层扫描算子的代价13.13大。
此计划中hash join的总代价为28.64。
(3) join完毕之后,需要做聚集运算,此计划中的聚集运算使用了HashAGG算子,此算子需要对join的结果集以c1列作为hash Key建立哈希表,因此它的启动代价又包含了一个建立哈希表的代价。聚集操作的启动代价为28.69,总代价为28.79。
3) optimizer源码组织
optimizer源码目录为:/src/gausskernel/optimizer。optimizer源码文件如表1-7所示。
表1-7 optimizer源码文件
模块 | 源码文件 | 功能 |
---|---|---|
plan | analyzejoins.cpp | 初始化查询后的连接简化 |
createplan.cpp | 创建查询计划 | |
dynsmp_single.cpp | SMP自适应接口函数 | |
planner.cpp | 查询优化外部接口 | |
planrecursive_single.cpp | with_recursive递归查询的处理函数 | |
planrewrite.cpp | 基于代价的查询重写 | |
setrefs.cpp | 完成的查询计划树的后处理(修复子计划变量引用) | |
initsplan.cpp | 目标列表、限定条件和连接信息初始化 | |
pgxcplan_single.cpp | 简单查询的旁路执行器 | |
planagg.cpp | 聚集查询的特殊计划 | |
planmain.cpp | 计划主函数:单个查询的计划 | |
streamplan_single.cpp | 流计划相关函数 | |
subselect.cpp | 子选择和参数的计划函数 | |
path | allpaths.cpp | 查找所有可能查询执行路径 |
clausesel.cpp | 子句选择性计算 | |
costsize.cpp | 计算关系和路径代价 | |
pathkeys.cpp | 匹配并建立路径键的公用函数 | |
pgxcpath_single.cpp | 查找关系和代价的所有可能远程查询路径 | |
streampath_single.cpp | 并行处理的路径生成 | |
tidpath.cpp | 确定扫描关系TID(tuple identifier,元组标识符)条件并创建对应TID路径 | |
equivclass.cpp | 管理等价类 | |
indxpath.cpp | 确定可使用索引并创建对应路径 | |
joinpath.cpp | 查找执行一组join操作的所有路径 | |
joinrels.cpp | 确定需要被连接的关系 | |
orindxpath.cpp | 查找匹配OR子句集的索引路径 |
4) optimizer主流程
optimizer主流程代码如下:
/* planmain.cpp */
...
/*
*优化器主函数
*生成基本查询的路径(最简化的查询计划)
*输入参数:
*root:描述需要计划的查询
*tlist: 查询生成的目标列表
*tuple_fraction: 被抽取的元组数量比例
*limit_tuples: 抽取元组数量的数量限制
*输出参数:
*cheapest_path: 查询整体上代价最低的路径
*sorted_path: 排好序的代价最低的数个路径
*num_groups: 估计组的数量(如果查询不使用group运算返回1)
*/
void query_planner(PlannerInfo* root, List* tlist, double tuple_fraction, double limit_tuples,query_pathkeys_callback qp_callback, void *qp_extra, Path** cheapest_path, Path** sorted_path, double* num_groups, List* rollup_groupclauses, List* rollup_lists){
...
/* 空连接树简单query 快速处理 */
if (parse->jointree->fromlist == NIL) {
...
return;
}
setup_simple_rel_arrays(root); /* 获取线性版的范围表,加速读取 */
/* 为基础关系建立RelOptInfo节点 */
add_base_rels_to_query(root, (Node*)parse->jointree);
check_scan_hint_validity(root);
/* 向目标列表添加条目,占位符信息生成,最后形成连接列表 */
build_base_rel_tlists(root, tlist);
find_placeholders_in_jointree(root);
joinlist = deconstruct_jointree(root);
reconsider_outer_join_clauses(root); /* 基于等价类重新考虑外连接*/
/* 对等价类生成额外的限制性子句 */
generate_base_implied_equalities(root);
generate_base_implied_qualities(root);
(*qp_callback) (root, qp_extra); /* 将完整合并的等价类集合转换为标准型 */
fix_placeholder_input_needed_levels(root); /* 检查占位符表达式 */
joinlist = remove_useless_joins(root, joinlist); /* 移除无用外连接 */
add_placeholders_to_base_rels(root); /* 将占位符添加到基础关系 */
/* 对每个参与查询表的大小进行估计,计算total_table_pages */
total_pages = 0;
for (rti = 1; rti < (unsigned int)root->simple_rel_array_size; rti++)
{...}
root->total_table_pages = total_pages;
/* 准备开始主查询计划 */
final_rel = make_one_rel(root, joinlist);
final_rel->consider_parallel = consider_parallel;
...
/* 如果有分组子句,估计结果组数量 */
if (parse->groupClause) {...} /* 如果有分组子句,估计结果组数量 */
else if (parse->hasAggs||root->hasHavingQual||parse->groupingSets)
{...} /* 非分组聚集查询读取所有元组 */
else if (parse->distinctClause) {...} /* 非分组非聚集独立子句估计结果行数 */
else {...} /* 平凡非分组非聚集查询,计算绝对的元组比例 */
/* 计算代价整体最小路径和预排序的代价最小路径 */
cheapestpath = get_cheapest_path(root, final_rel, num_groups, has_groupby);
...
*cheapest_path = cheapestpath;
*sorted_path = sortedpath;
}
5. 查询执行——executor
执行器(executor)采用优化器创建的计划,并对其进行递归处理以提取所需的行的集合。这本质上是一种需求驱动的流水线执行机制,即每次调用一个计划节点时,它都必须再传送一行,或者报告已完成传送所有行。
图1-5 执行计划树示例
如图1-5所示的执行计划树示例,顶部节点是merge join节点。在进行任何合并操作之前,必须获取两个元组(merge join节点的两个子计划各返回1个元组)。因此,执行器以递归方式调用自身以处理其子计划(如从左子树的子计划开始)。merge join由于要做归并操作,因此它要子计划按序返回元组,从图1-5可以看出,它的子计划是一个sort节点。sort的子节点可能是seq scan节点,代表对表的实际读取。执行seq scan节点会使执行程序从表中获取一行并将其返回到调用节点。sort节点将反复调用其子节点以获得所有要排序的行。当输入完毕时(如子节点返回NULL而不是新行),sort算子对获取的元组进行排序,它每次返回1个元组,即已排序的第1行。然后不断排序并向父节点传递剩余的排好序的元组。
merge join节点类似地需要获得其右侧子计划中的第1个元组,看是否可以合并。如果是,它将向其调用方返回1个连接行。在下一次调用时,或者如果它不能连接当前输入对,则立即前进到1个表或另1个表的下一行(取决于比较的结果),然后再次检查是否匹配。最终,1个或另1个子计划用尽,并且merge join节点返回NULL,以指示无法再形成更多的连接行。
复杂的查询可能涉及多个级别的计划节点,但是一般方法是相同的:每个节点都会在每次调用时计算并返回其下一个输出行。每个节点还负责执行优化器分配给它的任何选择或投影表达式。
执行器机制用于执行所有4种基本SQL查询类型:SELECT、INSERT、UPDATE和DELETE。
(1) 对于SELECT,顶级执行程序代码仅需要将查询计划树返回的每一行发送给客户端。
(2) 对于INSERT,每个返回的行都插入INSERT指定的目标表中。这是在称为ModifyTable的特殊顶层计划节点中完成的。(1个简单的“INSERT ... VALUES”命令创建了1个简单的计划树,该树由单个Result节点组成,该节点仅计算一个结果行,并传递给ModifyTable树节点实现插入)。
(3) 对于UPDATE,优化器对每个计算的更新行附着所更新的列值,以及原始目标行的TID(tuple identifier,元组ID或行ID);此数据被馈送到ModifyTable节点,并使用该信息来创建新的更新行并标记旧行已删除。
(4) 对于DELETE,计划实际返回的唯一列是TID,而ModifyTable节点仅使用TID访问每个目标行并将其标记为已删除。
执行器的主要处理控制流程如下。
(1) 创建查询描述。
(2) 查询初始化:创建执行器状态(查询执行上下文)、执行节点初始化(创建表达式与每个元组上下文、执行表达式初始化)。
(3) 查询执行:执行处理节点(递归调用查询上下文、执行表达式,然后释放内存,重复操作)。
(4) 查询完成;执行未完成的表格修改节点。
(5) 查询结束:递归释放资源、释放查询及其子节点上下文。
(6) 释放查询描述。
1) executor源码组织
executor源码目录为:/src/gausskernel/runtime/executor。executor源码文件如表1-8所示。
表1-8 executor源码文件
模块 | 源码文件 | 功能 |
---|---|---|
executor | execAmi.cpp | 各种执行器访问方法 |
execClusterResize.cpp | 集群大小调整 | |
execCurrent.cpp | 支持WHERE CURRENT OF | |
execGrouping.cpp | 支持分组、哈希和聚集操作 | |
execJunk.cpp | 伪列的支持 | |
execMain.cpp | 顶层执行器接口 | |
execMerge.cpp | 处理MERGE指令 | |
execParallel.cpp | 支持并行执行 | |
execProcnode.cpp | 分发函数按节点调用相关初始化等函数 | |
execQual.cpp | 评估资质和目标列表的表达式 | |
execScan.cpp | 通用的关系扫描 | |
execTuples.cpp | 元组相关的资源管理 | |
execUtils.cpp | 多种执行相关工具函数 | |
functions.cpp | 执行SQL语言函数 | |
instrument.cpp | 计划执行工具 | |
lightProxy.cpp | 轻量级执行代理 | |
node*.cpp | 处理*相关节点操作的函数 | |
opfusion.cpp | 旁路执行器:处理简单查询 | |
spi.cpp | 服务器编程接口 | |
tqueue.cpp | 并行后端之间的元组信息传输 | |
tstoreReceiver.cpp | 存储结果元组 |
2) executor主流程
executor主流程代码为:
/* execMain.cpp */
...
/* 执行器启动 */
void ExecutorStart(QueryDesc *queryDesc, int eflags)
{
gstrace_entry(GS_TRC_ID_ExecutorStart);
if (ExecutorStart_hook) {
(*ExecutorStart_hook)(queryDesc, eflags);
} else {
standard_ExecutorStart(queryDesc, eflags);
}
gstrace_exit(GS_TRC_ID_ExecutorStart);
}
/* 执行器运行 */
void ExecutorRun(QueryDesc *queryDesc, ScanDirection direction, long count)
{
...
/* SQL 自调优: 查询执行完毕时,基于运行时信息分析查询计划问题 */
if (u_sess->exec_cxt.need_track_resource && queryDesc != NULL && has_track_operator &&(IS_PGXC_COORDINATOR || IS_SINGLE_NODE)) {
List *issue_results = PlanAnalyzerOperator(queryDesc, queryDesc->planstate);
/* 如果查询问题找到,存在系统视图 gs_wlm_session_history */
if (issue_results != NIL) {
RecordQueryPlanIssues(issue_results);
}
}
/* 查询动态特征, 操作历史统计信息 */
if (can_operator_history_statistics) {
u_sess->instr_cxt.can_record_to_table = true;
ExplainNodeFinish(queryDesc->planstate, queryDesc->plannedstmt, GetCurrentTimestamp(), false);
...
}
}
/* 执行器完成 */
void ExecutorFinish(QueryDesc *queryDesc)
{
if (ExecutorFinish_hook) {
(*ExecutorFinish_hook)(queryDesc);
} else {
standard_ExecutorFinish(queryDesc);
}
}
/* 执行器结束 */
void ExecutorEnd(QueryDesc *queryDesc)
{
if (ExecutorEnd_hook) {
(*ExecutorEnd_hook)(queryDesc);
} else {
standard_ExecutorEnd(queryDesc);
}
}