Skip to content

auto_explain 自动执行计划 ​

慢查询日志回答了「哪条 SQL 慢」,但没有回答「为什么慢」。要回答「为什么」,必须看到当时那条语句使用的执行计划。人工的做法是拿到 SQL 后立刻 EXPLAIN ANALYZE 复现——但问题在于:慢查询往往是偶发的,复现时数据分布、缓存状态、统计信息都可能已经变化,手工 EXPLAIN 看到的计划未必是案发时的计划。

auto_explain 就是为这个缺口准备的内核自带扩展:它像一个常驻的「执行计划行车记录仪」,当某条语句的执行时间超过阈值时,自动把该语句当时的真实执行计划写入日志。它解决了三个手工 EXPLAIN 无法解决的问题:偶发慢查询难以复现、生产环境不能随便跑 ANALYZE(副作用真实执行)、以及计划劣化(plan regression)需要「案发当时」的证据。

本页讲清楚它的加载方式、全部配置参数(官方一共十几个 GUC,逐一说明默认值与用途)、log_analyze 的性能陷阱、JSON 格式如何对接日志采集器,以及它与 pg_stat_statements 联动的完整定位工作流。学完本页,你应当能为任何一套生产库设计「超 3 秒的语句自动留下执行计划」的监控方案。

阅读本页时请对照上一页《慢查询日志》:auto_explain 的阈值语义、日志载体、采集链路与之完全同构,差异只在「多记了一份计划」。把这个同构关系记牢,两个工具的配置与运维就变成一套肌肉记忆。

基础概念 ​

它在慢查询工具链中的位置 ​

把上一页的三层工具表补全,auto_explain 属于「现场层」里最深的一档:

工具记录内容丢失的信息
log_min_duration_statement语句耗时 + 文本为什么慢(没有计划)
auto_explain语句耗时 + 文本 + 当时的执行计划计划之外的等待事件(锁、IO)
pg_stat_statements每类 SQL 的累计统计单次执行的参数、计划
pg_stat_activity当下正在发生什么历史信息

auto_explain 的独特价值在于「计划快照」:它记录的是该语句实际使用的计划,而不是复现时推算的计划。执行计划是优化器根据统计信息、参数值、成本参数现场计算的,同一条 SQL 在不同时间可能生成不同计划——这正是「昨天还快今天变慢」的最常见原因,也是 auto_explain 不可替代的原因。

它由 Takahiro Itagaki 编写并自 PostgreSQL 9.0 起进入内核发行版,是最老牌的 contrib 模块之一——十余年生产验证意味着它的行为边界(官方文档一页就讲完的全部参数)非常清楚,不存在「社区扩展版本碎片化」的问题。

模块形态与加载方式 ​

auto_explain 是 contrib 扩展,但与 pg_stat_statements 不同——它不提供任何 SQL 接口(没有函数、没有视图),纯粹是一个「加载进后端进程就开始工作」的模块。官方文档的表述是:它不提供 SQL 可访问的函数,使用方式就是把它加载进服务端。

三种加载方式,适用场景不同:

方式生效范围是否需要重启典型场景
shared_preload_libraries所有会话是(需重启)生产库常态监控
session_preload_libraries所有会话(每会话连接时加载)否(reload 生效)免重启的常态监控(PG14+ 语义良好)
LOAD 'auto_explain'仅当前会话否临时诊断、复现问题

LOAD 方式需要 superuser 权限。官方文档的典型示例用的正是 preload 方案:

conf
# postgresql.conf —— 官方示例风格的最小可用配置
session_preload_libraries = 'auto_explain'

auto_explain.log_min_duration = '3s'

注意官方强调的一点:默认行为是什么都不做。模块加载后必须至少设置 auto_explain.log_min_duration,否则没有任何输出——排障时「明明加载了却看不到计划」的第一检查项就是它。

与手工 EXPLAIN 的对照 ​

维度手工 EXPLAIN (ANALYZE, BUFFERS)auto_explain
计划来源现场重新规划语句实际使用的计划
副作用真实执行一遍(写场景危险)无额外执行(只是观察原执行)
时机事后复现案发当时
覆盖面逐条人工全量自动(可采样)
依赖无需要加载模块

