PostgreSQL:standard_planner函数和subquery_planner函数功能
一、subquery_planner函数
1、处理WITH语句
如果有一个WITH链表,使用查询处理每个链表,并为其构建一个initplan子计划结构。
2、处理FROM子句为空的语句
如果FROM子句为空,则将其替换为伪RTE_RESULT RTE,这样我们就不需要太多特殊情况来处理这种情况。
3、上拉子链接
查找WHERE和JOIN/ON子句中的ANY/EXISTS子句,并尝试将它们转换为JOIN。
注意,此步骤不会下降为子查询;如果我们上拉子查询,它们的SubLinks将在调出它们上拉前被处理。
4、常数简化FROM子句中的任何函数RTE,然后尝试“内联”任何设置为返回函数的函数。
扫描范围表中的函数RTE,对其进行常量简化,
如果可能的话,将它们内联(生成下一个可能被拉出来的子查询)。
这里递归问题的处理方式与子链接相同。
5、上拉子查询
检查连接树中的子查询是否可以合并到该查询中
6、扁平化处理UNION ALL
如果这是一个简单的UNION ALL查询,则将其扁平化为appendrel结构。
我们现在这样做是因为它需要对UNION ALL的叶子查询应用pull_up_subqueries,
上面没有涉及到这些查询,因为它们没有被jointree引用(在我们这样做之后它们将被引用)。
7、查看范围表以查看存在哪些类型的条目,如果不使用相关的SQL特性,我们可以跳过一些后续处理。
检测是否有任何RTE中的元素是RTE_JOIN类型;如果没有,可以避免执行flatten_join_alias_vars()的开销。
检查外部连接,如果没有,可以跳过reduce_outer_join()函数。
检查LATERAL RTEs。
当然,这必须在我们完成添加范围表条目之后完成(完成pull_up_subqueries()调用)。
(注意:实际上,处理继承的或分区的REL可能会导致其子表的RTE稍后添加;
但这些必须都是RTE_RELATION条目,因此它们不会使此处得出的结论无效。)
8、如果我们现在已经验证了查询目标关系是非继承的,请将其标记为叶目标。
9、预处理RowMark信息。
我们需要在子查询上拉(以便所有非继承的RTEs都存在)和继承展开之后再执行此操作,以便expand_inherited_tables可以使用这个信息来检查和修改。
10、如果存在HAVING子句,则务必设置hasHavingQual属性。
因为preprocess_expression将把constant-true条件减少为空的条件qual列表。但是HAVING TRUE并没有语义错误。
11、对targetlist和quals以及querytree中的其他随机表达式进行表达式预处理。
注意,我们不需要显式处理排序/分组表达式,因为它们实际上是targetlist的一部分。
预处理表达式:targetList(投影列)
Constant-folding可能已经把set-returning函数去掉
返回列信息returningList
预处理条件表达式
预处理Having表达式
窗口函数
Limit子句
On Conflict子句
集合操作(AppendRelInfo)
RTEs
* 现在,已经完成了预处理表达式,特别是扁平化连接别名变量,现在可以去掉joinaliasvars链表了。
* 它们不再匹配树中其他部分中的表达式,因为我们没有在那些链表中预处理表达式
* (而且是不希望这样做,例如,在那里展开一个SubLink将导致无用的未引用的子计划)。
* 把它们放在链表中只会给以后扫描树造成问题。
* 我们可以在这之后的每一次树扫描中使用QTW_IGNORE_JOINALIASES来防止这种情况,虽然这听起来不太可靠。
* 在某些情况下,我们可能想把“HAVING”条件转移到WHERE子句中。
* 如果HAVING子句包含聚合(显式的)或易变volatile函数(因为每个GROUP只执行一次HAVING子句),就不能这样做。
* 如果有任何非空GROUPING SET,也不能这样做;
* 如果在所有GROUPING SET中没有出现任何引用列,将这样的子句移动到WHERE可能会改变结果。
* (如果只有空的GROUP SET分组集,则可以按照下面讨论的那样简化HAVING子句->WHERE中。)
* 而且,执行子句的成本非常高,所以最好每组只执行一次,尽管这样会导致选择性selectivity。
* 如果不把整个规划过程重复一遍,这是很难估计的,因此我们使用启发式的方法:
* 包含子计划的条款在HAVING的后面。
* 否则,我们将把HAVING子句移动到WHERE中,希望在聚合之前而不是聚合之后消除元组。
* 如果查询有显式分组,那么可以简单地将这样的子句移动到WHERE中;
* 任何失败的GROUP子句都不会出现在输出中,因为它的元组不会到达分组或聚合阶段。
* 否则,我们必须有一个退化的(无变量的)HAVING子句,把它放在WHERE中,
* 以便query_planner()可以在一个控制结果节点中使用它,但同时还要确保不会发出一个伪造的聚合行。
* (这本来可以做得更好,但似乎不值得继续深入优化。)
* 请注意,现在不管是qual还是parse->jointree->quals,即使它们被声明为节点 *,
* 但它们在这个点上都是都是隐式的链表形式。
12、移除多余的GROUP BY 列
13、如果存在外连接,则尝试将它们转换为普通的内部连接。
在我们完成表达式预处理之后,这个步骤相对容易完成。
14、如果我们有任何RTE_RESULT关系,请查看是否可以从连接树中删除它们。
在我们完成了表达式预处理和外部连接减少之后,这一步最有效。
14、执行主要的计划过程。
如果存在继承的目标关系,则需要特殊处理,否则直接执行grouping_planner。
15、获取我们可以访问的outer-level的参数IDs,以便稍后在extParam/allParam计算中使用。
16、如果在此查询级别中创建了initplan,则调整现存的访问路径成本和并行安全标志,以反映这些成本。
在create_plan()运行之前,initPlans实际上不会被附加到计划树中,但是我们现在必须包含它们的效果。
17、确保我们已经为最终的关系确定了成本最低的路径
(我们没有在grouping_planner中这样做,而是在最终决定中加入了initPlan的成本,尽管这不太可能改变任何事情)。
二、standard_planner函数
0、创建PlannerGlobal对象
1、为这个规划器的调用设置全局状态。
由于在给定的命令中可能存在的所有级别的子查询中都需要此数据结构,
因此我们将其保存在一个单独的结构中,每个查询PlannerInfo都链接到这个结构中。
2、评估对这个查询使用并行模式是否可行。
我们不能在独立的后台过程中进行此操作,或者在可以修改数据的命令种执行此操作,
或者说如果是涉及游标的操作,或者GUCs设置为不允许并行的值,或者查询树中存在不安全的并行函数,均不允许。
* (请注意,我们确实允许CREATE TABLE AS、SELECT INTO和CREATE MATERIALIZED VIEW来使用并行计划,
* 但是这是安全的,因为命令写到了一个全新的表中,并行处理过程worker看不到。
* 如果工workers能看到这个表,那么group locking会导致它们忽略leader的重量级关系扩展锁和GIN page锁,这会变得不安全。
* 如果希望做到并行插入,那么就必须解决这个问题;另外,更新和删除有存在额外的问题,特别是关于组合CIDs。
* 目前,如果在一个并行worker内部运行,则不会尝试使用并行模式。
* 可能最终能够放宽这一限制,但就目前而言,最好不要让worker试图创建它们自己的worker。
* 在serializable事务模式下,不能使用并行,因为谓词锁定代码不支持并行。
* 如果有人试图在serializable模式下运行并行计划,虽然这不是灾难性的,但它不会得到worker,而是会串行运行。
* 但是,假设相同的serializable级别将在计划时和执行时生效,这似乎是一个很好的启发,
* 因此,如果我们处于serializable模式,则不要生成并行计划。
3、如果确实创建了一个聚合或聚合合并计划,则通常将glob->parallelModeNeeded设置为false,
并在计划创建期间更改为true (cf. create_gather_plan, create_gather_merge_plan)。
* 但是,如果force_parallel_mode = on或force_parallel_mode = regress,
* 那么只要安全,我们就强制执行并行模式,即使最终计划不使用并行。
* 如果查询包含任何不安全的内容,那么这样做是不安全的;在这种情况下,parallelModeOK将为false。
* 注意,在这一点之后,parallelModeOK无法更改。
* 否则,查询中的所有内容要么是并行安全的,要么是并行限制的,
* 在任何一种情况下,都应该可以施加并行模式限制。
* 如果这最终出现了问题,要么查询中包含的某个函数被错误地标记为并行安全或并行限制(实际它是并行不安全的),
* 要么查询规划器本身有错误。
4、确定扫描比例
* 我们不知道用户最终会从游标中获取多少元组,但通常情况下,用户不需要所有元组,
* 或者更喜欢快速启动计划,以便更快地处理一些元组。
* 使用GUC参数来决定优化哪个分数。
* 我们将cursor_tuple_fraction记录为一个简单的分数,这意味着边界情况0和1必须在这里特别处理。
* 我们将1转换为0(“所有元组”),将0转换为非常小的分数。
5、主规划过程入口(可能会递归执行)
6、选择最优路径并把该路径转换为执行计划
获取顶层的RelOptInfo
选择最佳路径
生成执行计划
7、如为可滚动游标创建计划,确保它可以根据需要向后运行。
在需要时在最外层添加一个物化(Material)节点。
8、为了测试的目的,可以选择添加一个Gather节点,前提是这样做实际上是安全的。
* 如果有任何initplan连接到formerly-top的计划节点,将它们移动到Gather节点;
* 这与我们在materialize_finished_plan中的Material节点相同。
* 因为这个Gather没有parallel-aware的后代信号,所以我们不需要重新扫描参数。
* 使用并行模式
9、如果生成了Params,运行计划树并计算每个计划节点的extParam/allParam集合。
* 理想情况下,我们应该将其合并到set_plan_references的树遍历中,
* 但是现在它必须是独立的,因为需要在主计划之前而不是之后访问子计划。
10、最后的清理工作
11、构建PlannedStmt结构
三、函数入参和返回值
pg_plan_queries Query List --> PlannedStmt List
pg_plan_query Query --> PlannedStmt
Planner Query --> PlannedStmt
standard_planner Query --> PlannedStmt
创建PlannerGlobal对象
subquery_planner PlannerGlobal, Query --> PlannerInfo
创建PlannerInfo对象
fetch_upper_rel PlannerInfo --> RelOptInfo
get_cheapest_fractional_path RelOptInfo --> Path
create_plan PlannerInfo, Path --> Plan
PlannerGlobal:保留整个planner调用的状态,记录做计划期间的全局信息,例如:子计划、子计划的范围表等,每条查询语句有且仅有一个该变量。此状态在正在规划的命令中存在的所有级别的子查询中共享。
PlannerInfo:指向父查询的规划器相关信息(首次调用为空)
https://blog.csdn.net/weixin_47373497/article/details/121429243
- 点赞
- 收藏
- 关注作者
评论(0)