PostgreSQL——查询优化——生成优化计划1

Posted weixin_47373497

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PostgreSQL——查询优化——生成优化计划1相关的知识,希望对你有一定的参考价值。

2021SC@SDUSC

概述

我负责的PostgreSQL代码部分:查询的编译与执行
此篇博客的分析内容:查询优化——生成计划
查询优化的整个过程可以分为预处理,生成路径和生成计划三个阶段。在前几篇博客中,我分析完了查询优化的第二个阶段——生成路径,详细的介绍和分析了生成基本关系的访问路径的方法,生成索引扫描路径,生成TID扫描路径以及生成最终路径的方法。完成路径生成以后,查询规划器就可以开始进行计划的生成。所以在查询优化的整个第二阶段分析完成以后,我将继续分析查询优化的第三个阶段——生成优化计划,在这篇博客中我会以生成计划为总脉络,逐个分析不同优化计划的生成

生成可优化的MIN/MAX聚集计划

在生成计划时,规划器会首先处理一种比较特殊的查询:查询中含有MIN/MAX聚集函数,并且聚集函数使用的属性上建有索引或者属性恰好是 ORDER BY 子句中指定的属性。,在这种特殊情况下,可以直接从索引或者已经排序好的元组集中取到含有最大最小值的元组,从而避免了扫描全表带来的开销。规划器会先检查一个查询是否可以优化到不对全表扫描而直接读取元组,如果可以则生成可优化的MIN/MAX聚集计划,否则需要对全表扫描,生成普通计划。而负责生成可优化的MIN/MAX聚集计划的主函数是preprocess_minmax_aggregates.

preprocess_minmax_aggregates函数

preprocess_minmax_aggregates函数从一个选定的路径生成计划,如果该路径对应的查询满足可优化的MIN/MAX聚集计划的条件,该函数会返回一个计划,否则该函数返回空值。如果该函数能够生成一个非空的计划,则后续生成普通计划的步骤就不再进行,将这个计划进行完善后作为最终的计划。

preprocess_minmax_aggregates(PlannerInfo *root)

	Query	   *parse = root->parse;//查询树(Query结构体)
	FromExpr   *jtnode;
	RangeTblRef *rtr;
	RangeTblEntry *rte;
	List	   *aggs_list;
	RelOptInfo *grouped_rel;
	ListCell   *lc;//临时变量

	//判断minmax_aggs列表是不是空的,如果是空的则跳过
	Assert(root->minmax_aggs == NIL);

//通过查询树(Query结构体)中的字段hasAggs判断该查询或者子查询中是否存在聚集函数,如果存在则继续进行,否则退出
	if (!parse->hasAggs)
		return;

	Assert(!parse->setOperations);	//判断是否有查询树的setOperations字段是否为空
	Assert(parse->rowMarks == NIL);

	//通过查询树中的字段groupClause和hasWindowFuncs字段分别判断查询或者子查询中是否存在分组group by或者窗口函数,如果存在则退出
	//因为如果存在分组或者窗口函数,则必须对表中的所有元组进行扫描,不可能优化到直接获取单个元组
	if (parse->groupClause || list_length(parse->groupingSets) > 1 ||
		parse->hasWindowFuncs)
		return;

