给 PostgreSQL 的 EXPLAIN 输出做一次"瘦身"

2026-05-26 15 预计阅读时间:1 分钟
来源:postgr.es AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:9 分钟

排查慢查询时,你跑了一条 EXPLAIN ANALYZE,终端瞬间吐出几十行密密麻麻的文本——内存分配、缓冲区命中、工作进程数、小数点后三位的行数估算……你真正关心的不过是:为什么这里走了 HashJoin 而不是 NestLoop?但所有噪声把关键信息淹没了。

Andrei Lepikhov 在阅读 Jimmy Angelakos 的《PostgreSQL Mistakes and How to Avoid Them》时,被同样的问题刺痛,于是写了 explain_prettier——一个纯 PL/pgSQL 实现的 EXPLAIN 后处理工具,用来精简和稳定查询计划输出。

信息过载:不是小问题

拿一个真实场景举例。同一条查询,坏计划用 HashJoin,好计划用 NestLoop,两个 EXPLAIN ANALYZE 输出并排摆在一起,每份都有几十行。你要找的差异可能只是某个节点的连接方式变了,但 Peak MemoryBuffersWorker 这些平台相关的字段占了大半视觉空间。

更麻烦的是:想看 ANALYZE 的实际执行时间,就必须带上 BUFFERSMEMORY——Postgres 没有开关让你"只看时间,不看内存细节"。你无法说:给我计划结构,去掉实现细节。没有这个选项。

回归测试的噩梦

EXPLAIN 输出在不同版本、不同机器上会"漂移"。这对扩展开发者是实实在在的痛点。

平台依赖字段:一个简单的 Sort 节点,Peak Memory 在一台机器上是 45kB,换台机器可能变成 48kB。分配器不同、内存对齐不同,数值就不同。而 EXPLAIN 没有参数能抑制这个字段——Sort 节点在内存中执行时它总是被打印。Hash 节点也一样,Hash BatchesOriginal MemoryPeak Memory 全是平台相关的,你躲不掉。

版本间命名变化:PG 16 及之前,InitPlan 显示带 InitPlan N 后缀,引用结果写成 $0;PG 17 去掉了后缀,引用变成了 InitPlan 1。SubPlan 也类似——后缀消失,参数引用格式变了,而且 PG 17 的 Filter 行从只显示 $1 变成了完整的表达式(包含操作符、ALL/ANY 关键字、具体列名)。

这些变化语义上更清晰了,但对测试是灾难:同一个查询在 PG 16 和 PG 17 上产生不同的文本输出。CI/CD 管线挂了——不是因为计划真的变了,而是字符串匹配被内部差异打断。

维护多套 expected 输出文件很快变成噩梦。

写文章和文档时的噪声

展示 BitmapScan 的优势时,读者不需要知道 Peak Memory: 45kBBuffers: shared hit=3。书页空间有限,你不得不缩小字体才能塞进一个中等复杂度的计划。

写优化前后对比时,真正重要的是计划结构怎么变了,而不是内存大小、缓冲区、代价估算这些在你机器上本来就不一样的数字。

你可以让 AI 生成"干净"的计划,但无法完全信任输出,也无法保证它按正确的 DBMS 版本和配置验证每条计划。

explain_prettier:两个函数解决两类场景

explain_prettier 是一个 PL/pgSQL 脚本,安装后提供两个核心函数:

函数 输入 主要用途
explain_prettier() 执行查询并后处理其 EXPLAIN 输出 测试稳定化
prettier_explain() 接收已有的 EXPLAIN 文本 事后排查,无需访问服务器或数据

默认行为:隐藏所有平台依赖数据——内存分配变成占位符,工作进程数变成占位符,行数估算的小数截断,实际时间取整。缓冲区、Hash 内存分配等实现细节全部移除。同时保留核心信息:计划结构(节点、连接、扫描方式)、过滤条件、行数估算。

Phantom 对象(SubPlan、InitPlan 等)的标准化尚未实现,但已在路线图上。

控制输出粒度

