PostgreSQL 查询偶发变慢怎么查:用 auto_explain 把慢的那一次执行计划抓下来(实测 1ms 和 650ms 是同一条 SQL)
PostgreSQL 偶发慢查询怎么排查:在 PostgreSQL 17.11 测试库(orders 202 万行自造数据)实测,同一条 SQL 换一个参数从约 1ms 变成 650–894ms,手动 EXPLAIN 用常见参数永远复现不了。用 auto_explain(log_min_duration + log_analyze + log_timing=off)把慢的那一次计划连同参数值 $1 = '7' 写进日志,看到 Index Scan Backward 过滤掉 1,644,859 行;建 (customer_id, created_at DESC) 复合索引后同一参数 0.7–1.4ms。另实测:被 statement_timeout 取消的语句 auto_explain 不记录计划。
PostgreSQL 17.11 测试库实测(自造数据):SELECT … WHERE customer_id=$1 AND status='paid' ORDER BY created_at DESC LIMIT 20,普通客户 0.8–1.7ms,一个「老客户」(2 万行订单全在 300–365 天前)650–894ms。auto_explain 日志给出 duration 698.868 ms、Query Parameters: $1 = '7'、Index Scan Backward using orders_created_idx、Rows Removed by Filter: 1644859、Buffers shared hit=1653767。建复合索引后 0.696–1.378ms。pgbench 对比 auto_explain 关 / 只记时长 / log_analyze 关 timing / 开 timing,四组 tps 差异落在同组 3 次波动之内。被 statement_timeout 取消的语句日志只有 ERROR 和 STATEMENT,没有计划。
适用对象
- 我的判断:所谓「偶发慢」,多数不是偶发,而是某几个参数值稳定地慢。手动 EXPLAIN 复现不了,是因为你换了参数。
- auto_explain 只要设 log_min_duration 就能工作;加 log_analyze 时同时设 log_timing=off,能拿到实际行数和 Rows Removed by Filter,代价最小。PG16 起日志里还会带 Query Parameters(参数值)。
- 本次测试库上 auto_explain 四种配置的 pgbench tps 差异没有超过同组波动;但这只是 0.06ms 的小查询,官方文档明确提醒 log_analyze 可能带来很大开销,生产上请先在测试环境测。
- 被 statement_timeout 取消的语句不会留下计划。log_min_duration 必须设得比 statement_timeout 小,而且要让一部分慢语句能跑完。
偶发慢先别调参数,先让数据库在慢的那一刻把计划写下来
写这篇前我列了 5 个角度:给一年前的自己的建议、常见说法哪里不对、换个定义、对小团队为什么重要、用别的领域类比。最后选了第二个:网上很多偶发慢的建议是先加 shared_buffers、重建索引、VACUUM,我不同意。不知道慢的那一次走了什么计划之前,改任何参数都是猜。
换个定义更好理解:偶发慢 = 数据分布不均 + 计划对某些参数不合适。规划器按统计平均值估算,客户 7 有 2 万行,它估算 11,626–12,817 行,觉得「倒着扫 created_at 索引,很快就能凑够 20 行」;实际客户 7 的订单都在一年前,要倒着扫过 164 万行才凑够。
所以第一步不是优化,而是取证:auto_explain 就是 PostgreSQL 自带的「行车记录仪」,超过阈值的语句自动把计划写进日志。
同一条 SQL,参数不同:1ms 和 650ms
SQL:SELECT id FROM orders WHERE customer_id = $1 AND status = 'paid' ORDER BY created_at DESC LIMIT 20。每个参数跑 3 次,客户 7 跑两轮。
普通客户的计划是 Bitmap Index Scan on orders_customer_idx → Sort → Limit,几十行排个序就完;客户 7 的计划是 Index Scan Backward using orders_created_idx,带 Filter (customer_id = 7 AND status = 'paid')。规划器估算的 cost 只有 199,实际要读 165 万个缓冲页。
| customer_id | 耗时 ms | 计划顶层扫描 |
|---|---|---|
| 1001 | 1.042 / 0.922 / 1.178 | Bitmap Heap Scan + Sort |
| 1002 | 1.087 / 1.147 / 0.789 | Bitmap Heap Scan + Sort |
| 7(第一轮) | 893.726 / 652.007 / 694.260 | Index Scan Backward using orders_created_idx |
| 1003 | 1.129 / 0.889 / 0.835 | Bitmap Heap Scan + Sort |
| 7(第二轮) | 650.008 / 658.462 / 667.919 | Index Scan Backward using orders_created_idx |
| 1004 | 1.046 / 0.931 / 0.756 | Bitmap Heap Scan + Sort |
auto_explain 怎么开:4 个参数够用,日志里直接看到参数值
会话级试用(需要超级用户):LOAD 'auto_explain'; SET auto_explain.log_min_duration = '100ms'; SET auto_explain.log_analyze = on; SET auto_explain.log_buffers = on; SET auto_explain.log_timing = off;。长期使用写进 postgresql.conf:session_preload_libraries = 'auto_explain',再加上面几个参数,新连接生效、不用重启;放 shared_preload_libraries 也可以,但要重启。
测试库日志原文(用 psql 的 \bind 走扩展协议传参):LOG: duration: 698.868 ms plan: / Query Parameters: $1 = '7' / Limit (cost=0.43..199.27 rows=20) (actual rows=20 loops=1) / Buffers: shared hit=1653767 / -> Index Scan Backward using orders_created_idx on orders (cost=0.43..127427.84 rows=12817) / Filter: ((customer_id = 7) AND (status = 'paid'::text)) / Rows Removed by Filter: 1644859。
这一段日志把三件事都说清了:慢的是哪个参数($1 = '7')、走了什么计划(倒着扫 created_at 索引)、为什么慢(过滤掉 164 万行)。Query Parameters 这一行由 auto_explain.log_parameter_max_length 控制,默认 -1 记录完整参数值;参数里有手机号、邮箱等敏感数据时,可以设成 0 关掉或设一个截断长度。
| 参数 | 建议值 | 原因 |
|---|---|---|
| auto_explain.log_min_duration | 比业务超时小一档,如 500ms | 默认 -1 不记录;设 0 会记录所有语句,日志会爆 |
| auto_explain.log_analyze | on | 要看实际行数和 Rows Removed by Filter |
| auto_explain.log_timing | off | 官方说明逐节点计时可能显著拖慢查询;只关计时仍保留实际行数 |
| auto_explain.log_buffers | on | Buffers shared hit/read 能区分是读缓存多还是读磁盘多 |
| auto_explain.log_parameter_max_length | 按合规要求定(默认 -1 全记) | 参数可能含个人信息 |
| auto_explain.sample_rate | 高峰期可设 0.1–0.5 | 只对一部分会话生效,降低开销 |
开销:小查询上四种配置差异落在波动内,但别把它当生产结论
pgbench -M prepared -c 8 -j 4 -T 20,脚本是随机普通客户的上面那条 SQL(平均 0.06ms),每种配置跑 3 次。结果四组都在 12.2 万–13.8 万 tps 之间,组内 3 次的波动(如关闭 auto_explain 时 124,122–137,744)比组间差异还大,测不出稳定差别。
我对这组数的态度是:它只说明「在这台机器、这种很短的索引查询上,开 log_analyze + log_timing=off 没有明显代价」,不能推到复杂查询。官方文档对 log_analyze 的提醒很直接:开启后所有语句都会逐节点计时,不管是否最终被记录,可能带来非常大的性能影响。生产上我会先 log_timing=off,再配 sample_rate,在预发环境压一轮再上。
| 配置 | tps 第 1 次 | 第 2 次 | 第 3 次 |
|---|---|---|---|
| 不开(只预加载,未设阈值) | 137,744 | 126,375 | 124,122 |
| 只设 log_min_duration=200ms | 121,609 | 122,379 | 122,322 |
| + log_analyze=on,log_timing=off | 132,081 | 126,092 | 127,050 |
| + log_analyze=on,log_timing=on | 123,573 | 122,070 | 137,335 |
一个坑:被 statement_timeout 取消的语句,auto_explain 不记计划
我把 statement_timeout 设成 300ms 再跑客户 7:客户端收到 ERROR: canceling statement due to statement timeout,日志里只有 ERROR 和 STATEMENT 两行,没有计划。RESET statement_timeout 后再跑同一条,auto_explain 才记下 duration: 666.035 ms 和完整计划。原因是 auto_explain 在语句正常执行结束时才输出,被取消的语句走不到那一步。
这意味着:如果你的应用超时设得很紧(比如 1s),而 log_min_duration 设成 3s,那最该抓的语句永远抓不到。log_min_duration 要明显小于 statement_timeout;超时报错本身怎么查,见 PostgreSQL 报错速查里的 canceling statement due to statement timeout 页。
抓到计划之后:复合索引让客户 7 从 650ms 降到 1ms 以内
计划的问题是「按时间倒序找某个客户的前 20 条」只能二选一:要么按客户找全再排序,要么按时间扫再过滤。CREATE INDEX orders_cust_created_idx ON orders (customer_id, created_at DESC) 之后,两件事一个索引就能做完:客户 7 三次 1.378 / 0.974 / 0.696ms,计划变成 Index Scan using orders_cust_created_idx、Index Cond: (customer_id = 7)、Buffers shared hit=23;客户 1001 和占 40% 的客户 1 也都在 0.77–1.45ms。
代价:这个索引 61MB,表 222MB,写入多一个索引维护。生产上用 CREATE INDEX CONCURRENTLY,避免长时间锁表;建之前先确认没有长事务,否则 CONCURRENTLY 会一直等。
我排查偶发慢的四步
- 01
1
开 auto_explain 取证
log_min_duration 小于业务超时,log_analyze=on、log_timing=off,等慢的那一次自己写进日志。
- 02
2
看 Query Parameters 和估算偏差
记下慢的参数值;对比 rows 估算和实际,盯 Rows Removed by Filter 和 Buffers。
- 03
3
用慢参数复现
在测试环境用同一个参数 EXPLAIN (ANALYZE, BUFFERS),确认和日志里的计划一致。
- 04
4
再决定改法
复合索引、改写 SQL、扩展统计或调整 plan_cache_mode,改完用同一参数验证。
免费 · 留邮箱领检查单
PostgreSQL 14 停更升级检查单(Excel 可直接打开)
- PG14 2026-11-12 停更,升 17 / 18 前后 19 项检查:只读预检、--check、回滚、补统计、回归
- 每项写清怎么查、通过标准、不处理会怎样(附测试库实测数字)
- 配合站上的只读预检 SQL 使用,每个业务库各跑一次
觉得有用?转给正在做这件事的同事或朋友
参考依据
以下来源用于确认市场趋势、政策背景和术语边界;具体落地方案仍以客户的数据范围、权限和交付目标为准。
常见问题
PostgreSQL 查询偶尔很慢、手动执行又很快,是什么原因?
常见原因是数据分布不均,某些参数值让规划器选了不合适的计划。本次 PostgreSQL 17.11 测试库上同一条 SQL,普通客户约 1ms,一个订单全在一年前的客户 650–894ms。手动 EXPLAIN 时换了参数,就复现不了。
auto_explain 怎么开启?要重启数据库吗?
放进 session_preload_libraries 对新连接生效,不用重启;放 shared_preload_libraries 需要重启。至少要设 auto_explain.log_min_duration,默认 -1 什么都不记录。会话里试用可以 LOAD 'auto_explain'(需要超级用户)。
auto_explain.log_analyze 对性能影响大吗?
官方文档提醒可能影响很大,因为所有语句都会逐节点计时。本次测试库上 0.06ms 的小查询、log_timing=off 时测不出稳定差异,但这不能代表复杂查询;建议配合 log_timing=off 和 sample_rate,先在测试环境压测。
为什么设置了 auto_explain,超时的慢 SQL 还是没有执行计划?
被 statement_timeout 取消的语句不会被 auto_explain 记录。本次实测超时那次日志只有 ERROR 和 STATEMENT。log_min_duration 要设得比 statement_timeout 小。
auto_explain 日志里能看到绑定参数的值吗?
PostgreSQL 16 起可以,日志里有 Query Parameters 一行,由 auto_explain.log_parameter_max_length 控制,默认 -1 记录完整值,设 0 关闭。本次 PG 17.11 日志里是 $1 = '7'。
下一步
推荐动作
你提供 auto_explain 抓到的慢计划和 pg_stat_statements 结果(可脱敏),我们判断是数据分布、索引、统计还是参数问题,给出验证步骤;生产变更由你方授权执行。也可以先买 ¥59 PG 性能排查学习包自查。