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

Posted weixin_47373497

tags:

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

2021SC@SDUSC

目录

概述

我负责的PostgreSQL代码部分:查询的编译与执行
此篇博客的分析内容:查询优化——生成计划
查询优化的整个过程可以分为预处理,生成路径和生成计划三个阶段。在上一篇博客中我分析了生成完整计划的过程和其中调用的函数在这篇博客中我会分析查询优化的最后一步同时也是生成计划的最后一步——整理计划树
生成的完整计划经过计划树整理后就可以交给查询执行器去执行了,负责整理工作的主函数是set_plan_references函数。整理计划树是查询优化器的最后一步,主要是为了方便执行器的执行,对计划树一些表达上的细节做最后的调整。例如,将上层的Var结构变为对子计划的输出结果的引用,获取操作符的OID等。同时,这一步也会删除那些没有任何用处的子查询扫描计划节点。在进行整理工作时会使用不同的函数来进行不同的整理动作,主要的整理函数如下:

主要函数功能介绍
fix_expr_references通过调用fix_expr_references_walker,完成表达式(目标属性表或条件表达式)的清理工作
set_subqueryscan_references在SubqueryScan上进行set_plan_references的操作,即试图去除掉SubqueryScan节点
fix_opfuncids通过调用fix_opfuncids_walker,在一个表达式的树中对操作符调用的表达式节点(OpExpr node)的值进行补全
trivial_subqueryscan检测一个SubqueryScan是否可以从计划树中删除
adjust_plan_varnos通过rtoffset调整varnos和其他所涉及的表的索引在计划树中的偏移量
adjust_expr_varnos通过rtoffset来调整变量中的varnos在一个表达式中的偏移量
fix_expr_references做表达式(如目标属性表或约束表达式)最后的清理工作
set_join_references通过设置varnos为内连接或外连接和设置attno的值为内连接或外连接的结果的元组的数目来修改join节点的目标属性表和约束表达式
set_inner_join_references处理出现在内部索引连接的表达式的join节点
set_uppernode_references根据左子树子计划的返回元组来更新上层的目标变量列表和表达式
build_tlist_index为一个子节点的目标属性表建立一个索引的结果
search_indexed_tlist_for_var在一个索引的列表中寻找一个变量var,假如找到的话,返回它的一份拷贝,假如没有找到,则返回null
search_indexed_tlist_for_non_var在一个索引的列表中寻找一个非变量的节点,假如存在返回一个Var指向这个item,假如不存在,则返回null
join_references通过修改句子中的varno/varattno的值为外连接或内连接的目标属性表来建立一个join表达式或目标属性表的集合
replace_vars_with_subplan_refs这个程序修改表达式树,使得所有的变量节点都与子计划的目标节点相关联。它主要用来处理上层计划节点中的非连接表达式或目标属性表

下面,我将挑选部分函数进行代码分析和函数讲解

set_plan_references函数

该函数的功能有
1:将各种子查询的范围表减少到一个单一的列表中,并且消除对执行者无用的RangeTblEntry字段
2:调整扫描节点中的变量,以匹配扁平的范围表
3:调整顶级计划节点中的变量,使它们指的是输出子计划
4:对ag计划节点中的汇总表局部聚合或最小聚合优化
5:PARAM_MULTIEXPR参数被普通的PARAM_EXEC参数所取代
6:计算运算符的regproc OIDs(即寻找实现每个运算符的函数)
7:创建计划所依赖的具体对象的清单,这些清单被plancache.c用来控制缓存计划的失效
8:为计划树上的每个计划节点分配一个唯一的ID

set_plan_references(PlannerInfo *root, Plan *plan)

	PlannerGlobal *glob = root->glob;
	int			rtoffset = list_length(glob->finalrtable);
	ListCell   *lc;
//将所有查询的RTE添加到扁平化的范围表中。RTE将通过rtoffset增加它们的rangetable索引(额外的RTEs,没有被Plan树引用的,可能会在这些RTEs之后被添加)
	add_rtes_to_flat_rtable(root, false);
//调整PlanRowMarks的RT索引并添加到最终的行标记列表中

	foreach(lc, root->rowMarks)//遍历
	
		PlanRowMark *rc = lfirst_node(PlanRowMark, lc);
		PlanRowMark *newrc;
    //因为所有的字段都是标量,所以平面拷贝就足够了 
		newrc = (PlanRowMark *) palloc(sizeof(PlanRowMark));
		memcpy(newrc, rc, sizeof(PlanRowMark));
    //调整索引......但不是行标志的索引
		newrc->rti += rtoffset;
		newrc->prti += rtoffset;
		//添加到finalrowmarks字段
		glob->finalrowmarks = lappend(glob->finalrowmarks, newrc);
	
	//修改计划树
	return set_plan_refs(root, plan, rtoffset);

set_subqueryscan_references函数

在SubqueryScan上进行set_plan_references的操作,即试图去除掉SubqueryScan节点,如果不能,必须对其进行正常处理。

