当一条 SQL 从毫秒级退化到秒级时,最常见的问题不是“它慢了”,而是“慢的时候执行计划是什么样”。手动复现常常受缓存、参数和并发影响,很难还原真实执行路径。PostgreSQL 自带的 auto_explain 模块可以解决这个痛点,它挂在执行器末端,查询一结束就根据耗时阈值决定是否把执行计划写入服务器日志,因此不会改变语句本身的行为。

一、auto_explain 的工作机制与适用边界
auto_explain 是 PostgreSQL 的一个 contrib 扩展,但它并不是像普通扩展那样用 CREATE EXTENSION 启用,而是需要注册到 shared_preload_libraries 中。它通过 ExecutorEnd 钩子介入查询执行流程。当一个查询执行结束时,钩子会计算从 ExecutorStart 到 ExecutorEnd 的耗时,并与 auto_explain.log_min_duration 进行比较。只要达到阈值,它就会调用计划输出接口,把当前查询的执行计划写入 PostgreSQL 日志。
这里的关键点在于,auto_explain 记录的是实际发生的执行计划,而不是通过 EXPLAIN 重新生成的估算计划。由于它可以配合 log_analyze 打开实际执行统计,因此日志中的 loops、actual time、rows 等字段能够准确反映当时的资源消耗。不过,这种真实信息是有代价的:启用 log_analyze 会让每条超过阈值的查询在原有执行成本之上再增加一次计划遍历和统计开销。SQL 本身不会返回不同结果,但会消耗额外时间,通常开销低于 10%,但在高并发 OLTP 环境中仍然需要谨慎评估。
另一个容易混淆的地方是 auto_explain 与 log_statement、log_duration 的关系。log_statement 只记录 SQL 文本,log_duration 只记录语句耗时,而 auto_explain 记录的是执行计划树。三者可以配合使用,例如用 log_statement 确认是哪些 SQL 变慢,再用 auto_explain 查看这些 SQL 的具体执行路径。需要明确的是,auto_explain 只对服务器日志有效,不会把计划写入 pg_stat_statements 等视图,也不会持久化到数据库表。如果需要长期保留或做聚合分析,必须借助外部日志采集系统。
二、参数配置与加载方式
使用 auto_explain 前,需要修改 postgresql.conf,将 auto_explain 加入 shared_preload_libraries。注意 shared_preload_libraries 只在服务器启动时读取,修改后必须重启 PostgreSQL 实例才能让模块加载。像 auto_explain.log_min_duration 这样的普通参数可以在会话级动态调整,但模块本身必须预加载。一个完整的配置示例如下:
# postgresql.conf 中启用 auto_explain shared_preload_libraries = 'auto_explain' # 超过 500ms 的查询记录执行计划 auto_explain.log_min_duration = '500ms' # 启用实际执行统计 auto_explain.log_analyze = on # 记录缓冲区命中情况 auto_explain.log_buffers = on # 记录每个节点的实际执行时间 auto_explain.log_timing = on # 输出更详细的表别名、列引用信息 auto_explain.log_verbose = on # 同时记录嵌套语句,例如函数内部的 SQL auto_explain.log_nested_statements = on # 输出格式可以是 text、json、xml、yaml auto_explain.log_format = text
如果暂时无法重启实例,也可以在单个会话中通过 LOAD 命令临时加载 auto_explain,然后设置相关参数。例如执行 LOAD 'auto_explain'; 后再执行 SET auto_explain.log_min_duration = '1s';。但这种方式只对当前会话有效,无法自动覆盖所有连接。生产环境还是建议写入配置文件并重启一次,保证所有后端进程都具备记录能力。
除了 log_min_duration,另一个重要参数是 auto_explain.log_level。默认是 LOG 级别,日志会进入 PostgreSQL 主日志文件。如果需要更高或更低级别,可以设置为 NOTICE、WARNING 或 ERROR。通常保持 LOG 即可,这样既能被日志系统捕获,又不会与真正的错误信息混淆。auto_explain.sample_rate 也值得关注,它接受 0 到 1 之间的浮点数,表示超过阈值的查询被记录的概率。例如设置 0.1 表示只记录 10% 的慢查询。这个参数在超大流量环境下很有用,可以避免日志暴增,但排查偶发慢查询时可能会漏掉关键样本,建议在问题定位阶段设为 1。
三、日志输出解读与常见分析思路
假设有一张订单表 orders,以及一个按用户分组的聚合查询。该查询耗时超过阈值后,日志会出现类似下面的执行计划片段:
2024-05-12 10:23:45.123 UTC [18543] LOG: duration: 1234.567 ms plan:
Query Text: select user_id, count(*) from orders where created_at > now() - interval '30 days' group by user_id limit 100;
Aggregate (cost=12345.67..12345.89 rows=100 width=16) (actual time=1234.123..1234.456 rows=100 loops=1)
Group Key: user_id
-> Seq Scan on orders (cost=0.00..11234.56 rows=500000 width=8) (actual time=0.012..1100.234 rows=500000 loops=1)
Filter: (created_at > (now() - '30 days'::interval))
Rows Removed by Filter: 250000
Buffers: shared hit=3210 read=67890
Planning Time: 0.345 ms
Execution Time: 1234.567 ms
在这段日志中,先看最外层节点 Aggregate 的 actual time,它显示聚合操作实际耗时约 1234 毫秒。继续向下看 Seq Scan on orders 节点,actual time 占了大头,并且 Buffers 字段里的 read 值很大,说明大量数据来自磁盘读取。Rows Removed by Filter 达到 25 万行,意味着 created_at 过滤条件并不高效。此时结合表结构和索引情况,通常会考虑在 created_at 上建立索引,或者改写查询避免全表扫描。
如果启用了 log_verbose,计划中还会显示更多 schema、列别名信息;如果 log_buffers 打开,每个节点都会带有 Buffers 信息。shared hit 表示从共享缓冲区命中的页面数,read 表示从磁盘读取的页面数。在高并发写库中,read 很高也可能与缓存命中率低有关,不一定只是索引缺失。结合 log_timing 的 actual time 可以更精确判断时间消耗发生在哪个节点。
日志中的 Planning Time 和 Execution Time 分别对应语句解析规划时间和执行器运行时间。如果 Planning Time 异常高,往往是分区表数量过多、继承关系复杂或查询使用了大量 OR 条件导致计划生成变慢,这时即使执行时间不高,也可能触发阈值。而 Execution Time 很高,则通常对应执行计划中的实际扫描、连接或排序环节,需要重点排查底层节点。
四、生产环境使用建议与性能控制
在 OLTP 生产环境开启 auto_explain 时,不建议一开始就把 log_analyze 打开,因为实际统计需要额外遍历计划并累计时间。可以先只设置 auto_explain.log_min_duration = '1s' 并关闭 log_analyze,这样记录的是估算计划,虽然缺少真实行数和耗时,但足以判断是否存在全表扫描、多表嵌套循环等结构问题。待确认日志增长和 CPU 影响可控后,再根据需要打开 log_analyze 和 log_buffers。
一种常见实践是采用两级阈值:数据库全局设置较宽松的阈值,例如 2 秒,用于自动记录明显慢的查询;当某个业务模块出现性能问题时,再通过会话级 SET 将阈值调到 200ms,只针对该连接采集更细粒度的计划。这种组合方式既能控制日志体量,又能在排查阶段获得足够数据。对于只读副本或低峰期,可以暂时降低阈值做全量采集,但采集完成后应恢复原值。
关于日志格式,text 格式适合人工排查,可读性好;JSON 格式更适合日志采集系统做结构化分析。例如使用 auto_explain.log_format = 'json' 时,每条记录会输出 JSON 对象,方便上报到 ELK、Loki 等平台。如果团队已经具备集中化日志平台,建议线上环境使用 JSON 格式,并通过字段过滤和聚合分析慢查询趋势。
还需要注意日志轮转和磁盘空间。auto_explain 输出的执行计划可能非常长,包含复杂连接、子查询和 CTE 的计划可能占据数百 KB 甚至数 MB。频繁记录慢查询会迅速占满日志目录。因此,应结合日志轮转策略,例如按大小或时间切割,并设置合理的保留周期。对于使用云托管 PostgreSQL 的用户,不同云厂商对 auto_explain 的暴露程度不同,有的允许直接修改参数组启用,有的则通过扩展开关控制,需要查看具体平台文档后再操作。
PostgreSQL auto_explain慢查询执行计划修改时间:2026-08-28 05:51:41