玛卡巴卡的卡巴卡玛 发表于 2025-3-21 19:03:20

深入明白 PostgreSQL Planner:简化扫描路径与查询计划

引言

当向 PostgreSQL 发送查询时,查询通常会经过几个处置处罚阶段,并最终返回结果。这些阶段如下所示:

[*]剖析(Parse)
[*]分析(Analyze)
[*]重写(Rewrite)
[*]计划(Plan)
[*]执行(Execute)
在本文中,我们将仅关注“计划”阶段或“规划器(Planner)”模块,由于这是最风趣或最复杂的阶段。我将分享我对规划器模块的明白,并探讨它如那边置处罚一个简单的次序扫描。
规划器的目标非常简单:从可用路径中识别出最快的“路径”,并根据此路径制定一个“计划”,以便“执行器”模块在下一阶段执行它。然而,识别最快的“路径”就是造成规划器复杂的原因。
注:本文基于 PostgreSQL 16 撰写。
一切从哪里开始

在 postgres.c 中的 exec_simple_query() 函数是查询处置处罚阶段的出发点。我们将关注它进入 pg_plan_query() 后发生的事变。下面我只会提到它会调用的紧张函数。
https://img2024.cnblogs.com/other/3599012/202503/3599012-20250321163617569-955261382.png
在 pg_plan_query() 背后发生了什么?

实际上,发生了许多事变,例如:

[*]识别子查询、分区表、外部表、连接等
[*]通过表访问方法估算所有涉及的表的巨细
[*]识别完成查询的所有可能路径
[*]次序扫描、索引扫描、TID 扫描、并行工作线程等
[*]在所有路径中,找到最佳路径,通常是本钱最低的
[*]根据此路径制定计划
以一个简单的 SELECT 查询为例,该查询只涉及一个表,没有连接或子查询,下面是大致的调用栈:
https://img2024.cnblogs.com/other/3599012/202503/3599012-20250321163618329-419359600.png
这个调用栈图进行了极简化,但展示了规划器模块的几个关键元素。带有蓝色星号的块将在下一节中具体解释。
set_base_rel_sizes()

顾名思义,这是估算所有关系(表、视图、索引等)巨细的主要入口。巨细包括估算的行数(元组)和列数。通常需要通过“堆访问方法”来获取这些信息,这样它就可以访问“缓冲区管理器”和“存储管理器”,以提供对巨细的估算。
总巨细将是所有相关表的巨细。这对后续的“本钱估算”阶段非常紧张。
set_base_rel_pathlist()

对于一个简单表的次序扫描,程序将在此处结束。对于更复杂的查询,将有不同的路径构建技术。有关其他路径构建技术,请参阅 allpaths.c 中的 set_rel_pathlist()。
当前,默认添加了 4 种扫描路径:

[*]次序扫描(Sequential Scan)

[*]次序扫描所有内容

[*]部分次序扫描(Partial Sequential Scan)

[*]由于要通过“聚合”节点(在下一阶段处置处罚)进行聚合,因此被标记为“部分”。这本质上意味着并行次序扫描。
[*]仅在关系或查询被认为是并行安全时添加

[*]索引扫描(Index Scan)

[*]如果表有索引,它可以被认为是一个潜在路径

[*]TID 扫描(TID Scan)

[*]如果查询包罗范围限定子句(如 WHERE ctid > '(1,30)' AND ctid < '(30,5)'),则可以选择 TID 扫描

所有这些路径都涉及一些本钱,通过元组或页面的数量以及每个元组/页面的本钱因子来估算,本钱因子配置如下:
# Planner Cost Constants

- `seq_page_cost` = 1.0                  # 任意尺度
- `random_page_cost` = 4.0               # 与上述相同的尺度
- `cpu_tuple_cost` = 0.01                  # 与上述相同的尺度
- `cpu_index_tuple_cost` = 0.005         # 与上述相同的尺度
- `cpu_operator_cost` = 0.0025             # 与上述相同的尺度
- `parallel_setup_cost` = 1000.0         # 与上述相同的尺度
- `parallel_tuple_cost` = 0.1            # 与上述相同的尺度不同的路径方法有不同的本钱盘算,它们会调用以下方法来盘算“启动本钱”和“运行本钱”:

