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函数执行流程

开始
删除冗余连接约束:select_nonredundant_join_clauses函数
对连接约束信息排序: order_qual_clauses函数
分离外连接获得约束子句:extract_actual_clauses函数
创建嵌套循环计划树:make_nestloop函数
复制代价估计信息:copy_generic_path_info函数
结束

create_nestloop_plan函数

create_nestloop_plan(PlannerInfo *root,
					 NestPath *best_path)
{
	NestLoop   *join_plan;//存放最后的返回结果
	Plan	   *outer_plan;//内关系对应的计划
	Plan	   *inner_plan;//内关系对应的计划
	List	   *tlist = build_path_tlist(root, &best_path->path);
	List	   *joinrestrictclauses = best_path->joinrestrictinfo;
	List	   *joinclauses;
	List	   *otherclauses;
	Relids		outerrelids;
	List	   *nestParams;
	Relids		saveOuterRels = root->curOuterRels;

	//从best_path中获取目标属性及连接约束信息
	outer_plan = create_plan_recurse(root, best_path->outerjoinpath, 0);

	root->curOuterRels = bms_union(root->curOuterRels,
								   best_path->outerjoinpath->parent->relids);

	inner_plan = create_plan_recurse(root, best_path->innerjoinpath, 0);

	//恢复数据
	bms_free(root->curOuterRels);
	root->curOuterRels = saveOuterRels;

	
	//对连接约束信息进行排序
	//对连接约束信息按执行器最佳执行顺序排序,只考虑代价估计信息,即按照代价估计信息进行排序
	joinrestrictclauses = order_qual_clauses(root, joinrestrictclauses);

	if (IS_OUTER_JOIN(best_path->jointype))//检查是否存在外连接
	{
	//分离外连接获得约束子句
		extract_actual_join_clauses(joinrestrictclauses,
									best_path->path.parent->relids,
									&joinclauses, &otherclauses);
	}
	else
	{
		//对内联的所有子句进行同样的处理
		joinclauses = extract_actual_clauses(joinrestrictclauses, false);
		otherclauses = NIL;
	}

	//用nestloop参数替换任何外部关系变量
	if (best_path->path.param_info)
	{
		joinclauses = (List *)
			replace_nestloop_params(root, (Node *) joinclauses);
		otherclauses = (List *)
			replace_nestloop_params(root, (Node *) otherclauses);
	}

	//识别任何应该由该连接节点提供的nestloop参数,并从root->curOuterParams中删除它们
	outerrelids = best_path->outerjoinpath->parent->relids;
	nestParams = identify_current_nestloop_params(root, outerrelids);
//创建嵌套循环计划树,将目标属性,连接子句,外连接子句赋值给Nestloop计划节点,并将其外关系计划树和内关系计划树作为其左右子树
	join_plan = make_nestloop(tlist,
							  joinclauses,
							  otherclauses,
							  nestParams,
							  outer_plan,
							  inner_plan,
							  best_path->jointype,
							  best_path->inner_unique);
    //复制代价估计信息,该部分是对Path中代价估计的一个拷贝
	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
    
	return join_plan;
}

这里需要解释一下外连接子句的处理,外连接的结果元组某一侧可以为空值。PostgreSQL并没有将外连接作为连接子句处理,而是将它赋值给NestLoop节点包含的plan中的qual字段,采取一种类似于基本表的扫描方式

create_nestloop_plan函数最后返回的结构体——NestLoop

typedef struct NestLoop
{
Join join;
}
typedef struct Join
{
	Plan		plan;
	JoinType	jointype;
	bool		inner_unique;
	List	   *joinqual;		//连接约束信息
} Join;

除了嵌套循环连接外,还有归并连接和Hash连接两种连接方式,但是生成计划的过程类似于我在这里分析的循环嵌套连接。主要过程都包括提取有效连接约束信息,对连接约束信息排序,分离外连接,创建相应类型的计划树,复制代价估计等步骤。但是单独的,对于归并连接,还需要考虑内外关系的排序情况;对于Hash连接,要从连接约束子句中提取出Hash连接子句,将其赋值给计划节点的不同字段。

总结

通过此篇源码分析,了解到了PostgreSQL查询优化的第三阶段——生成优化计划。

感谢批评指正

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixin_47373497

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值