深入理解 PostgreSQL Planner:简化扫描路径与查询计划
引言
当向 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 发布!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
数据思维 = 会写SQL或Python?
OSCHINA 编辑部【OSC 有问必答】栏目,每周一会,聚焦开发者提出的实际问题,邀请行业专家、技术大咖或资深开发者进行深度剖析和解答,人话版呈现开发者们最关心的问题。 欢迎各位开发者说出你最关心的技术难题,也欢迎资深开发 er、行业专家、学者大咖们自荐! 交流可添加微信:JunoHsu1122 数据驱动的时代,开发者们面临的挑战不仅仅是技术实现,更是如何将数据转化为实际的业务价值。 数据思维,作为连接技术与业务的桥梁,正逐渐成为开发者们的必备能力。 本期【OSC 有问必答】栏目,我们邀请到了拥有十年银行数据分析项目经验的资深专家钱兴会,他将从实战角度出发,深入探讨数据思维的本质、应用场景以及如何在银行业务中落地。 本期嘉宾: 钱兴会 Fintech Career社区主理人、北京市人工智能行业协会专家、清华大数据产业联盟专家委员会专家、广东大数据产业联盟专家委员、拥有超过十年银行数据分析项目经验的资深专业人士,服务了数十家大型银行。 在金融领域的多个数据驱动项目中发挥了关键作用,擅长通过数据挖掘与分析提升业务效率与决策质量。专业背景涵盖了风险管理、客户行为分析以及产品优化等多个领域。...
- 下一篇
三分钟掌握音频提取 | 在 Rust 中优雅地处理视频音频
前言 在多媒体开发中,从视频中提取音频是一个常见需求。比如,你可能需要分离背景音乐来单独欣赏,或者提取对白用于语音分析,甚至为视频生成字幕。无论目的如何,音频提取都是多媒体处理中的基础操作。 传统上,我们可以通过 FFmpeg 命令行工具快速实现这一功能,例如: ffmpeg -i input.mp4 -vn -acodec copy output.aac 这条命令用 -vn 禁用视频流,-acodec copy 直接拷贝音频流,简单高效。但对于 Rust 开发者来说,直接在代码中调用命令行工具可能会遇到一些麻烦,尤其是在需要深度集成或精细控制时。难道就没有更优雅的方式吗?本文将带你探索如何在 Rust 中处理音频提取,既实用又易懂,三分钟让你上手! 痛点与场景 在 Rust 项目中处理音视频时,开发者常常会遇到以下问题: 命令行调用不够灵活 通过 std::process::Command 执行 FFmpeg 命令需要启动外部进程,不仅增加了资源开销,还得手动处理错误和输出。万一路径不对或参数写错,调试起来也很头疼。 参数繁琐,学习成本高 FFmpeg 的参数多如牛毛,像 -vn、-...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS关闭SELinux安全模块
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- CentOS6,CentOS7官方镜像安装Oracle11G
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- Red5直播服务器,属于Java语言的直播服务器
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS7,CentOS8安装Elasticsearch6.8.6