set_subqueryscan_references(PlannerInfo *root,
							SubqueryScan *plan,
							int rtoffset)

	RelOptInfo *rel;
	Plan	   *result;

	 //需要查找子查询的RelOptInfo,因为需要它的子根 
	 	rel = find_base_rel(root, plan->scan.scanrelid);

	//递归地处理子计划
	plan->subplan = set_plan_references(rel->subroot, plan->subplan);

	if (trivial_subqueryscan(plan))
	
		// 我们可以省略SubqueryScan节点,直接拉出子计划
		result = clean_up_removed_plan_level((Plan *) plan, plan->subplan);
	
	else
	
		 //保留SubqueryScan节点,必须做set_plan_references本来要做的处理。注意,在这里没有做set_upper_references(),因为SubqueryScan在创建时总是会正确引用其子计划的输出
		
		plan->scan.scanrelid += rtoffset;
		plan->scan.plan.targetlist =
			fix_scan_list(root, plan->scan.plan.targetlist, rtoffset);
		plan->scan.plan.qual =
			fix_scan_list(root, plan->scan.plan.qual, rtoffset);

		result = (Plan *) plan;
	
   //返回查询树
	return result;

set_join_references函数

通过将varnos设置为OUTER_VAR或INNER_VAR,并将attno值设置为相应的外连接或内连接元组项目的结果域号,来修改连接节点的目标列表和quals以引用其子计划

set_join_references(PlannerInfo *root, Join *join, int rtoffset)

	Plan	   *outer_plan = join->plan.lefttree;
	Plan	   *inner_plan = join->plan.righttree;
	indexed_tlist *outer_itlist;
	indexed_tlist *inner_itlist;

	outer_itlist = build_tlist_index(outer_plan->targetlist);
	inner_itlist = build_tlist_index(inner_plan->targetlist);

	//首先处理连接表达式(包括合并或散列子句)这些在逻辑上是在联接的下面,所以它们总是可以使用输入列表中的所有可用值。也可以处理NestLoopParams,因为这些不能引用可空的子表达式
	join->joinqual = fix_join_expr(root,join->joinqual,
								   outer_itlist,
								   inner_itlist,
								   (Index) 0,
								   rtoffset);

	//做具体的连接类型的事情
	if (IsA(join, NestLoop))
	
		NestLoop   *nl = (NestLoop *) join;
		ListCell   *lc;

		foreach(lc, nl->nestParams)
		
			NestLoopParam *nlp = (NestLoopParam *) lfirst(lc);

			nlp->paramval = (Var *) fix_upper_expr(root,
												   (Node *) nlp->paramval,												   outer_itlist,OUTER_VAR,rtoffset);
//检查用简单的Var替换了任何PlaceHolderVar
			if (!(IsA(nlp->paramval, Var) &&
				  nlp->paramval->varno == OUTER_VAR))
				elog(ERROR, "NestLoopParam was not reduced to a simple Var");
		
	
	else if (IsA(join, MergeJoin))//判断链接类型是否为MergeJoin
	
		MergeJoin  *mj = (MergeJoin *) join;

		mj->mergeclauses = fix_join_expr(root,
										 mj->mergeclauses,
										 outer_itlist,
										 inner_itlist,
										 (Index) 0,
										 rtoffset);
	
	else if (IsA(join, HashJoin))//判断是否为连接是否为HashJoin
	
		HashJoin   *hj = (HashJoin *) join;

		hj->hashclauses = fix_join_expr(root,
										hj->hashclauses,
										outer_itlist,
										inner_itlist,
										(Index) 0,
										rtoffset);

//HashJoin的哈希键被用来从它的外部计划中寻找哈希表的匹配图元。
		hj->hashkeys = (List *) fix_upper_expr(root,
											   (Node *) hj->hashkeys, outer_itlist,OUTER_VAR,rtoffset);
	

	 //需要修正 targetlist 和 qpqual,它们在逻辑上位于连接之上,这意味着它们不应该重复使用任何在外层连接的可空侧计算的输入表达式
	 //判断连接的类型
	switch (join->jointype)
	
		case JOIN_LEFT:
		case JOIN_SEMI:
		case JOIN_ANTI:
			inner_itlist->has_non_vars = false;
			break;
		case JOIN_RIGHT:
			outer_itlist->has_non_vars = false;
			break;
		case JOIN_FULL:
			outer_itlist->has_non_vars = false;
			inner_itlist->has_non_vars = false;
			break;
		default:
			break;
	

	join->plan.targetlist = fix_join_expr(root,
										  join->plan.targetlist,
										  outer_itlist,
										  inner_itlist,
										  (Index) 0,
										  rtoffset);
	join->plan.qual = fix_join_expr(root,
									join->plan.qual,
									outer_itlist,
									inner_itlist,
									(Index) 0,
									rtoffset);

	pfree(outer_itlist);
	pfree(inner_itlist);


search_indexed_tlist_for_non_var函数

