- 引言 — "我们不会加入提示"
- 为什么叫"建议"而不是提示
- 建议实际长什么样
- 告诉你建议有没有生效的反馈
- pg_stash_advice — 不改应用代码也能固定计划
- Wiki 上的六条反对理由,逐条打分
- 诚实的局限
- 设计背后的故事 — mechanism, not policy
- 那么,什么时候该用、什么时候不该用
- 结语
- 参考资料
引言 — "我们不会加入提示"
PostgreSQL 社区里长期悬着这样一句话。项目 Wiki 的 Optimizer hints discussion 页面开篇就是它 — 我们不打算像其他数据库里常见的那样去实现提示。
紧接着列出了六条理由,概括如下。
- 让应用代码的可维护性变差 — 把提示塞进查询里,日后就需要大规模重构。
- 妨碍升级 — 今天有用的提示,升级之后可能会拖累性能。
- 助长糟糕的 DBA 习惯 — 不去找真正的原因,而是贴一个提示了事。
- 不适应数据规模的变化 — 表还小的时候正确的提示,表变大之后就错了。
- 实际上会让性能变差 — 大多数情况下优化器是对的。
- 妨碍规划器本身的改进 — 用提示的人不会把问题报告给项目。
这份清单并不只是固执。它更像是把 Oracle 和 MySQL 里提示实际制造出的问题整理了一遍。但同一个页面又补了一句 — 如果有提案能避开其他提示系统里观察到的问题,我们是感兴趣的。
2026 年 6 月 4 日发布的 PostgreSQL 19 Beta 1 里,这个提案落地了。Robert Haas 编写的 pg_plan_advice 和 pg_stash_advice,两个 contrib 模块。发行说明的说法是"让用户能够稳定并控制规划器的决策"。
本文要看的正是这到底是什么,以及它在上述六条里抹掉了哪些、又没能抹掉哪些。顺带一提,19 的正式发布定在 2026 年 9〜10 月左右,眼下还是 beta。
为什么叫"建议"而不是提示
官方文档 F.30. pg_plan_advice 是这样描述这个模块的 — 用一门叫作"plan advice"的专用迷你语言,把规划器的主要决策描述出来、复现出来、改变掉。用意有两个:把用户判断为好的计划固定下来,以及试验一下规划器认为不是最优的那个计划。
叫 advice 而不是 hint,是有设计上的理由的。有三点和既有的提示不一样。
第一,建议活在 SQL 之外。 Oracle 的提示和 pg_hint_plan 会把提示放进查询文本内部(注释里)。pg_plan_advice 则通过一个叫 pg_plan_advice.advice 的 GUC 给出,或者通过 pg_stash_advice,以查询 ID 为键存进单独的仓库。应用的 SQL 字符串不会被碰。这个设计正对着 Wiki 反对理由第 1 条去的。
第二,它不是替代规划器,而是收窄搜索空间。 这是最重要的差别。照搬文档的原话 — 不管给出什么建议,对那条查询而言,出来的只会是核心规划器本来就会考虑的计划。也就是说,建议不是"用这个计划"这种命令,更接近"从这些候选里挑,把那些排除掉"。规划器认为在语义上会给出错误结果的计划,不管建议怎么说都不会出现。
第三,可以只固定一部分。 可以把整段建议字符串整个塞进去,把整个计划钉死,但也可以只指定必要的那一小块,其余交给规划器。Christophe Pettus 在 Hints, Part 3: Advice, Not Orders(2026-04-21) 里指出,这种克制的用法有余地缓解 Wiki 反对理由第 2 条(妨碍升级)和第 4 条(数据规模)。不过他紧接着补了一句 — 这个工具只是允许克制,并不强制。
建议实际长什么样
抽象地讲没有实感,来看实物。加载这个模块之后,EXPLAIN 会多出一个 PLAN_ADVICE 选项,把规划器当下选中的计划,写回成一段建议字符串。
EXPLAIN (COSTS OFF, PLAN_ADVICE)
SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id;
文档里给出的输出是这样的。
Generated Plan Advice:
JOIN_ORDER(f d)
HASH_JOIN(d)
SEQ_SCAN(f d)
NO_GATHER(f d)
读法很直观 — 按这个顺序连接 f 和 d,把 d 放在内侧做哈希连接,两边都用顺序扫描,没有用并行 Gather。把这段字符串原样喂回去,就能复现同一个计划。
SET pg_plan_advice.advice = 'JOIN_ORDER(f d)';
建议语言的词汇是刻意收窄的。文档定义的标签大致分四类。
- 扫描方式:
SEQ_SCAN(target),TID_SCAN(target),INDEX_SCAN(target index_name),INDEX_ONLY_SCAN(target index_name),BITMAP_HEAP_SCAN(target),DO_NOT_SCAN(target) - 连接顺序:
JOIN_ORDER(...)— 用括号括起来可以强制或松散地约束连接顺序 - 连接方式:
HASH_JOIN(...),MERGE_JOIN_PLAIN(...),MERGE_JOIN_MATERIALIZE(...),NESTED_LOOP_PLAIN(...),NESTED_LOOP_MEMOIZE(...),NESTED_LOOP_MATERIALIZE(...) - 并行化:
GATHER(...),GATHER_MERGE(...),NO_GATHER(target)
再加上分区级连接(PARTITIONWISE)和半连接唯一性(SEMIJOIN_UNIQUE / SEMIJOIN_NON_UNIQUE)的控制。目标是按别名指定的,同一个别名出现多次时会像 f#2 这样带上出现序号,还有能指到分区或子计划的扩展语法。
Daniel Westermann 的实操记录(2026-03-13)很好地展示了这个流程。把 100 万行的 t1 和 200 万行的 t2 连接起来,规划器会吐出这样的建议。
JOIN_ORDER(t2 t1) NESTED_LOOP_MEMOIZE(t1) SEQ_SCAN(t2) INDEX_SCAN(t1 public.t1_pkey) NO_GATHER(t1 t2)
原样喂回去,全部标记为 matched,计划也保持不变。反过来只把连接顺序倒过来,给出 JOIN_ORDER(t1 t2),计划就变成了 Merge Join + Sort + Materialize。这是确认建议确实生效的最短实验。
告诉你建议有没有生效的反馈
这一点是我个人在这套设计里最喜欢的部分。提示系统的老毛病是"用了提示,感觉好像被忽略了,但没法确认"。pg_plan_advice 会在每一行建议后面用注释附上判定结果。
INDEX_SCAN(f no_such_index) /* matched, inapplicable, failed */
HASH_JOIN(f) /* matched */
HASH_JOIN(g) /* not matched */
JOIN_ORDER(f g) /* partially matched */
按文档,判定词汇一共六种 — matched(目标在规划过程中被看到过,且可以应用)、partially matched(只看到了一部分,或没能一起看到)、not matched(目标完全没被看到)、inapplicable(比如索引不存在,标签根本无法应用)、conflicting(两条建议互相矛盾)、failed(某条建议已经 matched,但最终计划没有遵循它)。
打开 pg_plan_advice.feedback_warnings 后,建议没有被强制执行时会弹出警告。默认是关闭的。这为什么重要 — 建议会随时间腐坏(模式在变、索引会消失、数据分布会漂移),腐坏的建议被悄悄忽略,和吵吵嚷嚷地报警,在运维层面完全是两回事。
pg_stash_advice — 不改应用代码也能固定计划
pg_plan_advice 是以会话 GUC 的形式给出建议的。拿来做实验没问题,但生产环境里不可能为了固定一条问题查询,就在每个会话里都执行一遍 SET。所以有了 F.33. pg_stash_advice。
它的机制很简单。把查询 ID 映射到建议字符串(文档里叫 advice stash)放进动态共享内存,只要正在规划的查询 ID 出现在这个仓库里,对应的建议就会被自动应用。Shaun Thomas 在 pgEdge 的文章(2026-06-05)里给出的用法是这样的。
SELECT pg_create_advice_stash('production_tuning');
SELECT pg_set_stashed_advice('production_tuning', 5424487836266966148,
'INDEX_SCAN(f idx_fact_dim_id) NESTED_LOOP_PLAIN(f)');
SET pg_stash_advice.stash_name = 'production_tuning';
pg_stash_advice.stash_name 也可以通过 ALTER DATABASE ... SET 或 ALTER ROLE ... SET 来设置,也就是说,实际上可以做到一行应用代码都不改,就把某条查询的计划钉死。这和 SQL Server 的 plan guide、DB2 的计划固定属于同一类运维手法。
有几个前提条件。得放进 shared_preload_libraries 并重启,compute_query_id 也得打开(设成 auto 的话,模块加载时会自动打开)。默认情况下,仓库会以 30 秒为间隔持久化到数据目录下的 pg_stash_advice.tsv,这个间隔可以通过 pg_stash_advice.persist_interval 调整。
Wiki 上的六条反对理由,逐条打分
接下来是有意思的部分。看看上面这套设计,到底在多大程度上抹掉了项目自己写下的这些反对理由。
1. 应用可维护性 — 抹掉了。 建议活在 SQL 之外,查询文本不会被污染。这是明确的改进。
5. 大多数情况下优化器是对的 — 结构性地抹掉了。 建议只能在核心规划器会考虑的那个计划集合里挑选。规划器判断为"会给出错误结果"的计划,就算用建议也拉不出来。也就是说,最坏的情况是"计划慢",而不是"计划错"。这正是它和 Oracle 式提示的根本差别。
6. 妨碍规划器改进 — 部分抹掉了。 得益于反馈注释和 EXPLAIN (PLAN_ADVICE),"规划器选了什么、我强制了什么"会以机器可读的形式留下来。这是附到 bug 报告里的好格式。不过它依然拦不住那些贴上建议就悄悄了事的人。
2. 妨碍升级 / 4. 数据规模 — 只抹掉了一部分。 把建议控制到最低限度,剩下的部分规划器会继续自适应。但一旦把整个计划整段钉死,这两个问题原样又回来了。Pettus 的说法正是这个意思 — 完全固定计划,会把妨碍升级和扩展性的问题重新招回来,因为这个工具只是允许克制,并不强制克制。文档也说了同样的话 — 数据分布变化时规划器能够改变计划,通常是一个特性而不是 bug。
3. 糟糕的 DBA 习惯 — 完全没抹掉。 Pettus 那句话最诚实 — 与其修统计信息的问题,DBA 要是转而抓起 pg_plan_advice,得到的照样是同样糟糕的结果。而他把这归为"不是技术问题,而是文化问题"。这是工具解决不了的那类反对理由。
归纳一下,六条里有两条确凿无疑地被抹掉了,一条抹掉了大半,两条只有在用户克制使用时才会被抹掉,还有一条完全没抹掉。成绩不算差,但也谈不上全胜。
诚实的局限
文档自己承认的东西不少。这部分不能跳过。
控制不到的领域很大。 聚合到底走排序还是走哈希,决定不了。eager aggregation、partitionwise aggregation 这类策略也碰不了。UNION、INTERSECT 这类集合运算的行为同样不在控制范围内。按 depesz 整理的提交说明,这个功能瞄准的是连接方式、扫描类型这类结果,不会去动代价估算或选择度(selectivity)本身。
就算计划不变,代价也会跟着来。 pg_stash_advice 文档的措辞很明确 — 应用计划建议,即便没有改变计划,也会带来明显的(noticeable)性能代价。文档直接说了,只在需要时才用。pgEdge 的文章也解释说,因为建议会在中途打断规划器的循环,一点性能损耗是不可避免的。具体数字两边都没给,所以这里我不会凭空编一个。
安全模型还不成熟。 文档用的词是"unsophisticated"。创建、修改仓库只有超级用户或被授权的角色能做,但任何用户都可以在自己的会话里设置 pg_stash_advice.stash_name,这就意味着仓库内容可能暴露给没有权限的用户。建议字符串里带着索引名和表别名,因此成了一条模式信息泄露的路径。
会占用内存。 仓库放在动态共享内存里。文档给出的建议是,决定要塞多少条建议进去时,要把这一点考虑进去。
而且这是 1.0。 Haas 本人在自己的博客上(在它还没被提交的时候)这样写道 — 这非常明显是 1.0 版本的技术,局限不少,还有相当多的地方仍然相当粗糙。提交发生在 2026 年 3 月 12 日,并进了 beta,但没有理由认为那句自我评价说错了。
最后,depesz 的一句话最能概括这个工具的性格 — 强大,同时也是一把脚枪。意思是,需求一变,留在生产环境里的建议就可能造成伤害。
设计背后的故事 — mechanism, not policy
还有一点值得知道。Haas 刻意把这项工作做得很克制。用他自己的话说 — 我是刻意想把机制和策略分开的。pg_plan_advice 这个核心技术只提供最简单的生成建议的方式(也就是 EXPLAIN)和最简单的给出建议的方式(也就是 pg_plan_advice.advice 设置),但它是可插拔的。
这种分离之所以重要,是因为它下面铺的那层基础设施,可能比 pg_plan_advice 本身活得更久。据 pganalyze 整理,Haas 给 RelOptInfo 加了一个叫 pgs_mask 的位掩码,让扩展能在规划器最早的阶段就施加"这张表不要用顺序扫描"这类约束。既有的 pg_hint_plan 没有这个东西,所以不得不复制超过 2,500 行核心源码,还要操纵全局配置;pganalyze 观察到的是,站在这层基础设施上,那份拷贝的大部分都可以删掉。也就是说,就算你不喜欢 pg_plan_advice 本身,第三方的提示扩展也因此多了一条风险小得多的重新实现路径。
Haas 最初提议的三个模块里,pg_collect_advice 没有出现在 19 文档的 contrib 列表里。真正落地的是两个 — pg_plan_advice(F.30)和 pg_stash_advice(F.33)。
那么,什么时候该用、什么时候不该用
先照搬文档自己的警告 — 因为规划器通常会做出好的决策,推翻它的判断很容易适得其反。只有当约束规划器选择所带来的风险小于收益时,用计划建议才是合适的。
动手之前,先做这些事。
- 确认
ANALYZE有在正常跑,统计目标是否足够 - 如果列之间相关,先试试扩展统计信息(
CREATE STATISTICS) - 检查
random_page_cost、effective_cache_size这类代价参数是否和实际硬件相符 - 问自己一句,是不是想用强制连接方式,来掩盖缺索引带来的问题
把这四件事都做过一遍之后,规划器依然反复出错、而这个错误真的会让业务疼的那条特定查询 — 这才是 pg_plan_advice 该出场的地方。用 Pettus 的话说,它不是统计调优的替代品,而是针对那些躲不开的规划器误判的最后手段。
不该用的时候
- 19 现在还不是 GA。眼下是 beta,正式发布定在 2026 年 9〜10 月左右。现在还不到规划生产上线的时候。
- 问题出在统计信息而不是规划器本身的时候。大多数情况都属于这一类。
- 想固定的不是几条查询,而是几十条的时候。到了那个地步,真正出问题的是模式(schema)、统计信息或者硬件,不是这个工具。
- 数据分布持续变化的表。今天正确的建议,下个月就错了 — Wiki 反对理由第 4 条,这套设计同样没能抹掉。
- 团队里没有记录"谁在什么时候、为什么设了这条建议"的文化。没人认领的建议,几个月后就会变成没人敢碰的地雷。
要用的话,就这样用
建议尽量降到最低限度。这正是文档自己推荐的方式 — 把建议字符串精简到只控制必要的部分,只用在规划器误判风险高的地方。一旦把整个计划整段复制粘贴进去,Wiki 反对理由第 2 条和第 4 条就原样回来了。而且要打开 feedback_warnings,别让腐坏的建议悄无声息地死掉。
结语
把 pg_plan_advice 总结成"Postgres 终于接受提示了",有两处会说错。第一,这是约束,不是命令 — 因为它只会从规划器本来就会考虑的计划里挑,就算用错了,最坏的结果也是计划慢,而不是计划错。第二,这是机制,不是策略 — Haas 只打了一个最小的地基,其余交给了扩展。
对项目长期坚持的这份反对清单,这套设计给出的答案大概是过半。SQL 污染和"优化器通常是对的"被结构性地解决了;升级和扩展性问题只有在用户克制使用时才会缓解;糟糕的 DBA 习惯不是工具能解决的问题。这一半是真的改进,另一半仍然是你的责任 — 两句话都是真的。
等 19 正式 GA 之后,把统计信息和代价参数都调过、依然搞不定的那一条查询,用刚好够用的量去用它。这就是这个工具自己要求的用法。
参考资料
- PostgreSQL 19 Beta 1 Released! (2026-06-04)
- PostgreSQL 19 文档 — F.30. pg_plan_advice
- PostgreSQL 19 文档 — F.33. pg_stash_advice
- PostgreSQL Wiki — Optimizer hints discussion(六条反对理由的出处)
- Robert Haas — pg_plan_advice: Plan Stability and User Planner Control for PostgreSQL? (2026-03)
- Christophe Pettus — Hints, Part 3: Advice, Not Orders (2026-04-21)
- depesz — Waiting for PostgreSQL 19: Add pg_plan_advice contrib module (2026-03-22)
- Daniel Westermann (dbi services) — PostgreSQL 19: pg_plan_advice (2026-03-13)
- Shaun Thomas (pgEdge) — Looking Forward to Postgres 19: Query Hints (2026-06-05)
- pganalyze — Waiting for Postgres 19: Better Planner Hints with Path Generation Strategies
현재 단락 (1/96)
PostgreSQL 社区里长期悬着这样一句话。项目 Wiki 的 [Optimizer hints discussion](https://wiki.postgresql.org/wiki/Opti...