[*]cost_seqscan()
[*]cost_indexscan()
[*]cost_tidscan()
你可以通过调整这些本钱来影响规划器在选择最理想路径时的决策。例如,如果你希望规划器更多地利用并行扫描,可以思量降低并行扫描元组的本钱,例如将 parallel_tuple_cost 设置为更小的值,如 0.001。
add_path 被调用来将路径添加到潜在路径列表中,但请记住,规划器的路径构建机制具有驱逐机制。这意味着如果我们打算添加一个明显优于当前路径的新路径,规划器可能会丢弃所有现有路径,接受新的路径。类似地,如果要添加的路径明显较差,它将不会被添加。
如果规划器认为并行次序扫描是安全的,则会调用 add_partial_path。这种次序扫描被称为“部分”扫描,由于它需要收集和聚合数据以形成最闭幕果,因此会产生额外的本钱,那么就会造成并行性并不总是理想的结果。这里有个履历法则:

[*]如果 PostgreSQL 必须扫描大量数据,但我们只需要此中一小部分,利用并行扫描可以提高服从;
[*]如果 PostgreSQL 必须扫描大量数据,而且大部分数据都是我们需要的,并行扫描可能会更慢。
generate_gather_paths

如果已经添加了某些部分路径(通常是次序扫描子路径),则会调用该函数。这个例程添加了一种新的路径范例“gather”,它包罗一个子路径“次序扫描”。聚合路径需要思量每个子路径的本钱,以及从并行工作线程获取元组并聚合数据的本钱。
get_cheapest_fractional_path 和 create_plan

一旦所有潜在路径候选项被添加后,调用这个函数来选择最便宜的路径,即具有最低总本钱的路径。然后,这个选择的路径将被通报给 create_plan,在这里路径(及其子路径,如果有的话)将被递归创建,并形成最终的计划结构,供执行器明白和执行。
检察计划

我们可以在查询前利用 EXPLAIN ANALYZE 来检查规划器选择的最便宜计划及其本钱细节。以下示例是一个查询计划,它包罗一个名为“gather”的主计划,内里有一个名为“次序扫描”的部分计划,并且有 2 个工作线程。你可以通过箭头(->)来判定哪个是子路径。
postgres=# explain analyze select * from test where a > 500000 and a <600000;
                        QUERY PLAN
------------------------------------------------------------
Gather(cost=1000.00..329718.40 rows=112390 width=36) (actual time=62.362..5106.295 rows=99999 loops=1)
   Workers Planned: 2
   Workers Launched: 2
   ->Parallel Seq Scan on test(cost=0.00..317479.40 rows=46829 width=36) (actual time=58.020..3416.544 rows=33333 loops=3)
         Filter: ((a > 500000) AND (a < 600000))
         Rows Removed by Filter: 13300000
Planning Time: 0.489 ms
Execution Time: 5110.030 ms
(8 rows)如果规划器选择次序扫描主路径且没有任何子路径,查询计划将如下所示:
postgres=# explain analyze select * from test where a > 500000;
                        QUERY PLAN
------------------------------------------------------------
Seq Scan on test(cost=0.00..676994.40 rows=39571047 width=6) (actual time=0.011..7852.896 rows=39500000 loops=1)
   Filter: (a > 500000)
   Rows Removed by Filter: 500000
Planning Time: 0.115 ms
Execution Time: 9318.773 ms
(5 rows)总结

PostgreSQL 规划器的内部机制较为复杂,但通过本文剖析,您应已掌握其核心运行逻辑。若您需要针对特定场景定制优化查询性能(例如调整代价模型参数)或扩展功能(如引入新的扫描路径策略),至少能基于现有原理明确切入点。
本文由博客一文多发平台 OpenWrite 发布!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 深入明白 PostgreSQL Planner:简化扫描路径与查询计划