search_indexed_tlist_for_non_var 函数 在一个有索引的tlist中寻找一个非Var 如果找到一个匹配的,返回一个构造为引用该tlist项的Var。如果没有匹配,则返回NULL。注意:除非itlist->has_ph_vars或itlist>has_non_vars,否则调用这个是浪费时间。 此外,set_join_references()依赖于能够通过清除itlist->has_non_vars来防止非vars的匹配。

search_indexed_tlist_for_non_var(Expr *node,
								 indexed_tlist *itlist, Index newvarno)

	TargetEntry *tle;
//如果它是一个简单的常量,用Var来代替它是很愚蠢的,即使下面刚好有一个相同的常量;Var比常量的执行成本更高。 更重要的是,替换它可能会混淆执行器中的一些地方,这些地方希望看到简单的Consts,例如,放弃的列
	if (IsA(node, Const))//判断node节点的类型是否是const
		return NULL;

	tle = tlist_member(node, itlist->tlist);
	if (tle)
	
		//找到一个匹配的子计划输出表达
		Var		   *newvar;

		newvar = makeVarFromTargetEntry(newvarno, tle);
		newvar->varnoold = 0;	//没有一个普通的Var
		newvar->varoattno = 0;
		return newvar;
	
	return NULL;				//不匹配返回null

trivial_subqueryscan函数

检测一个SubqueryScan是否可以从计划树中删除。如果它没有资格检查,可以删除它,目标列表只是重新消化子计划的输出。

trivial_subqueryscan(SubqueryScan *plan)

	int			attrno;
	ListCell   *lp,
			   *lc;
	//如果计划表达式不为空则返回false
	if (plan->scan.plan.qual != NIL)
		return false;
//如果//列表的长度不一样则返回false
	if (list_length(plan->scan.plan.targetlist) !=
		list_length(plan->subplan->targetlist))
		return false;			

	attrno = 1;
	forboth(lp, plan->scan.plan.targetlist, lc, plan->subplan->targetlist)
	
		TargetEntry *ptle = (TargetEntry *) lfirst(lp);
		TargetEntry *ctle = (TargetEntry *) lfirst(lc);
     //如果tlist与垃圾状态不匹配,则返回false
		if (ptle->resjunk != ctle->resjunk)
			return false;	

//接受一个引用子计划tlist中相应元素的Var,或者一个等同于子计划元素的Const
		if (ptle->expr && IsA(ptle->expr, Var))
		
			Var		   *var = (Var *) ptle->expr;

			Assert(var->varno == plan->scan.scanrelid);
			Assert(var->varlevelsup == 0);
			//如果无序时则返回false
			if (var->varattno != attrno)
				return false;	
		
		else if (ptle->expr && IsA(ptle->expr, Const))
		
			if (!equal(ptle->expr, ctle->expr))
				return false;
		
		else
			return false;

		attrno++;
	
	return true;

build_tlist_index函数

build_tlist_index — 为一个子列表建立一个索引数据结构。
在大多数情况下,子计划tlist将是只有Vars的 "平面 "tlist。
所以我们试图通过提取Vars的信息来优化这种情况,将父列表与子列表相匹配仍然是一个O(N^2)操作,但至少比普通的常数小得多。

build_tlist_index(List *tlist)

	indexed_tlist *itlist;
	tlist_vinfo *vinfo;
	ListCell   *l;//临时变量

	//创建有足够槽位的数据结构,以容纳所有tlist条目
	itlist = (indexed_tlist *)
		palloc(offsetof(indexed_tlist, vars) +
			   list_length(tlist) * sizeof(tlist_vinfo));
    //itlist初始化
	itlist->tlist = tlist;
	itlist->has_ph_vars = false;
	itlist->has_non_vars = false;

	//找到Vars并填入索引数组
	vinfo = itlist->vars;
	foreach(l, tlist)//遍历tlist链表
	
		TargetEntry *tle = (TargetEntry *) lfirst(l);

		if (tle->expr && IsA(tle->expr, Var))
		
			Var		   *var = (Var *) tle->expr;
           //给varno字段赋值
			vinfo->varno = var->varno;
			 //给varattno字段赋值
			vinfo->varattno = var->varattno;
			 //给resno字段赋值
			vinfo->resno = tle->resno;
			vinfo++;
		
		//如果表达式是一个PlaceHolderVar类型则将has_ph_vars设置为true
		else if (tle->expr && IsA(tle->expr, PlaceHolderVar))
			itlist->has_ph_vars = true;
		//否则如果不是的话就将has_non_vars字段设置为true
		else
			itlist->has_non_vars = true;
	

	itlist->num_vars = (vinfo - itlist->vars);

	return itlist;

总结

通过这篇博客,我讲解了查询优化的最后一个步骤即整理计划树(此步骤之后就可以将经过计划树整理后就可以交给查询执行器去执行了)的主要函数及其功能。以及对其中一些函数的代码进行分析,感谢批评指正!

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

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

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

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

PostgreSQL学习系列—EXPLAIN ANALYZE查询计划解读

禁用 PostgreSQL 查询优化?

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