//如果查询包含任何CTE,则拒绝;因为没有办法对一个CTE建立索引扫描
	if (parse->cteList)
		return;

	 //限制查询只能精确地引用一个表,因为连接 条件不能被合理地处理
	 //单一的表可能是一个继承父表,包括一个UNION ALL子查询的情况
	jtnode = parse->jointree;
	//检查范围表的个数来判断是否为单表查询,如果存在2个或者2个以上的范围表,则退出,否则通过函数planner_rt_fetch读取该范围表
	while (IsA(jtnode, FromExpr))
	
		if (list_length(jtnode->fromlist) != 1)
			return;
		jtnode = linitial(jtnode->fromlist);
	
	if (!IsA(jtnode, RangeTblRef))
		return;
	rtr = (RangeTblRef *) jtnode;
	rte = planner_rt_fetch(rtr->rtindex, root);//如果为单表查询则通过函数planner_rt_fetch读取该范围表
	if (rte->rtekind == RTE_RELATION)//判断是否为普通表
	else if (rte->rtekind == RTE_SUBQUERY && rte->inh)// 扁平化的UNION ALL子查询
		
	else
		return;
	//扫描tlist和HAVING子句,找到所有的聚合体,并验证是否都是MIN/MAX聚合,如果有一个不是则立即停止
	// 通过函数find_minmax_aggs_walker查找目标属性和having子句中出现的所有聚集函数
	 //如果都是MIN/MAX聚集函数,则继续否则退出
	 //函数find_minmax_aggs_walker递归扫描目标属性或having子句中的聚集节点(用数据结构Aggref表示的节点),检查是否都为MIN/MAX型聚集节点。如果是,则将所有MIN/MAX型聚集节点保存到变量agg_list中,并返回FALSE;如果在查找过程中发现了非MIN/MAX型的聚集节点,则返回true,说明存在其它类型的聚集函数,这种情况下,直接退出,不会进行优化处理。
	aggs_list = NIL;
	if (find_minmax_aggs_walker((Node *) root->processed_tlist, &aggs_list))//没有聚集函数则退出
		return;
	if (find_minmax_aggs_walker(parse->havingQual, &aggs_list))
		return;


	 //对于变量aggs_list中保存的每个MIN/MAX聚集函数,通过函数build_minmax_path查找可用哪个索引对其进行优化,并计算它优化后的代价(利用索引访问一条元组的代价,不包括对目标属性的代价评估)如果都可以被优化则继续,否则退出
	 //函数build_minmax_path对于给定的MIN/MAX聚集节点info,在关系rel中试图找到一个可以使之优化的索引,并创建最优的索引路径
	//为每个聚合体建立一个访问路径。如果任何一个聚合体,是不可索引的,则放弃建立访问路径的操作
	 

函数build_minmax_path的主要流程步骤:
(1)判断聚集与索引属性是否匹配,并且确定了索引扫描方向。主要通过函数match_agg_to_index_col来判断是否匹配,如果聚集的排序操作符与索引中前向扫描的操作符类匹配则返回ForwardScanDirection,进行前向扫描;未匹配,则判断聚集的排序操作符与索引中后向扫描的操作符类是否匹配,如果匹配则返回BackwardScanDirection;如果都不匹配则返回NoMovementScanDirection,表明不进行扫描。
(2)提取约束信息即按照怎样的约束条件通过索引获取元组
(3) 创建索引访问路径。通过函数create_index_path来实现,具体实现可以看我前一篇的博客,分析了创建索引扫描路径
(4)进行代价评估,并选取所有索引扫描路径中代价最小的路径

	foreach(lc, aggs_list)
	
		MinMaxAggInfo *mminfo = (MinMaxAggInfo *) lfirst(lc);
		Oid			eqop;
		bool		reverse;

		eqop = get_equality_op_for_ordering_op(mminfo->aggsortop, &reverse);
		if (!OidIsValid(eqop))
			elog(ERROR, "could not find equality operator for ordering operator %u",
				 mminfo->aggsortop);

		if (build_minmax_path(root, mminfo, eqop, mminfo->aggsortop, reverse))
			continue;
		if (build_minmax_path(root, mminfo, eqop, mminfo->aggsortop, !reverse))
			continue;
		//聚合体没有可索引的路径,所以返回
		return;
	

	 //首先创建一个MinMaxAggPath节点
	foreach(lc, aggs_list)//为每个MinMaxAggPath创建一个输出 Param 节点
	
		MinMaxAggInfo *mminfo = (MinMaxAggInfo *) lfirst(lc);

		mminfo->param =
			SS_make_initplan_output_param(root,
										  exprType((Node *) mminfo->target),
										  -1,
										  exprCollation((Node *) mminfo->target));
	

	 //用适当的估计成本和其他需要的数据创建一个MinMaxAggPath节点,并将其添加到UPPEREL_GROUP_AGG上层,在那里它将与标准聚合实现竞争
	 
	grouped_rel = fetch_upper_rel(root, UPPERREL_GROUP_AGG, NULL);
	add_path(grouped_rel, (Path *)
			 create_minmaxagg_path(root, grouped_rel,
								   create_pathtarget(root,
													 root->processed_tlist),
								   aggs_list,
								   (List *) parse->havingQual));

