引言
在上篇博客中我们已经介绍过,optimizer 查询规划的主处理部分是由位于planner.cpp文件下的函数subquery_planner() 来完成的;在本篇博客中,我将通过对这一函数的学习对查询优化过程的主处理逻辑和代码架构进行解析。
文件路径
/src/gausskernel/optimizer/plan/planner.cpp
查询规划流程逻辑架构
在上篇博客中我们介绍了,函数 standard_planner() 会调用 subquery_planner() 来完成查询规划的实际生成工作。在查询计划树构造完成后,standard_planner() 调用 set_plan_references() 来完成查询规划的清理工作。
top_plan = set_plan_references(root, top_plan);
在之前的博客中我们介绍过,查询优化的最终目的是得到可被执行模块执行的最优计划树,其整个过程可分为预处理、生成路径和生成计划三个阶段( 详见 SQL optimizer 查询优化概述 )。
其中预处理阶段,subquery_planner() 通过调用preprocess_const_params(), pull_up_sublinks(), lazyagg_main() 等函数来对查询树(Query结构体)进行进一步的改造和重写;
……
preprocess_const_params(root, (Node*)parse->jointree);
……
replace_empty_jointree(parse);
……
pull_up_sublinks(root);
……
reduce_orderby(parse, false);
……
removeNotNullTest(root);
……
lazyagg_main(parse);
……
而生成路径和生成计划的工作,subquery_planner() 主要调用 grouping_planner() 来处理。( inheritance_planner函数在处理完继承关系后仍然调用 grouping_planner() 函数来进行查询计划和最优路径的生成)。在执行过程中,grouping_planner() 不再对查询树做变换处理,而是将查询中的信息进行规范化并传给 query_planner() 。query_planner() 函数调用 make_one_rel() 进入查询优化阶段,并将最优路径放入 cheapest_path。然后grouping_planner函数再调用 get_cheapest_fractional_path_for_pathkeys函数寻找符合需求的排序路径 sorted_path,并最终确定最优路径并生成基本计划树(调用create_plan函数)。
if (parse->resultRelation && parse->commandType != CMD_INSERT &&
rt_fetch(parse->resultRelation, parse->rtable)->inh)
plan = inheritance_planner(root);
else {
plan = grouping_planner(root, tuple_fraction);
……
最后,subquery_planner() 调用set_plan_references() 函数清理现场,做一些变量的调整工作,不对计划树做本质的改变。我将查询规划过程中涉及的主要函数之间的调用逻辑以及各函数功能总结如下图。
subquery_planner() 代码架构
通过上面的介绍我们已经知道,函数 subquery_planner() 会完成查询计划和最优执行路径的生成,并将生成结果保存在一个 Plan 结构变量中返回给 standard_planner() 。接下来我们来关注 subquery_planner() 中一些主要步骤的处理和函数调用。
Plan* subquery_planner(PlannerGlobal* glob, Query* parse, PlannerInfo* parent_root, bool hasRecursion, double tuple_fraction, PlannerInfo** subroot, int options, ItstDisKey* diskeys, List* subqueryRestrictInfo)
在完成必要的变量声明和初始化后,函数为当前查询构造PlannerInfo的初始结构。其中root是一个PlannerInfo* 类型的变量,它保存查询计划生成过程中的各类信息。
/* Create a PlannerInfo data structure for this subquery */
root = makeNode(PlannerInfo);
root->parse = parse;
root->glob = glob;
root->query_level = parent_root ? parent_root->query_level + 1 : 1;
root->parent_root = parent_root;
root->plan_params = NIL;
root->planner_cxt = CurrentMemoryContext;
root->init_plans = NIL;
root->cte_plan_ids = NIL;
root->eq_classes = NIL;
root->append_rel_list = NIL;
root->rowMarks = NIL;
root->hasInheritedTarget = false;
root->grouping_map = NULL;
root->subquery_type = options;
root->param_upper = NULL;
root->hasRownumQual = false;
为接下来的预处理重写和查询优化工作申请合适的内存空间。
QueryRewriteContext = AllocSetContextCreate(CurrentMemoryContext,
RewriteContextName,
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_INITSIZE,
ALLOCSET_DEFAULT_MAXSIZE);
oldcontext = MemoryContextSwitchTo(QueryRewriteContext);
进行CTE( Common Table Expressions,公用表表达式)的递归检查。CTE是一个命名的临时结果集,作用范围是当前语句。CTE可以理解成一个可以复用的子查询,它由with-as 语句声明在SELECT关键字前。关于with语句以及CTE的相关知识此处不再详述。
if (hasRecursion || (parent_root && parent_root->is_under_recursive_cte)) {
root->is_under_recursive_cte = true;
root->is_under_recursive_tree = parent_root->is_under_recursive_tree;
} else {
root->is_under_recursive_cte = false;
}
check_is_support_recursive_cte(root);
调用preprocess_const_params(),用常量替换查询中的等式。
DEBUG_QRW("Before rewrite");
preprocess_const_params(root, (Node*)parse->jointree);
DEBUG_QRW("After const params replace ");
处理CTE(with子句),为其构造子查询结构。
if (parse->cteList) {
SS_process_ctes(root);
}
替换掉空的FROM语句。
replace_empty_jointree(parse);
如果含有子链接,进行子链接的提升(下篇博客中将介绍)。
if (parse->hasSubLinks) {
pull_up_sublinks(root);
DEBUG_QRW("After sublink pullup");
}
减少order by语句。
reduce_orderby(parse, false);
重写 “懒聚合” lazy aggs。
if ((LAZY_AGG & u_sess->attr.attr_sql.rewrite_rule)
&& permit_from_rewrite_hint(root, LAZY_AGG)) {
lazyagg_main(parse);
DEBUG_QRW("After lazyagg");
}
提升子查询。尝试将多个子查询合并到当前查询中。
parse->jointree = (FromExpr*)pull_up_subqueries(root, (Node*)parse->jointree);
UNION ALL 语句优化。
if (parse->setOperations) {
flatten_simple_union_all(root);
DEBUG_QRW("After simple union all flatten");
}
展开继承表。
if (u_sess->opt_cxt.is_stream || IS_PGXC_DATANODE) {
/*
* Expand the Dfs table.
*/
expand_dfs_tables(root);
}
expand_inherited_tables(root);
预处理表达式。表达式的预处理工作主要由函数preprocess_expression() 完成,该函数采用递归扫描的方式处理PlannerInfo结构体里面保存的目标属性、HAVING子句、OFFSET子句、LIMIT子句和连接树jointree。总体来说,做了以下几件事:
-
调用flatten_join_alias_vars函数,用基本关系变量取代连接别名变量;
-
调用函数eval_const_expression进行常量表达式的简化,也就是直接计算出常量表达式的值。例如 “ 1+1>2 ” 会被替换为FALSE;
-
对表达式进行规范化,主要是将表达式转化为最佳析取范式或合取范式;
-
调用make_subplan() 将子链接转化为子计划树。
parse->targetList = (List*)preprocess_expression(root, (Node*)parse->targetList, EXPRKIND_TARGET);
parse->returningList = (List*)preprocess_expression(root, (Node*)parse->returningList, EXPRKIND_TARGET);
preprocess_qual_conditions(root, (Node*)parse->jointree);
parse->havingQual = preprocess_expression(root, parse->havingQual, EXPRKIND_QUAL);
……
处理HAVING子句。如果HAVING子句中没有聚集函数,那么可以将其退化到WHERE子句中去,否则它将被写到查询树的HavingQual字段里面。
if (!parse->unique_check) {
newHaving = NIL;
foreach(l, (List *) parse->havingQual) {
Node *havingclause = (Node *)lfirst(l);
if (contain_agg_clause(havingclause) ||
contain_volatile_functions(havingclause) ||
contain_subplans(havingclause)
|| parse->groupingSets) {
/* keep it in HAVING */
newHaving = lappend(newHaving, havingclause);
} else if (parse->groupClause) {
/* move it to WHERE */
parse->jointree->quals = (Node *)
lappend((List *) parse->jointree->quals, havingclause);
} else {
/* put a copy in WHERE, keep it in HAVING */
parse->jointree->quals = (Node *)
lappend((List *) parse->jointree->quals,
copyObject(havingclause));
newHaving = lappend(newHaving, havingclause);
}
}
parse->havingQual = (Node *) newHaving;
}
DEBUG_QRW("After having qual rewrite");
消除外连接。
if (hasOuterJoins) {
reduce_outer_joins(root);
……
进行全连接的重写。
if (support_rewrite) {
reduce_inequality_fulljoins(root);
……
去除冗余的RTE(这一结构在这篇博客 SQL rewriter解读(3)—— rewriteHandler.cpp解读(三)中有介绍)。
if (hasResultRTEs)
remove_useless_result_rtes(root);
生成查询计划和执行路径,这一部分主要调用 grouping_planner() 来处理(inheritance_planner在处理完继承关系后仍然调用grouping_planner函数来进行查询计划和最优路径的生成)。在执行过程中,grouping_planner() 不再对查询树做变换处理,而是将查询中的信息进行规范化并传给 query_planner() 。query_planner() 函数调用 make_one_rel() 进入查询优化阶段,并将最优路径放入 cheapest_path。然后 grouping_planner函数再调用 get_cheapest_fractional_path_for_pathkeys函数寻找符合需求的排序路径 sorted_path,并最终确定最优路径并生成基本计划树(调用create_plan函数)。
if (parse->resultRelation && parse->commandType != CMD_INSERT &&
rt_fetch(parse->resultRelation, parse->rtable)->inh)
plan = inheritance_planner(root);
else {
plan = grouping_planner(root, tuple_fraction);
调用SS_finalize_plan() 清理计划树。
if (list_length(glob->subplans) != num_old_subplans || root->glob->nParamExec > 0)
SS_finalize_plan(root, plan, true);
函数subquery_planner() 内部的主要处理步骤大致就是这些。我将其主要步骤的处理流程总结如下图:
总结
在本篇博客中我对 subquery_planner() 函数的主要步骤流程以及查询优化的主处理逻辑和代码架构进行了学习和解读。个人能力有限,如有错误烦请不吝指出。