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

2021SC@SDUSC

概述

我负责的PostgreSQL代码部分:查询的编译与执行
此篇博客的分析内容:查询优化——生成计划
查询优化的整个过程可以分为预处理,生成路径和生成计划三个阶段。在上一篇博客中我分析了可优化的MIN/MAX聚集计划和普通计划的生成在这篇博客中我会分析生成完整计划
上篇博客介绍了依据最优路径best_path生成计划树的过程。之前讲过,生成路径仅仅考虑基本查询语句信息,并没有保留诸如GROUP BY,ORDER BY等信息。grouping_planner函数调用create_plan生成基本计划树后,则会依据查询树相关约束信息在前面生成的普通计划之上添加相应的计划节点生成完整计划。
生成完整计划的过程主要包含两个大步骤:创建聚集计划节点,创建排序计划节点

生成完整计划

创建聚集计划节点

通过调用函数make_agg,生成聚集计划节点,函数返回一个类型位Agg的结构

make_agg函数执行流程

开始
计划节点初始化
获得左子树代价
计算聚集代价
计算HAVING字句代价
复制代价信息目标属性等信息
结束

make_agg函数

make_agg(List *tlist, List *qual,
		 AggStrategy aggstrategy, AggSplit aggsplit,
		 int numGroupCols, AttrNumber *grpColIdx, Oid *grpOperators, Oid *grpCollations,
		 List *groupingSets, List *chain,
		 double dNumGroups, Plan *lefttree)
{ 
//传入参数分析:
//AggStrategy aggstrategy:类型为AggStrategy,指该聚集计划采用的聚集策略。聚集策略有:AGG_PLAIN(普通聚集)AGG_SORTED(排序聚集),AGG_HASHED(Hash聚集)
//int numGroupCols:类型为int,需要聚集的属性数目
//AttrNumber *grpColIdx:类型为AttrNumber指针类型,代表聚集属性的索引
//Oid *grpOperators:类型为OID指针类型,代表用于分组时的排序操作符
//Plan *lefttree:为Plan的指针类型(可以是实际的计划类型强制转换成Plan类型)代表添加聚集计划节点前的计划树,该计划树作为Agg的左子树

	Agg		   *node = makeNode(Agg);//产生agg节点
	Plan	   *plan = &node->plan;
	long		numGroups;

	/* Reduce to long, but 'ware overflow! */
	numGroups = (long) Min(dNumGroups, (double) LONG_MAX);
//初始化计划节点
	node->aggstrategy = aggstrategy;//初始化聚集策略
	node->aggsplit = aggsplit;
	node->numCols = numGroupCols;//初始化分组属性列数
	node->grpColIdx = grpColIdx;//初始化分组列的索引
	node->grpOperators = grpOperators;//初始化分组操作符
	node->grpCollations = grpCollations;
	node->numGroups = numGroups;//初始化预计分组的数目
	node->aggParams = NULL;	//调用SS_finalize_plan()函数填充这个
	node->groupingSets = groupingSets;
	node->chain = chain;

	plan->qual = qual;
	plan->targetlist = tlist;//初始化目标列
	plan->lefttree = lefttree;//初始化左子树
	plan->righttree = NULL;

	return node;
}
make_agg函数返回的结构体——Agg
typedef struct Agg
{
	Plan		plan;           //计划树
	AggStrategy aggstrategy;	//聚集策略
	int			numCols;		//分组属性列数
	AttrNumber *grpColIdx;		//分组列的索引
	Oid		   *grpOperators;	//分组操作符
	Oid		   *grpCollations;
	long		numGroups;		//预计分组数目
	Bitmapset  *aggParams;		//Aggref输入中使用的参数ID
	//规划器仅在HASHED/MIXED情况下提供numGroups和aggParams
	List	   *groupingSets;	//要使用的分组集
	List	   *chain;			//链式聚合/排序节点
} Agg;

通过数据结构可以看出来,Agg计划节点除了包含Plan类型成员变量外,还添加了和聚集分组相关的一些成员变量,包括分组属性数目,分组属性的索引,分组的操作符等,这些扩展成员变量由外部调用传入参数直接赋值;获得左子树的代价(仅保留大小估计,时间代价会被覆盖),调用cost_agg计算他的聚集代价,对于存在HAVING子句的情况调用cost_qual_eval估计它的代价;最后将代价,目标属性,HAVING子句赋给plan相应的成员变量(成员变量的解释在前一篇博客的顺序扫描计划中),并将原有计划树作为它的左子树添加进来形成完整的计划树。

make_agg函数调用——copy_plan_costsize函数——获得左子树代价
copy_plan_costsize(Plan *dest, Plan *src)
{
	dest->startup_cost = src->startup_cost;//为开始的代价赋值
	dest->total_cost = src->total_cost;//给总代价赋值
	dest->plan_rows = src->plan_rows;//记录计划的行数
	dest->plan_width = src->plan_width;//记录计划的宽度
	//假设被插入的节点不具有并行能力
	dest->parallel_aware = false;
	//假设插入的节点是并行安全的
	dest->parallel_safe = src->parallel_safe;
}