一句话总结:手工 EXPLAIN 是「案情重演」,auto_explain 是「行车记录仪」。两者的输出格式完全一致(都是 EXPLAIN 的输出),因此本站《EXPLAIN ANALYZE》一页的所有解读技巧在 auto_explain 日志上同样适用。

同一条 SQL 的计划为什么会变 ​

理解 auto_explain 的价值,先要理解计划的「易变性」。优化器为每条语句现场计算计划,输入包括:

输入变化来源对计划的典型影响
统计信息ANALYZE 定期刷新、数据写入漂移估算行数变化 → 算子选择翻转
绑定参数每次执行的参数值不同选择性差异 → 索引扫描/顺序扫描切换
缓存与内存状态共享缓冲命中率、work_mem 余量并行度、溢出盘与否
表物理形态膨胀、页顺序、索引碎片I/O 估算失真
会话与全局参数SET、参数组变更成本模型整体偏移

其中「绑定参数」最隐蔽:预编译语句的计划在规划时可能看不到参数值(generic plan),而首次执行用的是 custom plan,两者可能走完全不同的索引。这就是「测试环境飞快、生产偶发超慢」的常见根因。auto_explain 记录的正是「某次真实参数下的真实计划」,是唯一能坐实这类问题的证据源。

语法与用法 ​

安装与预加载 ​

auto_explain 随 PostgreSQL 发行,无需单独编译安装。配置预加载:

conf
# 方案 A:shared_preload_libraries(经典方案,需重启)
# 若已有其他库,用逗号追加
shared_preload_libraries = 'pg_stat_statements,auto_explain'

# 方案 B:session_preload_libraries(免重启)
session_preload_libraries = 'auto_explain'
bash
# 修改 shared_preload_libraries 后需要重启
$ systemctl restart postgresql

# 验证已加载
$ psql -c "SHOW shared_preload_libraries;"
$ psql -c "SELECT * FROM pg_available_extensions WHERE name = 'auto_explain';"

两种 preload 的取舍:shared_preload_libraries 在服务启动时加载一次、所有后端共享,开销最小,但改配置要重启;session_preload_libraries 在每个后端建立时加载,开销略高(仅一次 dlopen),但可以 reload 生效、适合「临时开启一段时间再关」的运维节奏。生产库如果决定长期开启,用 A;如果只是阶段性排查,用 B。

单会话加载适合排障现场:

sql
-- superuser 会话内临时加载并调参,只影响当前会话
LOAD 'auto_explain';
SET auto_explain.log_min_duration = 0;
SET auto_explain.log_analyze = on;
-- ...执行待观察的语句...

核心参数速查表 ​

官方参数全集如下(均为 superuser 级别可调):

参数默认值作用
auto_explain.log_min_duration-1记录计划的耗时阈值(ms),0 全记,-1 关闭
auto_explain.log_analyzeoff输出 EXPLAIN ANALYZE 级别信息(实际行数、循环次数、耗时)
auto_explain.log_verboseoff输出 EXPLAIN VERBOSE 级别信息(列/表达式细节)
auto_explain.log_buffersoff输出缓冲区使用统计(需 log_analyze)
auto_explain.log_waloff输出 WAL 使用统计(需 log_analyze)
auto_explain.log_timingon每个计划节点的时间统计(需 log_analyze,关闭可大幅降开销)
auto_explain.log_triggersoff输出触发器执行统计(需 log_analyze)
auto_explain.log_settingsoff输出影响规划的、非默认值的配置项
auto_explain.log_formattext计划输出格式:text / xml / json / yaml
auto_explain.log_levelLOG计划写入日志时的日志级别(DEBUG5~LOG、INFO、NOTICE、WARNING)
auto_explain.log_nested_statementsoff记录函数内部嵌套语句的计划
auto_explain.log_parameter_max_length-1语句文本中绑定参数值的截断长度(字节)
auto_explain.sample_rate1.0会话内语句的采样比例
auto_explain.max_plan_depth30计划树打印的最大深度(超深层不打印)

记住三个「强制搭配」:log_buffers / log_wal / log_timing / log_triggers 都只在 log_analyze 开启时生效;而 log_analyze 开启有全局性能代价(后文详述)。

推荐的起步配置 ​

conf
# postgresql.conf —— 生产库推荐起步配置
shared_preload_libraries = 'auto_explain'

# 超过 3 秒的语句记录执行计划
auto_explain.log_min_duration = '3s'

