由 John Doe 八月 12, 2025
你需要使用到优化器参数,调整 SQL 执行计划吗?新版本的 PostgreSQL,优化器参数的工作方式变得不同了。
特性提交日志
调整 EXPLAIN 中禁用节点的输出。
提交 c01743aa4 为 EXPLAIN 输出新增了功能,当计划节点的禁用节点计数大于 0 时,会显示该计数。但显然,喜欢这种输出方式的人并不多,由于禁用节点会向上累积到根计划节点,每个包含禁用子节点的父节点都会显示 “Disabled Nodes” 信息。这使得通过 EXPLAIN 输出很难(有时甚至不可能)确定哪些节点被禁用了。你可能会认为,可以通过手动累加某个节点子节点的 “Disabled Nodes” 输出数值,来判断该节点的禁用节点计数是否高于其子节点,但对于 Append 和 Merge Append 节点,如果某些禁用的子节点在初始化计划期间发生运行时剪枝,这种方法就不奏效了,这些子节点不会在 EXPLAIN 中显示。
在此,我们尝试改进这种输出方式:仅对那些自身被明确禁用的节点显示 “Disabled: true”。这似乎是大多数发表意见的人所期望的输出形式。实现方式是:累加当前节点所有子节点的禁用节点计数,若该总和小于当前节点的禁用节点计数,则说明当前节点自身被禁用。
此提交还修复了 make_sort() 函数中的一个 bug:该函数忽略了对 Sort 节点的禁用节点字段的设置。它本应参照cost_sort()
的做法进行设置,但一直未更新。在新的输出方式下,这种未正确维护该字段的做法显然是错误的,因为节点的禁用状态会被错误地归因于 Sort 节点的父节点。
讨论:https://postgr.es/m/9e4ad616bebb103ec2084bf6f724cfc739e7fabb.camel@cybertec.at
什么是优化器参数
PostgreSQL 包含了一系列设置,允许用户阻止或不鼓励规划器使用某些操作类型。这些设置以 “enable_” 开头,以操作名称结尾,例如,可以关闭enable_nestloop
,以不鼓励规划器在可能的情况下选择嵌套循环连接。
很多人第一次接触这些设置,是希望不鼓励使用顺序扫描,以检查 PostgreSQL 是否能够使用索引。在这种情况下,他们可以通过下面的命令,在会话中切换enable_seqscan
:
set enable_seqscan = off;
优化器参数的工作原理
其中一些设置的作用和你想象的一样,实际上会阻止规划器探索包含这些操作类型的计划路径。例如,将enable_indexonlyscan
设置为 off 确实会阻止仅索引扫描。
但是,对于在某些情况下可能是执行查询的唯一方式的操作类型,需要采用不同的方法。因此,将enable_seqscan
设置为 off 只是不鼓励使用顺序扫描,以便在没有索引的情况下,它仍然可以作为后备选项。在这种情况下,目前会给这类操作添加一个巨大的 “禁用成本”,即 1^10(100 亿),这样 PostgreSQL 基于成本的优化器极不可能选择这样的计划,除非没有其他选择。
下面是一个简单的例子,使用 PostgreSQL 17:
create table t (id bigint generated always as identity);
set enable_seqscan = off;
explain select * from t where id = 1;
QUERY PLAN
----------------------------------------------------------------------
Seq Scan on t (cost=10000000000.00..10000000025.00 rows=6 width=40)
Filter: (id = 1)
如你所见,尽管明确要求不使用顺序扫描,PostgreSQL 仍然选择了顺序扫描。这是因为我们的表没有索引,所以唯一的选择就是顺序扫描。成本统计数据确实显示,巨大的禁用成本(100 亿)被添加到了顺序扫描中,但由于没有其他选择,它仍然被选中。
虽然 1^10 是一个非常大的数字,但人们在 PostgreSQL 中执行的分析查询越来越多,这些查询的成本(即使是合理的)也可能变得极高。因此,即使存在替代计划,有时你最终可能会发现 PostgreSQL 选择了包含被禁用节点类型的计划!这远非理想情况,也不符合人们对该功能的期望。
社区有开发者报告了这个问题,另外还提到了禁用成本的方法,有一些其他不一致之处,比如对连接顺序的意外影响。
虽然一个选择可能是简单地增大这个数字,但社区 committer 认为这是把问题留给将来,开始探索其他的选择。
优化器参数的工作方式变更
在 PostgreSQL 18 中,将不再向任何被禁用的节点添加禁用成本,而是会为每个路径保留被禁用节点的计数。它会选择被禁用节点数量最少的路径,然后是成本最低的路径。
社区 committer 认为,理想的行为是:如果可以选择一个没有任何节点被禁用的计划,那么就选择成本最低的此类计划;如果不能,就选择总体成本最低的计划。
在 PostgreSQL 18 上测试我们的例子,得到以下查询计划:
create table t (id bigint generated always as identity);
set enable_seqscan = off;
explain select * from t where id = 1;
QUERY PLAN
---------------------------------------------------
Seq Scan on t (cost=0.00..38.25 rows=11 width=8)
Disabled: true
Filter: (id = 1)
我们可以看到,成本统计数据恢复正常,并且在顺序扫描节点上现在显示 “Disabled: true”(尽管enable_seqscan
被设置为 off,但还是选择了该节点)。
建议使用 SETTINGS 参数
建议大家尽可能多的使用 EXPLAIN 参数,包括 SETTINGS 参数。它会输出任何非默认的与规划器相关的设置,包括任何 “enable” 参数的值。
以我们的例子来说:
explain (settings) select * from t where id = 1;
QUERY PLAN
---------------------------------------------------
Seq Scan on t (cost=0.00..38.25 rows=11 width=8)
Disabled: true
Filter: (id = 1)
Settings: enable_seqscan = 'off'
这会显示任何从默认值更改过的参数,即使没有“被禁用”的节点。
总结
总之,PostgreSQL 不再为任何优化器参数添加成本,而是选择为每个计划路径保留被禁用节点的计数,并首先基于此计数确定计划选择的优先级,之后才基于成本。这应该能确保在可能的情况下,确实会避免使用那些操作的计划。
参考
提交日志:https://git.postgresql.org/pg/commitdiff/161320b4b