make_agg函数调用——cost_agg函数——计算聚集代价
cost_agg(Path *path, PlannerInfo *root,
		 AggStrategy aggstrategy, const AggClauseCosts *aggcosts,
		 int numGroupCols, double numGroups,
		 List *quals,
		 Cost input_startup_cost, Cost input_total_cost,
		 double input_tuples)
{
	double		output_tuples;
	Cost		startup_cost;
	Cost		total_cost;
	AggClauseCosts dummy_aggcosts;

	//如果传入NULL,则使用全部为零的代价
	if (aggcosts == NULL)
	{
		Assert(aggstrategy == AGG_HASHED);
		MemSet(&dummy_aggcosts, 0, sizeof(AggClauseCosts));
		aggcosts = &dummy_aggcosts;
	}

	//aggcosts的transCost.per_tuple部分对每个输入元组计算一次代价,finalCost.per_tuple部分对每个输出元组计算一次代价,这些计算的代价对应于评估最终表达式的值
	
	//如果要进行分组,需要对每个分组列和每个输入元组计算额外的cpu_operator_cost代价
	//如果不分组,将产生一个单一的输出元组,需要对每个输出元组收取cpu_tuple_cost代价
	
	//GG_SORTED和AGG_HASHED具有完全相同的总的CPU成本相同,但AGG_SORTED的启动成本较低。 如果输入路径已经被适当的排序,AGG_SORTED应该是首选(因为它没有内存风险)
	if (aggstrategy == AGG_PLAIN)
	{
		startup_cost = input_total_cost;
		startup_cost += aggcosts->transCost.startup;
		startup_cost += aggcosts->transCost.per_tuple * input_tuples;
		startup_cost += aggcosts->finalCost.startup;
		startup_cost += aggcosts->finalCost.per_tuple;
		//不是一个分组
		total_cost = startup_cost + cpu_tuple_cost;
		output_tuples = 1;
	}
	else if (aggstrategy == AGG_SORTED || aggstrategy == AGG_MIXED)
	{
		//在这里能够提供及时的输出
		startup_cost = input_startup_cost;
		total_cost = input_total_cost;
		if (aggstrategy == AGG_MIXED && !enable_hashagg)
		{
			startup_cost += disable_cost;
			total_cost += disable_cost;
		}
		//这样的计算方式与HASHED情况相匹配
		total_cost += aggcosts->transCost.startup;
		total_cost += aggcosts->transCost.per_tuple * input_tuples;
		total_cost += (cpu_operator_cost * numGroupCols) * input_tuples;
		total_cost += aggcosts->finalCost.startup;
		total_cost += aggcosts->finalCost.per_tuple * numGroups;
		total_cost += cpu_tuple_cost * numGroups;
		output_tuples = numGroups;
	}
//如果有资格证,要考虑其成本和选择性
	if (quals)
	{
		QualCost	qual_cost;

		cost_qual_eval(&qual_cost, quals, root);
		startup_cost += qual_cost.startup;
		total_cost += qual_cost.startup + output_tuples * qual_cost.per_tuple;

		output_tuples = clamp_row_est(output_tuples *
									  clauselist_selectivity(root,
															 quals,
															 0,
															 JOIN_INNER,
															 NULL));
	}

	path->rows = output_tuples;
	path->startup_cost = startup_cost;
	path->total_cost = total_cost;
}
make_agg函数调用——cost_qual_eval函数——计算Having字句代价
cost_qual_eval(QualCost *cost, List *quals, PlannerInfo *root)
{
//输入可以是一个隐式AND的布尔表达式列表,也可以是一个RestrictInfo节点列表,表达式或者是一个RestrictInfo节点的列表
//结果包括一次性的(启动)组件和每个评价的组件
	cost_qual_eval_context context;
	ListCell   *l;

	context.root = root;
	context.total.startup = 0;
	context.total.per_tuple = 0;

	//不对最高级别的隐含ANDing计算代价

	foreach(l, quals)
	{
		Node	   *qual = (Node *) lfirst(l);

		cost_qual_eval_walker(qual, &context);
	}

	*cost = context.total;
}

创建排序计划节点

make_sort函数用于生成排序计划节点,函数返回一个类型为sort的结构

make_sort函数执行流程

开始
获得左子树代价
计算排序代价
复制代价信息目标属性等信息
结束

make_sort函数