# 记录实际行数与节点耗时(注意开销,见下文)
auto_explain.log_analyze = on
auto_explain.log_buffers = on

# JSON 格式便于采集器解析
auto_explain.log_format = 'json'

log_min_duration 的取值语义与 log_min_duration_statement 完全一致:正数毫秒阈值、0 全量、-1 关闭;支持 '3s' 这类带单位写法。阈值可以(也建议)设置得高于慢查询日志的阈值——计划比一行 duration 贵得多,只给真正严重的语句留计划。

上线验证清单 ​

配置完成后,按这张清单逐项验证(顺序即排障顺序):

bash
# 1. 模块已加载(两种 preload 任一可见即可)
psql -c "SHOW shared_preload_libraries;"
psql -c "SHOW session_preload_libraries;"

# 2. 参数已下发且来源正确(source 应为 configuration file)
psql -c "SELECT name, setting, source FROM pg_settings WHERE name LIKE 'auto_explain%';"
sql
-- 3. 制造一条超阈值语句,确认日志产出
SELECT pg_sleep(4);
text
LOG:  duration: 4003.512 ms  plan:
  Query Text: SELECT pg_sleep(4);
  Result  (actual rows=1.00 loops=1)
bash
# 4. 确认日志文件在增长且格式符合预期
$ tail -f /var/lib/postgresql/log/postgresql-*.log

四步全绿才算部署完成。第 2 步的 source 列特别重要:如果显示 session 或 override,说明有更高优先级的配置在起作用(可能是云平台参数组或 ALTER SYSTEM 残留)。

观察输出 ​

开启后,超过阈值的语句会在服务器日志中产生这样的条目(官方文档示例):

text
LOG:  duration: 3.651 ms plan:
  Query Text: SELECT count(*)
         FROM pg_class, pg_index
         WHERE oid = indrelid AND indisunique;
  Aggregate  (cost=16.79..16.80 rows=1 width=0) (actual time=3.626..3.627 rows=1.00 loops=1)
    ->  Hash Join  (cost=4.17..16.55 rows=92 width=0) (actual time=3.349..3.594 rows=92.00 loops=1)
          Hash Cond: (pg_class.oid = pg_index.indrelid)
          ->  Seq Scan on pg_class  (cost=0.00..9.55 rows=255 width=4) (actual time=0.016..0.140 rows=255.00 loops=1)
          ->  Hash  (cost=3.02..3.02 rows=92 width=4) (actual time=3.238..3.238 rows=92.00 loops=1)
                Buckets: 1024  Batches: 1  Memory Usage: 4kB
                ->  Seq Scan on pg_index  (cost=0.00..3.02 rows=92 width=4) (actual time=0.008..3.187 rows=92.00 loops=1)
                      Filter: indisunique

结构解读:

  • duration: 3.651 ms plan:——与慢查询日志同源的耗时前缀,后跟 plan: 标记。
  • Query Text: ...——语句原文。
  • 以下是标准 EXPLAIN 输出。有 actual time=... 说明 log_analyze 已开启;有 Buckets/Batches 说明节点带缓冲区细节。

详细说明 ​

log_min_duration 与两级日志的关系 ​

auto_explain.log_min_duration 与 log_min_duration_statement 相互独立、各管各的。同一条语句可能同时触发两者(一条 duration 行 + 一条 plan 行),也可能只触发其一。设计两级阈值是标准做法:

conf
# 一级:1 秒的语句记耗时(用于统计与趋势)
log_min_duration_statement = 1000

# 二级:3 秒的语句额外记计划(用于确诊)
auto_explain.log_min_duration = '3s'

这样做的理由:计划输出动辄几十上百行,只给 1% 最严重的语句留计划,日志量与可读性都可控。个别团队反过来用 auto_explain 完全替代慢查询日志(log_format = json 后两者格式统一),代价是所有慢语句都带计划、日志体积膨胀数倍——是否接受取决于日志管道的处理能力。

log_analyze 的性能陷阱(必读) ​

这是 auto_explain 最重要的一页警告,官方原文的措辞是「extremely negative impact on performance(对性能可能有极严重的负面影响)」:

当 log_analyze 开启时,所有被执行的语句都会进行逐计划节点计时——无论它们最终是否达到阈值、是否会被记录。

换句话说,计时开销不是「慢查询的开销」而是「全库所有查询的开销」。逐节点计时需要在每个算子进出时读取系统时钟,在某些系统上时钟读取本身就很贵。由此得出两条铁律:

铁律一:生产库长期开启时,必须关闭 log_timing。

conf
# 生产长期开启的安全形态:要行数不要逐节点计时
auto_explain.log_analyze = on
auto_explain.log_timing = off
auto_explain.log_buffers = on

log_timing = off 后计划里没有每个节点的 actual time,但仍有 rows、loops 与整条语句的总耗时——对「判断估算偏差、发现行数失控」这一主要用途完全够用,而计时开销消失。官方文档明确说明这是降低性能代价的推荐手段。

铁律二:需要完整逐节点耗时时,改用限时诊断窗口。

conf
# 诊断窗口(分钟级):完整信息
auto_explain.log_analyze = on
auto_explain.log_timing = on    # 窗口结束后立即关回

与上一页「全量诊断窗口」的 runbook 一样:开窗 → 复现 → 关窗 → 分析。

log_buffers 与 log_wal:从「慢」到「贵」 ​

log_analyze 开启后,这两个开关把计划从「时间维度」扩展到「资源维度」:

  • log_buffers:等价于 EXPLAIN (BUFFERS)。顶层汇总 shared hit/read/written、temp read/written 等块数。它回答的是「这次执行到底碰了多少页」——temp 高说明排序/哈希溢出到磁盘(work_mem 不足),shared read 高说明缓存未命中(内存不足或顺序扫大表)。
  • log_wal:等价于 EXPLAIN (WAL)。记录该语句产生的 WAL 量(records、bytes、fpi 页数)。它回答「这次执行写了多少 WAL」——批量 UPDATE 的 WAL 放大、检查点压力都从这里看。

这两个统计的采集成本远低于逐节点计时(块计数本来就在缓冲区管理中发生),可以放心长期开启。

log_format:选 text 还是 json ​

auto_explain.log_format 支持四种格式:text(默认)、json、xml、yaml。选择标准只有一个——谁来读这份日志:

消费方推荐格式理由
人肉阅读、邮件告警摘录text与 EXPLAIN 手册、各类文档对照最直观
日志采集器(JSON 管道)json与 jsonlog 无缝配合,结构化解析
既有 XML 工具链xml少见,兼容性需求
YAML 工具链yaml少见,兼容性需求

对接采集器时,json 格式的计划是一棵标准的嵌套 JSON 树(Plan 节点内嵌 Plans 数组),采集端可以直接按节点做统计与告警,例如「发现某模板语句出现 Seq Scan 节点即告警」。这正是官方把 json 列为一等格式的原因。

sample_rate:高并发下的采样 ​

auto_explain.sample_rate(默认 1.0)控制每个会话内多大比例的语句参与「计划记录候选」。它的定位与 log_statement_sample_rate 不同:前者按会话抽样语句,后者按语句抽样。当慢查询日志显示某模板语句每分钟触发数百次、计划几乎相同时:

conf
# 只记录 10% 会话的语句计划
auto_explain.sample_rate = 0.1

官方说明:对于嵌套语句,要么整段都参与采样、要么整段都不参与,不会出现半个函数被采样的情况。

max_plan_depth 与深计划 ​

PG16 引入 auto_explain.max_plan_depth(默认 30),限制打印的计划树深度。超深计划(大量嵌套子查询、分区裁剪后的超多分区计划)可能产出数万行日志,压垮采集管道。当 pgbadger 或采集端报出「单条日志过大」时,降低该值即可截断;日常 30 的默认值对绝大多数业务计划绰绰有余。

log_nested_statements:函数内部慢在哪 ​

PL/pgSQL 函数中的嵌套语句默认不产生独立的计划记录(整条函数调用算一条语句)。排查「函数整体慢、不知道哪一句慢」时:

conf
auto_explain.log_nested_statements = on

开启后,函数内每条语句的计划都会作为附属输出记录在调用语句的日志条目之下。注意它与 sample_rate 的交互:官方说明嵌套语句要么全采要么全不采,因此开启此参数时建议把 sample_rate 保持为 1。

log_verbose 与 log_settings 的用途 ​