生成普通计划

如果preprocess_minmax_aggregates函数返回的是空值,则需要继续生成普通计划。生成普通计划的入口函数是create_plan,这个函数为最优路径创建计划,根据路径节点的不同类型,分别调用不同的函数生成相应的计划。
普通计划主要分为:

普通计划类型主要功能
扫描计划主要有顺序扫描,索引扫描等计划类型
连接计划主要有嵌套循环连接,hash连接,归并连接等计划类型
其他计划例如:Append计划,Result计划,物化计划等

我会根据普通计划的类型来逐一进行分析

扫描计划——顺序扫描

函数create_seqscan_plan是主要用于生成顺序扫描计划,该函数最后会返回一个类型为SeqScan的结构。

create_seqscan_plan函数执行流程

开始
对扫描约束信息排序:order_qual_clauses函数
获取约束子句: extract_actual_clauses函数
创建顺序扫描计划:make_seqscan函数
复制代价估计信息:copy_generic_path_info函数
结束

create_seqscan_plan函数

//返回'最佳路径'扫描的基本关系的seqscan计划,限制子句为'scan子句'和targetlist 'tlist'
create_seqscan_plan(PlannerInfo *root, Path *best_path,
					List *tlist, List *scan_clauses)

//PlannerInfo *root:类型为PlannerInfo的指针,指向当前规划器的状态信息
//Path *best_path:类型为Path指针,志向最优路径
//List *tlist:类型为List指针,指向目标属性链表,链表中每一个节点都是一个var结构
// List *scan_clauses:为List指针,指向扫描约束信息
	SeqScan    *scan_plan;
	Index		scan_relid = best_path->parent->relid;
	
	Assert(scan_relid > 0);
	Assert(best_path->parent->rtekind == RTE_RELATION);

	//对扫描约束信息按照执行器代价估计的最佳执行顺序排序
	scan_clauses = order_qual_clauses(root, scan_clauses);

	//对约束信息的排序是完整的RestrictInfo类型。当对约束信息排序完成后,需要获得实际的约束子句
	//将RestrictInfo列表缩减为无表达式;忽略伪常数
	scan_clauses = extract_actual_clauses(scan_clauses, false);

	/* Replace any outer-relation variables with nestloop params */
	if (best_path->param_info)
	
		scan_clauses = (List *)
			replace_nestloop_params(root, (Node *) scan_clauses);
	
	//创建顺序扫描计划,将目标属性,扫描表,约束子句赋值给seqscan计划节点,并将其左右子树置空(扫描计划节点均以叶子形式存在)
	scan_plan = make_seqscan(tlist,
							 scan_clauses,
							 scan_relid);

	//复制估计代价信息,该部分是对Path中代价估计的一个拷贝
	copy_generic_path_info(&scan_plan->plan, best_path);

	return scan_plan;//返回SeqScan结构体

create_seqscan_plan函数返回的结构体——Scan

typedef struct Scan

	Plan		plan;//该顺序扫描节点对应的计划信息
	Index		scanrelid; //要扫描的表在范围表中的索引号
 Scan;
typedef Scan SeqScan;

连接计划——嵌套循环连接

嵌套循环连接是由函数create_nestloop_plan生成,该函数具有和前面分析过的create_seqscan相同的两个参数root和best_path。create_nestloop_plan函数将会创建一个嵌套循环连接计划节点,并连接子计划树(outer_plan和inner_plan)到该节点,最终形成嵌套循环连接计划并返回。

create_nestloop_plan函数执行流程

以上是关于PostgreSQL——查询优化——生成优化计划1的主要内容,如果未能解决你的问题,请参考以下文章

PostgreSQL——查询优化——生成优化计划1

PostgreSQL——查询优化——生成优化计划2

PostgreSQL——查询优化——生成优化计划2

PostgreSQL——查询优化——生成优化计划2

跟我一起读postgresql源码——Executor(查询执行模块之——可优化语句的执行)

PostgreSQL——查询优化——整理计划树