引言
在之前的博客中我主要对openGauss SQL引擎中的查询解析parser和查询重写rewriter这两个模块进行了学习和相关源文件的解析。我将openGauss SQL引擎中的几个主要模块parser、rewriter、optimizer、executor之间的关联关系、输入输出以及与数据库基本表之间的交互关系总结如下图。在之后我会开展对查询优化optimizer模块的学习。这也将是在本次比赛中我个人负责的最后一个模块的学习。
基于代价的查询优化
在前面的学习中我们已经知道,数据库的查询优化方法分为两个层次:基于规则的查询优化(逻辑优化,Rule Based Optimization,简称RBO)和基于代价的查询优化(物理优化,Cost Based Optimization,简称CBO)。
其中RBO是建立在关系代数基础上的优化,关系代数中有一些等价的逻辑变换规则,通过对关系代数表达式进行逻辑上的等价变换,可能会获得执行性能比较好的等式,这样就能提高查询的性能。而CBO则是在建立物理执行路径的过程中进行优化。例如,关系代数中虽然指定了两个关系的连接操作,但是这时的连接操作符属于逻辑运算符,它没有指定以何种方式实现这种逻辑连接操作,而查询执行器executor是不认识关系代数中的逻辑连接操作的,我们需要生成多个物理连接路径来实现关系代数中的逻辑连接操作,并根据查询执行器的执行步骤,建立代价计算模式,通过计算所有的物理连接路径的代价,从中选择出最优的路径。
我们知道,查询重写rewriter是基于优化规则的等价变换,属于逻辑优化,也可以称为基于规则的优化。那么,怎么衡量对一个SQL语句进行查询重写之后,它的性能一定是提升的呢?如上面所说,此时基于代价对查询重写进行评估就非常重要了,因此查询重写不只是基于规则的查询重写,还可以是基于代价的查询重写。
查询优化的大致阶段
在查询解析工作完成之后,其最终产物—查询树链表将被移交给查询优化模块,该模块的入口函数是pg _plan_queries,它负责将查询树链表变成执行计划链表。pg_plan_queries 只会处理非Utility命令( 即 SELECT/INSERT/UPDATE/DELETE/MERGE 指令,详见 SQL rewriter查询重写概述 ),它调用pg_plan_query对每一个查询树进行处理,并将生成的 PlannedStmt结构体构成一个链表(执行计划链表)返回。pg_plan_query中负责实际计划生成的是planner函数,它也是optimizer的入口函数。
PlannedStmt* planner(Query* parse, int cursorOptions, ParamListInfo boundParams)
如上所述,查询优化的最终目的是得到可被执行模块执行的最优计划,其整个过程可分为预处理、生成路径和生成计划三个阶段。
-
预处理阶段。预处理实际上是对查询树(Query结构体)的进一步改造,这种改造可通过SQL语句体现。在此过程中,最重要的是提升子链接和提升子查询;
-
生成路径阶段。接收到重写后的查询树后,采用动态规划算法或遗传算法,生成最优连接路径和候选路径链表;
-
生成计划阶段。用得到的最优路径,首先生成基本计划树,然后添加GROUP BY、HAVING和ORDER BY等子句所对应的计划节点形成完整计划树。
optimizer源码组织
openGauss查询优化模块大概由以下几部分组成(各部分文件路径见上图):
1. 查询重写prep。注意此重写非彼重写(rewriter),rewriter主要负责对查询树应用既定义的重写规则,而此处的查询重写主要负责子查询优化、谓词化简及正则化、谓词传递闭包等查询优化预处理阶段的重写工作。
2. 统计信息。它负责生成各种类型的统计信息,供代价估算模块来使用。
3. 代价估算。它主要进行选择率估算、行数估算、代价估算等,基于代价对查询重写进行评估。
4. 物理路径path。它负责生成执行的物理路径,包括最优连接路径和候选路径链表等。
5. 动态规划plan。它负责通过动态规划方法对物理路径进行搜索。
6. 遗传算法geqo。它负责通过遗传算法对物理路径进行搜索。需要注意的是,指示是否启用遗传算法进行优化的开关在path模块中;如果启用,且连接的表超过一定数量,就调用geqo目录中的遗传算法进行物理路径的优化。
入口函数 planner()
函数源码及部分注释如下:
/*****************************************************************************
*
* Query optimizer entry point
*
* To support loadable plugins that monitor or modify planner behavior,
* we provide a hook variable that lets a plugin get control before and
* after the standard planning process. The plugin would normally
* call standard_planner().
*
*****************************************************************************/
PlannedStmt* planner(Query* parse, int cursorOptions, ParamListInfo boundParams)
{
PlannedStmt* result = NULL;
instr_time starttime;
double totaltime = 0;
INSTR_TIME_SET_CURRENT(starttime);
#ifdef PGXC
……
/*
* A Coordinator receiving a query from another Coordinator
* is not allowed to go into PGXC planner.
*/
if ((IS_PGXC_COORDINATOR || IS_SINGLE_NODE) && !IsConnFromCoord())
result = pgxc_planner(parse, cursorOptions, boundParams);
else
#endif
result = standard_planner(parse, cursorOptions, boundParams);
totaltime += elapsed_time(&starttime);
result->plannertime = totaltime;
……
return result;
}
阅读函数头部注释我们可以知道,为了支持可加载的插件来监视或修改规划者的行为,函数提供了一个hook变量,让插件在标准规划过程之前和之后获得控制权。并且通常情况下,插件会进入调用standard_planner() 的分支。
函数的调用逻辑还是较为简单的。在进行了必要的变量声明和初始化后,进入到一个逻辑分支,这个分支会决定调用函数pgxc_planner还是standard_planner,而两者都会返回一个PlannedStmt类型的结构体给result。根据#ifdef PGXC和if语句内的宏名可以推测,pgxc_planner的调用与分布式的PG版本PGXC有关,因此大多数时候都是调用standard_planner函数来获取PlannedStmt结构体,即优化后的查询树。并且在分支的前后,函数通过INSTR_TIME_SET_CURRENT宏和函数elapsed_time来统计planner的执行时间,更新到reslut的字段plannertime中。
/* typedef struct PlannedStmt */
double plannertime; /* planner execute time */
结构体PlannedStmt定义的部分源码如下,它位于文件plannode.h
中。
typedef struct PlannedStmt {
NodeTag type;
CmdType commandType; /* select|insert|update|delete */
……
Plan* planTree; /* tree of Plan nodes */
List* rtable; /* list of RangeTblEntry nodes */
/* rtable indexes of target relations for INSERT/UPDATE/DELETE */
List* resultRelations; /* integer list of RT indexes, or NIL */
Node* utilityStmt; /* non-null if this is DECLARE CURSOR */
List* subplans; /* Plan trees for SubPlan expressions */
……
double plannertime; /* planner execute time */
……
} PlannedStmt;
总结
在本篇博客中我对openGauss查询优化optimizer模块进行了简要概述。在之后的博客中我会开展对这一部分相关源文件的学习和解读工作。感谢阅读。