两个低开销、按需开启的辅助开关:

  • log_verbose(默认 off):等价于 EXPLAIN VERBOSE,在每个节点上输出输出列与表达式的细节。排查「这个排序排的是什么列」「这个 Filter 条件被重写成什么样」时有用;代价是每行计划变长,日志体积明显增大,不建议长期开启。
  • log_settings(默认 off):在计划头部列出与内置默认值不同、且影响查询规划的配置项。它专治「同一份 SQL 在两套环境计划不同」的疑难杂症——把日志头一翻,work_mem、random_page_cost、effective_cache_size、max_parallel_workers_per_gather 的当前值一目了然,省去逐台登机检查的功夫。
text
LOG:  duration: 4218.992 ms  plan:
  Query Text: ...
  Settings: effective_cache_size = '2GB', random_page_cost = '4', work_mem = '8MB'
  ...

log_level:控制计划写到哪里去 ​

auto_explain.log_level(默认 LOG)决定计划以什么日志级别写出。合法取值从 DEBUG5 到 WARNING(以及 LOG)。用途有两类:其一,把计划级别调高到 WARNING,让只收集 WARNING 以上级别的采集管道也能带上计划;其二,调低到 DEBUG 系列,让计划只在 debug 排障会话中出现,避免进入常规日志流。绝大多数场景保持默认 LOG 即可,与 log_min_messages 的默认配置兼容。

与 pg_stat_statements 的配合:定位计划劣化 ​

两者组合是排查「计划劣化」的标准工作流。场景:某接口昨天 50ms、今天 3s,怀疑计划变了:

  1. pg_stat_statements 圈嫌疑:按 mean_exec_time 环比找出劣化最明显的语句模板,拿到它的 queryid 与语句文本(见《pg_stat_statements 语句统计》一页)。
  2. auto_explain 拿证据:确认 auto_explain.log_min_duration 低于该语句当前的均值(保证会命中),等它再次被记录——日志里就是案发时的真实计划。
  3. 对比历史计划:从变更管理记录或上次诊断窗口的日志里找出「快的时候」的计划,diff 两份计划的差异(通常是 Index Scan 变 Seq Scan、Nested Loop 变 Hash Join、估算行数与实际行数偏差悬殊)。
  4. 归因修复:估算偏差 → ANALYZE 表或调整统计目标;索引失效 → 检查参数类型、表达式写法;随机参数劣化 → 考虑 plan_cache_mode 或查询改写。

没有 auto_explain,第 2 步只能手工复现;而偶发劣化恰恰是最难复现的。

JSON 计划树的结构剖析 ​

log_format = 'json' 时,计划以标准的嵌套 JSON 输出。理解它的骨架,采集端与自动化脚本才有据可依:

text
{
  "Query Text": "SELECT ...",
  "Plan": {
    "Node Type": "Aggregate",
    "Actual Rows": 1,
    "Actual Loops": 1,
    "Plans": [
      {
        "Node Type": "Seq Scan",
        "Parent Relationship": "Outer",
        "Relation Name": "orders",
        "Actual Rows": 1000000,
        "Actual Loops": 1,
        "Plans": []
      }
    ]
  }
}

字段与 text 格式的对应关系一目了然:Node Type 即算子类型、Actual Rows 对应 text 输出的 actual ... rows=、子节点收纳在 Plans 数组中。基于这个结构可以做的自动化:递归遍历 Plans 统计节点类型分布;计算每个节点 Plan Rows 与 Actual Rows 的偏差率;检测 Node Type = "Seq Scan" 且 Relation Name 属于大表的组合并触发告警。注意字段的精确命名以所用版本的实际输出为准,不同大版本之间偶有增补。

log_triggers 详解 ​

log_triggers(需 log_analyze)把触发器的执行统计纳入计划输出:每个触发器的名称、关系与耗时单独成行。触发器热点表的排查价值很大——业务上「这条 UPDATE 慢」,真相可能是 AFTER 触发器里的 DML 或审计逻辑占了大头:

text
LOG:  duration: 812.441 ms  plan:
  ...
  Trigger for constraint fk_check: time=1.204 calls=1
  Trigger audit_log_trigger: time=796.512 calls=1

没有这个开关时,触发器耗时被合并进语句总耗时,主查询的计划看起来平平无奇,排查会兜一个大圈子。对触发器密集的表(审计、级联汇总场景)建议常态化开启;它同样要求 log_analyze = on,因此遵循与 log_buffers 相同的开销模型。

JSON 格式对接采集器 ​

