本文从数据库系统通信管理、SQL引擎和存储引擎3个方面对openGauss的代码结构进行介绍。一、通信管理openGauss查询响应使用简单的“单一用户对应一个服务器线程”的客户端/服务器模型实现。由于无法提前知道需要建立多少个连接,因此必须使用主进程(gaussmaster)。主进程在指定的TCP/IP(transmission control protocol/internet protocol,传输控制协议/互联网协议)端口上侦听传入的连接,只要检测到连接请求,主进程就会生成一个新的服务器线程。服务器线程之间使用信号量和共享内存相互通信,以确保整个并发数据访问期间的数据完整性。客户端进程可以被理解为满足openGauss协议的任何程序。许多客户端都基于C语言库libpq进行通信,但是该协议有几种独立的实现,例如Java JDBC驱动程序。建立连接后,客户端进程可以将查询发送到后端服务器。查询使用纯文本传输,即在前端(客户端)中没有进行解析。服务器解析查询语句、创建执行计划、执行并通过在已建立连接上传输检索到的结果集,将其返回给客户端。openGauss数据库中处理客户端连接请求的模块叫作作postmaster。前端程序发送启动信息给postmaster,postmaster根据信息内容建立后端响应线程。postmaster也管理系统级的操作,比如调用启动和关闭程序。postmaster在启动时创建共享内存和信号量池,但它自身不管理内存、信号量和锁操作。当客户端发来一个请求信息,postmaster立刻启动一个新会话,新会话对请求进行验证,验证成功后为它匹配后端工作线程。这种模式架构上处理简单,但是高并发下由于线程过多,切换和轻量级锁区域的冲突过大导致性能急剧下降。因此openGauss通过线程资源池化复用的技术来解决该问题。线程池技术的整体设计思想是线程资源池化,并且在不同连接直接复用。1. postmaster源码组织postmaster源码目录为:/src/gausskernel/process/postmaster。postmaster源码文件如下表所示。表 postmaster源码文件模块源码文件功能postmasterpostmaster.cpp用户响应主程序aiocompleter.cpp完成预取(prefetch)和后端写(backWrite)I/O操作alarmchecker.cpp闹钟检查线程lwlockmonitor.cpp轻量锁的死锁检测pagewriter.cpp写页面pgarch.cpp日志存档pgaudit.cpp审计线程pgstat.cpp统计信息收集startup.cpp服务初始化和恢复syslogger.cpp捕捉并写所有错误日志autovacuum.cpp垃圾清理线程bgworker.cpp后台工作线程(服务共享内存)bgwriter.cpp后台写线程(写共享缓存)cbmwriter.cpp修改数据块跟踪记录线程remoteservice.cpp远程服务线程,用于双机损坏页修复时的远程服务checkpointer.cpp检查点处理fencedudf.cpp保护模式下运行用户定义函数gaussdb_version.cpp版本特性控制twophasecleaner.cpp清理两阶段事务线程walwriter.cpp预写式日志写入 2. postmaster主流程postmaster主流程代码如下:/* postmaster.cpp */
...
int PostmasterMain(int argc, char* argv[])
{
InitializePostmasterGUC(); /* 初始化postmaster模块配置参数*/
...
pgaudit_agent_init(); /* 初始化审计模块*/
...
for (i = 0; i < MAXLISTEN; i++) /* 建立输入socket监听*/
t_thrd.postmaster_cxt.ListenSocket[i] = PGINVALID_SOCKET;
...
/* 建立共享内存和信号池*/
reset_shared(g_instance.attr.attr_network.PostPortNumber);
...
/* 初始化postmaster信号管理*/
gs_signal_slots_init(GLOBAL_ALL_PROCS + EXTERN_SLOTS_NUM);
...
InitPostmasterDeathWatchHandle(); /* 初始化宕机监听*/
...
pgstat_init(); /* 初始化统计数据收集子系统*/
InitializeWorkloadManager(); /* 初始化工作负载管理器*/
...
InitUniqueSQL(); /* 初始化unique SQL资源*/
...
autovac_init(); /* 初始化垃圾清理线程子系统*/
...
status = ServerLoop(); /* 启动postmaster主业务循环*/
...
}二、SQL引擎数据库的SQL引擎是数据库重要的子系统之一,它对上负责承接应用程序发送过来的SQL语句,对下则负责指挥执行器运行执行计划。其中优化器作为SQL引擎中最重要、最复杂的模块,被称为数据库的“大脑”,优化器产生的执行计划的优劣直接决定数据库的性能。本节从SQL语句发送到数据库服务器开始,对SQL引擎的各个模块进行全面的介绍与源码解析,以实现对SQL语句执行的逻辑与源码更深入的理解。其响应流程如图所示。openGauss数据库SQL查询响应流程1. 查询解析——parserSQL解析对输入的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源码文件如下表所示。表 parser源码文件模块源码文件功能parserparser.cpp解析主程序scan.l词法分析,分解查询成token(令牌)scansup.cpp处理查询语句转义符kwlookup.cpp将关键词转换为具体的tokenkeywords.cpp标准关键词列表analyze.cpp语义分析gram.y语法分析,解析查询token并产生原始解析树parse_agg.cpp处理聚集操作,比如SUM(col1)、AVG(col2)parse_clause.cpp处理子句,比如WHERE、ORDER BYparse_compatibility.cpp处理数据库兼容语法和特性支持parse_coerce.cpp处理表达式数据类型强制转换parse_collate.cpp对完成表达式添加校对信息parse_cte.cpp处理公共表格表达式(WITH 子句)parse_expr.cpp处理表达式,比如col、col+3、x=3parse_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);2. SQL查询分流——traffic coptraffic 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)源码文件如下表所示。表 traffic cop(tcop)源码文件模块源码文件功能tcopauditfuncs.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”的逻辑计划在经过查询重写前后的对比,如图所示。查询重写前后对比查询重写的主要工作在优化器中实现,源代码目录主要在/src/gausskernel/optimizer/prep,源码文件如表所示。表 查询重写源代码文件模块源码文件功能prepprepqual.cpp对谓词进行正则化preptlist.cpp对投影进行重写prepunion.cpp处理查询中的集合操作preprownum.cpp对表达式中的rownum进行预处理prepjointree.cpp化简表达式、子查询prepnonjointree.cppLazy Aggregation优化 除此之外,openGauss还提供了基于规则的rewrite接口,用户可以通过创建替换规则的方法对逻辑执行计划进行改写。例如视图展开功能,即通过rewrite模块中的规则进行替换,而视图展开的规则是在创建视图的过程中默认创建的。1) rewriter源码组织rewriter源码目录为:/src/gausskernel/optimizer/rewrite。源码文件如表所示。表 rewriter源码文件模块源码文件功能rewriterewriteDefine.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) 总代价。如图所示示例,查询中包含两张表,分别命名为t1、t2。t1与t2进行join操作,并且对c1列做聚集。代价计算示例示例中涉及的代价包括:(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源码文件如表所示。表 optimizer源码文件模块源码文件功能plananalyzejoins.cpp初始化查询后的连接简化createplan.cpp创建查询计划dynsmp_single.cppSMP自适应接口函数planner.cpp查询优化外部接口planrecursive_single.cppwith_recursive递归查询的处理函数planrewrite.cpp基于代价的查询重写setrefs.cpp完成的查询计划树的后处理(修复子计划变量引用)initsplan.cpp目标列表、限定条件和连接信息初始化pgxcplan_single.cpp简单查询的旁路执行器planagg.cpp聚集查询的特殊计划planmain.cpp计划主函数:单个查询的计划streamplan_single.cpp流计划相关函数subselect.cpp子选择和参数的计划函数pathallpaths.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)采用优化器创建的计划,并对其进行递归处理以提取所需的行的集合。这本质上是一种需求驱动的流水线执行机制,即每次调用一个计划节点时,它都必须再传送一行,或者报告已完成传送所有行。执行计划树示例如上图所示的执行计划树示例,顶部节点是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源码文件如下表所示。表 executor源码文件模块源码文件功能executorexecAmi.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.cppopfusion_util.cppopfusion_scan.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);
}
}三、存储引擎openGauss存储引擎支持多个存储引擎来满足不同场景的业务诉求,目前支持行存储引擎、列存储引擎和内存引擎。早期计算机程序通过文件系统管理数据,到了20世纪60年代这种方式就开始不能满足数据管理要求了,用户逐渐对数据并发写入的完整性、高效检索提出更高的要求。由于机械磁盘的随机读写性能问题,从20世纪80年代开始,大多数数据库一直在围绕着减少随机读写磁盘进行设计。主要思路是把对数据页面的随机写盘转换为对WAL(write ahead log,预写式日志)的顺序写盘,WAL持久化完成,事务就算提交成功,数据页面异步刷盘。但是随着内存容量变大、保电内存、非易失性内存的发展,以及SSD技术逐渐地成熟,I/O性能极大提高,经历了几十年发展的存储引擎需要调整架构来发挥SSD的性能和充分利用大内存计算的优势。随着互联网、移动互联网的发展,数据量剧增,业务场景多样化,一套固定不变的存储引擎不可能满足所有应用场景的诉求。因此现在的DBMS(database management system,数据库管理系统)需要设计支持多种存储引擎,根据业务场景来选择合适的存储模型。1. 数据库存储引擎要解决的问题(1) 存储的数据必须要保证ACID:原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)。(2) 高并发读写,高性能。(3) 数据高效存储和检索能力。2. openGauss存储引擎概述openGauss整个系统设计支持多个存储引擎来满足不同场景的业务诉求。当前openGauss存储引擎有以下3种:(1) 行存储引擎。主要面向OLTP(online transaction processing,在线交易处理)场景设计,例如订货发货,银行交易系统。(2) 列存储引擎。主要面向OLAP场景设计,例如数据统计报表分析。(3) 内存引擎。主要面向极致性能场景设计,例如银行风控场景。创建表的时候可以指定行存储引擎表、列存储引擎表、内存引擎表,支持一个事务里包含对3种引擎表的DML(Data Manipulation Language,数据操作语言)操作,可以保证事务ACID。1) storage源码组织storage源码目录为:/src/gausskernel/storage。storage源码文件如表所示。表 storage源码文件模块源码文件功能storageaccess基础行存储引擎方法cbtreehashheapindex...buffer缓冲区freespace空闲空间管理ipc进程内交互large_object大对象处理remote远程读replication复制备份smgr存储管理cmgr公共缓存方法cstore列存储引擎dfs分布式文件系统file文件类lmgr锁管理mot内存引擎page数据页 2) storage主流程storage主流程代码如下:/* smgr/smgr.cpp, 存储管理 */
...
/* 存储管理函数列表,包含磁盘初始化、开关、同步等操作函数 */
static const f_smgr g_smgrsw[] = {
/* 磁盘*/
{mdinit,
NULL,
mdclose,
mdcreate,
mdexists,
mdunlink,
mdextend,
mdprefetch,
mdread,
mdwrite,
mdwriteback,
mdnblocks,
mdtruncate,
mdimmedsync,
mdpreckpt,
mdsync,
mdpostckpt,
mdasyncread,
mdasyncwrite}};
/*
* 存储管理初始化
* 当服务器后端启动时调用
*/
void smgrinit(void)
{
int i;
/* 初始化所有存储相关管理器 */
for (i = 0; i < SMGRSW_LENGTH; i++) {
if (g_smgrsw[i].smgr_init) {
(*(g_smgrsw[i].smgr_init))();
}
}
/* 登记存储管理终止程序 */
if (!IS_THREAD_POOL_SESSION) {
on_proc_exit(smgrshutdown, 0);
}
}
/*
* 当后端服务关闭时,执行存储管理关闭代码
*/
static void smgrshutdown(int code, Datum arg)
{
int i;
/* 关闭所有存储关联服务 */
for (i = 0; i < SMGRSW_LENGTH; i++) {
if (g_smgrsw[i].smgr_shutdown) {
(*(g_smgrsw[i].smgr_shutdown))();
}
}
} 3. 行存储引擎openGauss的行存储引擎设计上支持MVCC(multi-version concurrency control,多版本并发控制),采用集中式垃圾版本回收机制,可以提供OLTP业务系统的高并发读写要求。架构如图所示。行存储架构行存储引擎的关键技术有:(1) 基于CSN(commit sequence number,待提交事务的序列号,它是一个64位递增无符号数)的MVCC并发控制机制,进行集中式垃圾数据清理。(2) 并行刷新日志,并行恢复。传统数据库一般都采用串行刷日志的设计,因为日志有顺序依赖关系,例如一个由事务产生的redo/undo log是有前后依赖关系的。openGauss的日志系统采用多个logwriter线程并行写的机制,充分发挥SSD的多通道I/O能力。(3) 基于大内存设计的缓冲管理器。行存储缓冲区主流程代码如下:/* buffer/bufmgr.cpp, 基础行存储管理 */
...
/* 查找或创建一个缓冲区 */
Buffer ReadBufferExtended(
Relation reln, ForkNumber fork_num, BlockNumber block_num, ReadBufferMode mode, BufferAccessStrategy strategy)
{
bool hit = false;
Buffer buf;
if (block_num == P_NEW) {
STORAGE_SPACE_OPERATION(reln, BLCKSZ);
}
/* 以smgr(存储管理器)级别打开一个缓冲区 */
RelationOpenSmgr(reln);
/* 拒绝读取非局部临时关系的请求,因为可能会获得监控不到的错误数据 */
if (RELATION_IS_OTHER_TEMP(reln) && fork_num <= INIT_FORKNUM)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot access temporary tables of other sessions")));
/* 读取缓冲区,更新统计信息数量反馈缓存命中与否情况 */
pgstat_count_buffer_read(reln);
pgstatCountBlocksFetched4SessionLevel();
buf = ReadBuffer_common(reln->rd_smgr, reln->rd_rel->relpersistence, fork_num, block_num, mode, strategy, &hit);
if (hit) {
pgstat_count_buffer_hit(reln);
}
return buf;
}
/* 释放一个缓冲区 */
void ReleaseBuffer(Buffer buffer)
{
BufferDesc* buf_desc = NULL;
PrivateRefCountEntry* ref = NULL;
/* 错误释放处理 */
if (!BufferIsValid(buffer)) {
ereport(ERROR, (errcode(ERRCODE_INVALID_BUFFER), (errmsg("bad buffer ID: %d", buffer))));
}
ResourceOwnerForgetBuffer(t_thrd.utils_cxt.CurrentResourceOwner, buffer);
if (BufferIsLocal(buffer)) {
Assert(u_sess->storage_cxt.LocalRefCount[-buffer - 1] > 0);
u_sess->storage_cxt.LocalRefCount[-buffer - 1]--;
return;
}
/* 释放当前缓冲区 */
buf_desc = GetBufferDescriptor(buffer - 1);
PrivateRefCountEntry *free_entry = NULL;
ref = GetPrivateRefCountEntryFast(buffer, free_entry);
if (ref == NULL) {
ref = GetPrivateRefCountEntrySlow(buffer, false, false, free_entry);}
Assert(ref != NULL);
Assert(ref->refcount > 0);
if (ref->refcount > 1) {
ref->refcount--;
} else {
UnpinBuffer(buf_desc, false);
}
}
/* 标记写脏缓冲区 */
void MarkBufferDirty(Buffer buffer)
{
BufferDesc* buf_desc = NULL;
uint32 buf_state;
uint32 old_buf_state;
if (!BufferIsValid(buffer)) {
ereport(ERROR, (errcode(ERRCODE_INVALID_BUFFER), (errmsg("bad buffer ID: %d", buffer))));}
if (BufferIsLocal(buffer)) {
MarkLocalBufferDirty(buffer);
return;
}
buf_desc = GetBufferDescriptor(buffer - 1);
Assert(BufferIsPinned(buffer));
Assert(LWLockHeldByMe(buf_desc->content_lock));
old_buf_state = LockBufHdr(buf_desc);
buf_state = old_buf_state | (BM_DIRTY | BM_JUST_DIRTIED);
/* 将未入队的脏页入队 */
if (g_instance.attr.attr_storage.enableIncrementalCheckpoint) {
for (;;) {
buf_state = old_buf_state | (BM_DIRTY | BM_JUST_DIRTIED);
if (!XLogRecPtrIsInvalid(pg_atomic_read_u64(&buf_desc->rec_lsn))) {
break;
}
if (!is_dirty_page_queue_full(buf_desc) && push_pending_flush_queue(buffer)) {
break;
}
UnlockBufHdr(buf_desc, old_buf_state);
pg_usleep(TEN_MICROSECOND);
old_buf_state = LockBufHdr(buf_desc);
}
}
UnlockBufHdr(buf_desc, buf_state);
/* 如果缓冲区不是“脏”状态,则更新相关计数 */
if (!(old_buf_state & BM_DIRTY)) {
t_thrd.vacuum_cxt.VacuumPageDirty++;
u_sess->instr_cxt.pg_buffer_usage->shared_blks_dirtied++;
pgstatCountSharedBlocksDirtied4SessionLevel();
if (t_thrd.vacuum_cxt.VacuumCostActive) {
t_thrd.vacuum_cxt.VacuumCostBalance += u_sess->attr.attr_storage.VacuumCostPageDirty;
}
}
}4. 列存储引擎传统行存储数据压缩率低,必须按行读取,即使读取一列也必须读取整行。openGauss创建表的时候,可以指定行存储还是列存储。列存储表也支持DML操作和MVCC。列存储架构如图所示。列存储架构列存储引擎有以下优势:(1) 列的数据特征比较相似,适合压缩,压缩比很高。(2) 当表列的个数比较多,但是访问的列个数比较少时,列存储可以按需读取列数据,大大减少不必要的读I/O开支,提高查询性能。(3) 基于列批量数据Vector(向量)的运算,CPU的缓存命中率比较高,性能比较好。列存储引擎更适合OLAP大数据统计分析的场景。1) 列存储源码组织列存储源码目录为:/src/gausskernel/storage/cstore。列存储源码文件如表所示。表 列存储源码文件模块源码文件功能cstorecompression数据压缩与解压cstore_allocspace空间分配cstore_am列存储公共API(application programming interface,应用编程接口)cstore_***_func支持函数cstore_psort列内排序cu数据压缩单元cucache_mgr缓存管理器custorage持久化存储cstore_delete删除方法cstore_update更新方法cstore_vector缓冲区实现cstore_rewriteSQL重写cstore_insert插入方法cstore_mem_alloc内存分配 2) 列存储主要API列存储主要API代码如下:/* cstore_am.cpp */
...
/* 扫描 APIs */
void InitScan(CStoreScanState *state, Snapshot snapshot = NULL);
void InitReScan();
void InitPartReScan(Relation rel);
bool IsEndScan() const;
/* 延迟读取APIs */
bool IsLateRead(int id) const;
void ResetLateRead();
/* 更新列存储扫描计时标记*/
void SetTiming(CStoreScanState *state);
/* 列存储扫描*/
void ScanByTids(_in_ CStoreIndexScanState *state, _in_ VectorBatch *idxOut, _out_ VectorBatch *vbout);
void CStoreScanWithCU(_in_ CStoreScanState *state, BatchCUData *tmpCUData, _in_ bool isVerify = false);
/* 加载数据压缩单元描述信息 */
bool LoadCUDesc(_in_ int col, __inout LoadCUDescCtl *loadInfoPtr, _in_ bool prefetch_control, _in_ Snapshot snapShot = NULL);
/* 从描述表中获取数据压缩单元描述*/
bool GetCUDesc(_in_ int col, _in_ uint32 cuid, _out_ CUDesc *cuDescPtr, _in_ Snapshot snapShot = NULL);
/* 获取元组删除信息*/
void GetCUDeleteMaskIfNeed(_in_ uint32 cuid, _in_ Snapshot snapShot);
bool GetCURowCount(_in_ int col, __inout LoadCUDescCtl *loadCUDescInfoPtr, _in_ Snapshot snapShot);
/* 获取实时行号。 */
int64 GetLivedRowNumbers(int64 *deadrows);
/* 获得数据压缩单元*/
CU *GetCUData(_in_ CUDesc *cuDescPtr, _in_ int colIdx, _in_ int valSize, _out_ int &slotId);
CU *GetUnCompressCUData(Relation rel, int col, uint32 cuid, _out_ int &slotId, ForkNumber forkNum = MAIN_FORKNUM,
bool enterCache = true) const;
/* 缓冲向量填充 APIs */
int FillVecBatch(_out_ VectorBatch *vecBatchOut);
/* 填充列向量*/
template
最后修改时间:2021-12-08 16:56:14
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




