引言
当向 PostgreSQL 发送查询时,查询通常会经过几个处置处罚阶段,并最终返回结果。这些阶段如下所示:
- 剖析(Parse)
- 分析(Analyze)
- 重写(Rewrite)
- 计划(Plan)
- 执行(Execute)
在本文中,我们将仅关注“计划”阶段或“规划器(Planner)”模块,由于这是最风趣或最复杂的阶段。我将分享我对规划器模块的明白,并探讨它如那边置处罚一个简单的次序扫描。
规划器的目标非常简单:从可用路径中识别出最快的“路径”,并根据此路径制定一个“计划”,以便“执行器”模块在下一阶段执行它。然而,识别最快的“路径”就是造成规划器复杂的原因。
注:本文基于 PostgreSQL 16 撰写。
一切从哪里开始
在 postgres.c 中的 exec_simple_query() 函数是查询处置处罚阶段的出发点。我们将关注它进入 pg_plan_query() 后发生的事变。下面我只会提到它会调用的紧张函数。
在 pg_plan_query() 背后发生了什么?
实际上,发生了许多事变,例如:
- 识别子查询、分区表、外部表、连接等
- 通过表访问方法估算所有涉及的表的巨细
- 识别完成查询的所有可能路径
- 次序扫描、索引扫描、TID 扫描、并行工作线程等
- 在所有路径中,找到最佳路径,通常是本钱最低的
- 根据此路径制定计划
以一个简单的 SELECT 查询为例,该查询只涉及一个表,没有连接或子查询,下面是大致的调用栈:
这个调用栈图进行了极简化,但展示了规划器模块的几个关键元素。带有蓝色星号的块将在下一节中具体解释。
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企服之家,中国第一个企服评测及商务社交产业平台。 |