log_format = 'json' 与 PG15+ 的 log_destination = 'jsonlog' 组合时,整条日志本身就是 JSON,计划作为字段值嵌入。采集链路:

text
PostgreSQL (jsonlog + auto_explain json)
  → Filebeat / Vector
  → Loki / Elasticsearch
  → Grafana / Kibana 看板

在采集端可以做的自动化包括:解析 Plan 树提取 Node Type 分布、对比 Plan Rows 与 Actual Rows 的偏差率(超过 10 倍标红)、按模板聚合计划变更事件。计划劣化的发现从「人看日志」升级为「机器盯偏差」。

采样与限流的组合拳 ​

把本页所有「量控」参数组合起来,形成一套完整的输出预算。目标:每天计划日志不超过 200MB。

conf
# 组合方案示例
auto_explain.log_min_duration = '3s'   # 只有 3s 以上才有资格
auto_explain.sample_rate = 0.25        # 再砍掉 3/4
auto_explain.log_timing = off          # 不逐节点计时(省性能)
auto_explain.max_plan_depth = 30       # 截断超深计划
auto_explain.log_format = 'json'       # 可压缩、可解析

估算方法:观察慢查询日志中「3 秒以上语句」的每日条数 N,计划日志大小 ≈ N × 采样率 × 单条计划体积(text 约 2~10KB,JSON 约为其 3~5 倍)。数据驱动地调整 sample_rate,而不是拍脑袋。日志侧再叠加轮转与压缩(见《日志轮转与归档》),预算闭环。

实战示例 ​

示例一:捕获 3 秒以上的语句计划 ​

完整走一遍从配置到拿到计划的流程。

bash
# 1. 配置预加载(重启生效)
$ sudo -u postgres vi $PGDATA/postgresql.conf
#   session_preload_libraries = 'auto_explain'
#   auto_explain.log_min_duration = '3s'
#   auto_explain.log_analyze = on
#   auto_explain.log_timing = off
#   auto_explain.log_buffers = on
$ sudo systemctl reload postgresql

# 2. 验证参数已生效
$ psql -c "SHOW session_preload_libraries;"
sql
-- 3. 制造一条超过 3 秒的语句(大表模拟)
SELECT count(*) FROM big_table t1, big_table t2
WHERE t1.id = t2.id AND t1.payload LIKE '%x%';
text
LOG:  duration: 4218.992 ms  plan:
  Query Text: SELECT count(*) FROM big_table t1, big_table t2
  WHERE t1.id = t2.id AND t1.payload LIKE '%x%';
  Aggregate (actual rows=1.00 loops=1)
    ->  Hash Join (actual rows=1000000.00 loops=1)
          Hash Cond: (t1.id = t2.id)
          ->  Seq Scan on big_table t1 (actual rows=1000000.00 loops=1)
                Filter: (payload ~~ '%x%'::text)
                Rows Removed by Filter: 9000000
          ->  Seq Scan on big_table t2 (actual rows=10000000.00 loops=1)

即使没有逐节点计时(log_timing = off),rows 与 Rows Removed by Filter 也足以定位问题:两次全表扫描 + 过滤器丢弃了 900 万行。

示例二:会话内临时抓计划(零配置改动) ​

不能改配置文件的环境(如受限的云托管库、紧急排障),用会话级方案:

sql
-- superuser 会话
LOAD 'auto_explain';
SET auto_explain.log_min_duration = '1s';
SET auto_explain.log_analyze = on;

-- 在同一会话中执行业务代码路径(例如调用存储过程)
CALL process_pending_orders();

-- 用完关闭
SET auto_explain.log_min_duration = -1;

LOAD 只影响当前会话,log_min_duration 的 SET 也只影响当前会话——这是把影响面降到最小的排查方式。非 superuser 用户不能 LOAD,需要管理员协助或使用 session_preload_libraries 方案。

示例三:JSON 格式对接采集 ​

conf
# PostgreSQL 侧
log_destination = 'jsonlog'
logging_collector = on
auto_explain.log_format = 'json'
auto_explain.log_min_duration = '2s'
text
{"timestamp":"2026-10-07 15:02:11.301 CST","pid":40211,"error_severity":"LOG",
 "message":"duration: 2318.442 ms  plan: {\"Query Text\":\"SELECT ...\",\"Plan\":{\"Node Type\":\"Aggregate\",...}}",
 "query":"..."}