explain_prettier() 允许通过 options 参数自定义 EXPLAIN 参数和过滤行为。主要控制项:

  • hide_platform_dependent:隐藏所有平台依赖数据(默认开启)
  • hide_execution_details:隐藏每个节点的执行细节行(默认开启)
  • hide_workershide_memoryhide_buffers:分别隐藏特定细节(默认全部开启)

全部默认开启 = 最大精简和稳定。需要更多细节时,关掉对应标志即可。

实操:安装与使用

从 GitHub 获取脚本并安装到数据库:

# 克隆仓库
git clone https://github.com/lepykhov/pg_track_optimizer.git
cd pg_track_optimizer

# 将 explain_prettier 的 PL/pgSQL 脚本安装到目标数据库
psql -d your_database -f sql/explain_prettier.sql

安装后,数据库中新增 explain_prettierprettier_explain 以及两个辅助函数。

在回归测试中使用

-- 替代原来的 EXPLAIN ANALYZE,输出自动精简
SELECT explain_prettier('SELECT * FROM orders WHERE status = $1', 'ANALYZE, BUFFERS');

从 psql 直接复制精简计划

-- 对已有 EXPLAIN 文本做后处理
SELECT prettier_explain(
  'Seq Scan on orders  (cost=0.00..154.00 rows=1200 width=68)
   Filter: (status = ''shipped'')
   Rows Removed by Filter: 3800
   Peak Memory: 45kB
   Buffers: shared hit=12'
);

输出变成:

Seq Scan on orders  (cost=0.00..154 rows=1200)
  Filter: (status = 'shipped')
  Rows Removed by Filter: 3800

内存和缓冲区消失了,行数的小数位截断了,结构一目了然。

对比两个计划

-- 好计划
SELECT prettier_explain(:bad_plan_text) AS bad_plan,
       prettier_explain(:good_plan_text) AS good_plan;

两个精简输出并排比较,连接方式差异一眼可见。

自定义过滤粒度——比如保留代价估算但隐藏其他细节:

SELECT explain_prettier(
  'SELECT * FROM orders JOIN customers ON orders.cust_id = customers.id',
  'ANALYZE, COSTS ON, HIDE_PLATFORM_DEPENDENT ON, HIDE_MEMORY OFF'
);

社区现状:零散的临时方案

PostgreSQL 主干测试套件本身也大量部分掩码 EXPLAIN 输出。代码搜索能看到 explain_masking()hide_bufs()hide_mem() 等函数散落在各处测试中,都是临时方案。新功能开发者——尤其是扩展开发者——不得不反复发明自己的掩码函数,毫无系统性。

Lepikhov 的期望是:核心提供一个统一的掩码/后处理函数,让所有人不再各自造轮子。explain_prettier 是朝这个方向迈出的一步。

什么时候该用,什么时候不该用

场景 建议
扩展回归测试跨版本/跨平台 explain_prettier,消除噪声,稳定匹配
排查生产慢查询 先跑完整 EXPLAIN ANALYZE,确认问题后用 prettier_explain 精简输出做对比或分享
写技术文章/文档 用精简版展示计划结构,关键数字手动标注
需要精确内存或缓冲区数据调优 不要用精简模式,那些被隐藏的字段正是你要看的

一个边界:explain_prettier 目前不做 Phantom 对象标准化,所以 PG 16 和 PG 17 之间 SubPlan/InitPlan 的命名差异仍需手动处理或等待后续版本。

快速上手清单

  1. git clone 仓库,用 psql 执行安装脚本
  2. 回归测试中把 EXPLAIN ANALYZE 替换为 SELECT explain_prettier(...)
  3. 事后分析时把原始 EXPLAIN 文本喂给 prettier_explain()
  4. 需要更多细节时,调整 options 参数中的隐藏标志
  5. 跨版本 SubPlan/InitPlan 差异暂需手动关注

EXPLAIN 是 Postgres 排查性能的利器,但利器不该被噪声包裹。给输出做一次瘦身,你看到的就是你真正需要看到的。


相关推荐