排查慢查询时,你跑了一条 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 Memory、Buffers、Worker 这些平台相关的字段占了大半视觉空间。
更麻烦的是:想看 ANALYZE 的实际执行时间,就必须带上 BUFFERS 和 MEMORY——Postgres 没有开关让你"只看时间,不看内存细节"。你无法说:给我计划结构,去掉实现细节。没有这个选项。
回归测试的噩梦
EXPLAIN 输出在不同版本、不同机器上会"漂移"。这对扩展开发者是实实在在的痛点。
平台依赖字段:一个简单的 Sort 节点,Peak Memory 在一台机器上是 45kB,换台机器可能变成 48kB。分配器不同、内存对齐不同,数值就不同。而 EXPLAIN 没有参数能抑制这个字段——Sort 节点在内存中执行时它总是被打印。Hash 节点也一样,Hash Batches、Original Memory、Peak 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: 45kB 或 Buffers: 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_workers、hide_memory、hide_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_prettier、prettier_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 的命名差异仍需手动处理或等待后续版本。
快速上手清单:
git clone仓库,用psql执行安装脚本- 回归测试中把
EXPLAIN ANALYZE替换为SELECT explain_prettier(...) - 事后分析时把原始 EXPLAIN 文本喂给
prettier_explain() - 需要更多细节时,调整
options参数中的隐藏标志 - 跨版本 SubPlan/InitPlan 差异暂需手动关注
EXPLAIN 是 Postgres 排查性能的利器,但利器不该被噪声包裹。给输出做一次瘦身,你看到的就是你真正需要看到的。