bash
# 采集侧:抽出计划 JSON 交给 jq 分析节点类型
jq -r '.message' postgresql.json \
  | sed 's/^duration: [0-9.]* ms  plan: //' \
  | jq -r '[.. | objects | .["Node Type"]?] | map(select(. != null)) | join(",")'

拿到每条慢语句的节点类型序列后,就能在告警规则里写「该模板语句出现 Seq Scan」这类结构化条件。

示例四:计划劣化的完整排查 ​

sql
-- 第 1 步:pg_stat_statements 环比找劣化语句(详见下一页)
SELECT queryid,
       calls,
       round(mean_exec_time::numeric, 1) AS mean_ms,
       round(total_exec_time::numeric / 1000, 1) AS total_s
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 5;
bash
# 第 2 步:确认 auto_explain 阈值能覆盖该语句
psql -c "SHOW auto_explain.log_min_duration;"
# 若当前均值为 3s,阈值 3s 只有一半命中,临时下调到 1s:
psql -c "ALTER SYSTEM SET auto_explain.log_min_duration = '1s';"
psql -c "SELECT pg_reload_conf();"
text
# 第 3 步:等命中后从日志拿计划,与上周同语句的计划 diff
LOG: duration: 2874.112 ms plan:
  Query Text: SELECT * FROM orders WHERE user_id = $1 AND status = 'pending' ORDER BY created_at DESC;
  Index Scan using idx_orders_user_created on orders (actual rows=3.00 loops=1)
    ...
sql
-- 第 4 步:核对统计信息新鲜度(估算偏差的最常见根因)
SELECT relname, last_analyze, last_autoanalyze, n_mod_since_analyze
FROM pg_stat_user_tables WHERE relname = 'orders';

整个流程中,auto_explain 的产出(案发计划)是唯一不可替代的证据;其余步骤都可以在数据库之外的观察完成。

示例五:log_settings 定位环境差异 ​

两套环境(预发与生产)同一条 SQL,预发 80ms、生产 4s,语句文本与表结构一致。开启 log_settings 后对比两边日志:

text
# 预发
LOG:  duration: 82.441 ms  plan:
  Query Text: SELECT ... FROM orders WHERE created_at > now() - interval '7 days';
  Settings: effective_cache_size = '8GB', random_page_cost = '1.1', work_mem = '64MB'
  Index Scan using idx_orders_created on orders  (actual rows=52310.00 loops=1)

# 生产
LOG:  duration: 4218.992 ms  plan:
  Query Text: SELECT ... FROM orders WHERE created_at > now() - interval '7 days';
  Settings: effective_cache_size = '512MB', random_page_cost = '4', work_mem = '8MB'
  Seq Scan on orders  (actual rows=52310.00 loops=1)

Settings: 行直接暴露了根因:生产库的 random_page_cost = 4、effective_cache_size = 512MB 让优化器认为索引扫描不划算。修正参数后计划回归 Index Scan。这个例子也说明了 auto_explain 输出的另一个隐性价值——Settings 行的存在让「参数差异」这种跨环境排查从人肉登机变成读日志。

注意事项 ​

log_analyze 的计时开销作用于全库所有语句,不是只有被记录的语句。 官方警告原文是「extremely negative impact」。长期开启必须配 log_timing = off;需要完整计时时使用限时窗口。这是本页最重要的一条。

shared_preload_libraries 修改后必须重启。 改配置文件后只 reload 是不够的,SHOW shared_preload_libraries 确认加载成功再排下一步。可以免重启的场景用 session_preload_libraries 或会话内 LOAD。

默认一切都不输出。 模块加载只是前提,auto_explain.log_min_duration 必须显式设置(默认 -1 完全关闭)。排查「加载了但没日志」时第一件事就是查这个参数的值与来源。

计划日志的体积可能是语句日志的几十倍。 复杂计划的 text 输出可达数百行,JSON 更是体积惊人。务必让 auto_explain.log_min_duration 高于 log_min_duration_statement,并确认采集管道与磁盘轮转配置能承受。

auto_explain 不提供 SQL 接口。 它没有视图、没有函数,一切输出都走服务器日志。想用 SQL 查历史计划的场景,应使用 pg_stat_statements 的统计配合日志归档,或第三方方案(如 pgwatch 的计划留存能力,见 17.4 节)。