static Sort *
make_sort(Plan *lefttree, int numCols,
		  AttrNumber *sortColIdx, Oid *sortOperators,
		  Oid *collations, bool *nullsFirst)
{ 
//传入参数分析:
//int numCols:类型为int,代表排序的属性数目
//AttrNumber *sortColIdx:类型为AttrNumber 指针类型,代表排序的属性索引
 //Oid *sortOperators:类型为Oid指针类型,代表排序的操作符

	Sort	   *node = makeNode(Sort);//创建节点
	Plan	   *plan = &node->plan;//初始化计划
//初始化节点
	plan->targetlist = lefttree->targetlist;
	plan->qual = NIL;
	plan->lefttree = lefttree;//给左子树赋值
	plan->righttree = NULL;//右子树置空
	node->numCols = numCols;//给列数赋值
	//给sort节点赋值
	node->sortColIdx = sortColIdx;//给节点的排序属性列索引赋值
	node->sortOperators = sortOperators;//给节点的排序操作符赋值

	return node;
}
make_sort函数返回的结构体——Sort
typedef struct Sort
{
	Plan		plan;           //生成的计划
	int			numCols;		//排序属性列数
	AttrNumber *sortColIdx;		//排序属性列索引
	Oid		   *sortOperators;	//排序操作符
	Oid		   *collations;		//碰撞的OID	
} Sort;

通过数据结构可以看出,Sort计划节点除了包含Plan类型成员变量外,添加了和排序相关的一些成员变量,包括排序属性索引,排序操作符等,这些扩展成员变量由外部调用传入参数直接赋值。这里需要额外做的是调用cost_Sort估计排序代价,并将原有计划树连接至左子树,形成完整计划树。

make_sort函数函数调用——cost_sort函数——计算排序代价
cost_sort(Path *path, PlannerInfo *root,
		  List *pathkeys, Cost input_cost, double tuples, int width,
		  Cost comparison_cost, int sort_mem,
		  double limit_tuples)
{
	Cost		startup_cost = input_cost;//开始的代价等于输入的代价
	Cost		run_cost = 0;//初始化运行代价为0
	double		input_bytes = relation_byte_size(tuples, width);
	double		output_bytes;
	double		output_tuples;
	long		sort_mem_bytes = sort_mem * 1024L;

	if (!enable_sort)
		startup_cost += disable_cost;

	path->rows = tuples;

//要确保排序的成本永远不会被估计为零,即使传入的元组数为零
	if (tuples < 2.0)
		tuples = 2.0;

	//包括默认的每项成本的比较
	comparison_cost += 2.0 * cpu_operator_cost;

	//判断limit是否有效
	if (limit_tuples > 0 && limit_tuples < tuples)
	{
		output_tuples = limit_tuples;
		output_bytes = relation_byte_size(output_tuples, width);
	}
	else
	{
		output_tuples = tuples;
		output_bytes = input_bytes;
	}

	if (output_bytes > sort_mem_bytes)
	{
		//将使用基于磁盘的排序方法对所有图元进行排序
		double		npages = ceil(input_bytes / BLCKSZ);
		double		nruns = input_bytes / sort_mem_bytes;
		double		mergeorder = tuplesort_merge_order(sort_mem_bytes);
		double		log_runs;
		double		npageaccesses;

		//假设大约有N个对数N的比较
		startup_cost += comparison_cost * tuples * LOG2(tuples);

		//开始计算磁盘开销
		
      //计算logM(r)为log(r) / log(M)
	if (nruns > mergeorder)log_runs = ceil(log(nruns) / log(mergeorder));
		else
			log_runs = 1.0;
		npageaccesses = 2.0 * npages * log_runs;
		//假设3/4的访问是连续的,1/4的访问是不连续的
		startup_cost += npageaccesses *
			(seq_page_cost * 0.75 + random_page_cost * 0.25);
	}
	else if (tuples > 2 * output_tuples || input_bytes > sort_mem_bytes)
	{
		 //使用一个有界的堆排序,在内存中只保留K个元组,所以元组比较的总数为Nlog2 K
		 //但是常数系数比quicksort高一点,对它进行调整,使成本曲线在交叉点是连续的
		 
		startup_cost += comparison_cost * tuples * LOG2(2.0 * output_tuples);
	}
	else
	{
		//对所有的输入图元使用普通的quicksort(快速排序)方法
		startup_cost += comparison_cost * tuples * LOG2(tuples);
	}

	 //不计算cpu_tuple_cost代价,因为一个排序节点是不做质量检查或预测的
	 //要注意,在这里使用tuples而不是output_tuples
	run_cost += cpu_operator_cost * tuples;

	path->startup_cost = startup_cost;
	path->total_cost = startup_cost + run_cost;
}

由以上计划节点的生成过程可以得知,后期完整计划树的封装主要是计算其相应代价信息,并将相关信息填入计划节点。对于不同的语句,前期还会生成相应的物理执行策略。例如,聚集的时候会产生依据数据的统计信息决定采取普通聚集,排序聚集或Hash聚集,不同的物理执行策略就会产生不同的代价。

总结

通过这篇博客,我讲解了生成完整计划的过程和其中调用的函数,感谢批评指正!

  • 0
    点赞
  • 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、付费专栏及课程。

余额充值