在之前的博客中我主要对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的入口函数。
如上所述,查询优化的最终目的是得到可被执行模块执行的最优计划,其整个过程可分为预处理、生成路径和生成计划三个阶段。
预处理阶段。预处理实际上是对查询树(Query结构体)的进一步改造,这种改造可通过SQL语句体现。在此过程中,最重要的是提升子链接和提升子查询;
生成路径阶段。接收到重写后的查询树后,采用动态规划算法或遗传算法,生成最优连接路径和候选路径链表;
生成计划阶段。用得到的最优路径,首先生成基本计划树,然后添加GROUP BY、HAVING和ORDER BY等子句所对应的计划节点形成完整计划树。
openGauss查询优化模块大概由以下几部分组成(各部分文件路径见上图):
1. 查询重写prep。注意此重写非彼重写(rewriter),rewriter主要负责对查询树应用既定义的重写规则,而此处的查询重写主要负责子查询优化、谓词化简及正则化、谓词传递闭包等查询优化预处理阶段的重写工作。
2. 统计信息。它负责生成各种类型的统计信息,供代价估算模块来使用。
3. 代价估算。它主要进行选择率估算、行数估算、代价估算等,基于代价对查询重写进行评估。
4. 物理路径path。它负责生成执行的物理路径,包括最优连接路径和候选路径链表等。
5. 动态规划plan。它负责通过动态规划方法对物理路径进行搜索。
6. 遗传算法geqo。它负责通过遗传算法对物理路径进行搜索。需要注意的是,指示是否启用遗传算法进行优化的开关在path模块中;如果启用,且连接的表超过一定数量,就调用geqo目录中的遗传算法进行物理路径的优化。
函数源码及部分注释如下:
阅读函数头部注释我们可以知道,为了支持可加载的插件来监视或修改规划者的行为,函数提供了一个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中。
结构体PlannedStmt定义的部分源码如下,它位于文件中。
在本篇博客中我对openGauss查询优化optimizer模块进行了简要概述。在之后的博客中我会开展对这一部分相关源文件的学习和解读工作。感谢阅读。