嵌套语句与函数调试注意组合。 log_nested_statements 开启后函数内语句逐条记录,配合 log_analyze 时开销同样作用于函数内所有语句;sample_rate < 1 时嵌套语句整体采或不采,不要指望函数内部分语句被采样。

参数截断要单独设置。 auto_explain.log_parameter_max_length 独立于全局的 log_parameter_max_length,默认 -1(完整记录绑定参数)。含敏感参数的语句要在模块级显式截断,防止计划日志泄露数据。

云托管环境的可用性要单独确认。 多数云厂商允许在参数组里设置 shared_preload_libraries 与 auto_explain.*,但少数托管产品限制了预加载库清单或对 LOAD 权限有额外约束,动手前先确认所在平台的说明。

与 pg_stat_statements 共用预加载配置时注意顺序与重复。 两个库都写入同一个 shared_preload_libraries 列表,逗号分隔、不重复;改这条列表是最常见的「需要重启却只做了 reload」的失误点。上线前用 SHOW shared_preload_libraries; 核对最终形态。

计划日志记录的是「案发时刻」的快照,不会自动长期留存。 日志轮转后旧计划随之消失。重要的劣化证据要复制归档(贴进工单或事故报告),不要指望日志文件长期保留——它与慢查询日志共用同一套轮转策略。

复盘时计划要与统计信息快照一起留。 只有计划没有当时的 pg_stats(last_analyze 时间、n_distinct、相关性)与表行数,事后很难判断估算偏差的成因。事故归档时把计划、统计摘要、修复动作三者打包在一起。

最佳实践 ​

  1. 生产库用 session_preload_libraries 起步,验证一个迭代后再改 shared_preload_libraries。 免重启意味着可以随时一键关闭,把「引入新监控组件」的风险降到最低;确认稳定后再固化到重启窗口。
  2. 两级阈值分层:慢查询日志 1s、计划日志 3s。 计划输出很贵,只给最严重的 1% 语句留计划。两个阈值都要写进 runbook,而不是散落在某台机器的配置里。
  3. 长期开启的配置必须是 log_analyze = on + log_timing = off + log_buffers = on。 这是官方文档推荐的低开销形态:拿得到行数偏差与缓冲区统计,付得起计时开销的缺席。
  4. 计划格式统一用 JSON 并接入采集管道。 text 格式留给人肉应急;常态监控下结构化的计划才能被自动分析(节点类型统计、估算偏差率、计划变更事件)。
  5. 把「auto_explain 计划留存」纳入版本升级验收。 大版本升级后计划可能整体变化,升级窗口开启 log_min_duration = 1s 的 auto_explain,对比升级前后的 Top SQL 计划,是最快的回归发现手段。
  6. 与 pg_stat_statements 组成固定工作流:统计层找嫌疑、计划层拿证据。 mean_exec_time 环比异常 → 等待 auto_explain 命中 → 计划 diff → 归因修复。四步都写进故障处理手册。
  7. 触发器热点表开启 log_triggers。 业务表触发器逻辑复杂时,触发器自身耗时可能占语句大头;log_triggers(需 log_analyze)能把每个触发器的耗时单独列出,避免冤枉主查询。
  8. 高并发大表集群用 sample_rate 控制量。 慢语句每分钟数百条时,sample_rate = 0.1 足以收集到代表性计划;同类语句计划高度一致时无需全量留存。
  9. 敏感字段语句显式设置 log_parameter_max_length。 计划日志默认记录完整绑定参数,比慢查询日志更容易携带敏感数据;在模块级截断,并把计划日志纳入含敏数据管理。
  10. 深计划场景调低 max_plan_depth。 数千分区的表、超复杂视图嵌套可能产出巨型计划;采集端出现超大日志告警时优先调整该参数,而不是盲目加磁盘。
  11. 建立「计划档案」习惯。 把每次事故的 auto_explain 计划、当时的统计信息快照、修复动作归档到一起。下次同类问题直接对照档案,定位时间从小时级降到分钟级。
  12. 记住它只是「现场层」工具。 auto_explain 给出的是计划证据,锁等待、连接堆积、硬件瓶颈不在它的视野内;完整定位必须回到 pg_stat_activity / pg_locks 与系统指标,最终汇入本节第四页的优化流程。

下一节

继续学习:pg_stat_statements 慢查询分析

参考链接 ​