排查PostgreSQL慢查询时,很多人习惯先用EXPLAIN ANALYZE手动执行一遍问题SQL,观察执行计划。但生产环境里有相当一部分慢查询是偶发的、依赖当时的数据分布和缓存状态的,等你想复现的时候它已经不慢了。这时候auto_explain插件的价值就体现出来了:它能在后台自动记录实际执行时间超过阈值的SQL及其真实执行计划,而其中的log_buffers选项会额外输出缓冲区访问统计,让你清楚看到每一步计划节点到底命中了多少共享内存页、读了多少磁盘、写了多少临时块,这是定位IO瓶颈的第一手数据。

Buffers各项指标到底在说什么
开启log_buffers后,执行计划的每个节点后面会多出一段Buffers统计,典型输出类似:Buffers: shared hit=85 read=12, temp read=7 written=7。这几个数字含义各不相同,混为一谈会导致误判。
shared hit表示从shared_buffers共享内存池中直接读到的数据块数量,这部分基本是纯内存操作,开销极小。shared read则表示缓冲池里没有、必须从操作系统层面读取的块,注意这里读的可能是OS的page cache,也可能是真正的磁盘IO,PostgreSQL本身区分不了这两者。如果read的占比很高,说明缓存命中率差,要么是内存配置不足,要么是表数据量增长后热点数据已经装不进内存了。
temp read和temp written指向的是临时文件访问。一旦执行计划里出现这两个值,基本可以断定发生了排序溢出、哈希聚合溢出或者中间结果物化,work_mem不够用了。临时块是写完再读回来的,一次溢出意味着双倍IO,对性能的杀伤力往往比shared read更大。
如何正确配置auto_explain
auto_explain是contrib自带的模块,配置前先确认安装目录存在。最方便的方式是修改postgresql.conf并重启,也可以用LOAD 'auto_explain';在当前会话临时加载后设置会话级参数,适合临时抓问题。
# postgresql.conf 中追加以下配置 shared_preload_libraries = 'auto_explain' # 记录执行超过1秒的语句 auto_explain.log_min_duration = '1s' # 必须开启,否则不显示真实行数和耗时 auto_explain.log_analyze = on # 输出buffers统计,依赖log_analyze auto_explain.log_buffers = on # 记录计划节点粒度耗时 auto_explain.log_timing = off # 计划树使用JSON格式,方便程序解析 auto_explain.log_format = 'json' # 嵌套语句也记录 auto_explain.log_nested_statements = on
有几个坑要特别提醒。第一,log_buffers必须在log_analyze = on的前提下才会生效,因为buffers统计来自真实执行过程,仅做EXPLAIN静态分析时根本拿不到这些数据。第二,log_analyze会带来额外开销,因为它要求语句真实执行完毕并收集统计,官方文档明确说明这在高负载库上可能有可感知的影响,建议只在排查窗口期开启,或者把log_min_duration设得高一点,只抓真正的慢查询。第三,log_timing = on时每个计划节点都要计时,开销比统计本身大得多,除非要分析节点级耗时,平时保持off即可,Buffers数据足以回答大部分IO问题。
配置生效后,日志里会出现类似下面的记录,可以直接根据Buffers数字下判断:
duration: 2314.528 ms plan:
Query Text: SELECT * FROM orders o JOIN users u ON u.id=o.uid ...
Nested Loop (rows=50000 loops=1)
Buffers: shared hit=1420 read=38550, temp read=2100 written=2100
-> Index Scan using users_pkey on users u (rows=100 loops=1)
Buffers: shared hit=102
-> Bitmap Heap Scan on orders o (rows=500 loops=100)
Buffers: shared hit=1318 read=38448三种典型瓶颈的判断思路
拿到Buffers数据后怎么解读?可以按三种模式来归类。
第一种是shared read巨大但temp为空。这说明扫描路径走了太多磁盘块,通常是索引缺失或者索引失效导致全表扫描、位图扫描退化为顺序扫描。对应措施是检查pg_stat_user_tables里的seq_scan计数,为高频过滤列补建合适的复合索引,并关注列的统计信息是否过期,ANALYZE一下有时就能让计划回到索引扫描。
第二种是temp读写成对出现。排序或哈希节点把中间结果写到了磁盘,处理办法是评估能否通过调大work_mem让它留在内存里,或者在SQL层面减少参与排序的数据量,比如先过滤再排序、用覆盖索引避免回表。注意work_mem是按每个排序、哈希节点单独分配的,一个复杂查询可能同时有多个节点各占一份,调大时要对内存总量心里有数。
第三种是shared hit极高但依然慢。此时瓶颈不在IO而在CPU或行数估算偏差上,Buffers干净得很却耗时离谱,多半是rows估算和实际差距悬殊,导致优化器选错了连接方式,比如该用哈希连接却选了嵌套循环。这种情况要更新统计信息、调整default_statistics_target,必要时用pg_hint_plan之类手段干预计划。
auto_explain与EXPLAIN ANALYZE怎么选
两者输出内容基本一致,区别在于触发方式。EXPLAIN ANALYZE需要人工干预并重新执行SQL,适合可稳定复现的问题,但重新执行有副作用:写操作会真实改数据(通常要包在事务里ROLLBACK),而且当时的数据量和缓存状态可能已变化,看到的计划未必是出问题时的那一个。
auto_explain则是被动捕获,语句慢了才记,不慢不记,对生产环境友好得多。它的日志可以持续积累,配合log_format = 'json'还能用脚本批量解析,统计哪类查询的read量最大、命中率随时间怎么变化,这些趋势信息对容量规划很有价值。实践中推荐的做法是:log_min_duration常开设一个宽松阈值(比如5秒),排查期临时收紧到几百毫秒并打开log_analyze,问题解决后恢复,兼顾观测能力和性能开销。
最后提一句日志膨胀问题:开启log_nested_statements后,一条应用层慢SQL可能带出几十条内部语句的计划,日志量会明显上涨,记得给日志目录预留空间并配置好轮转策略,避免排查工具本身变成新的故障点。
PostgreSQL慢查询auto_explainlog_buffers修改时间:2026-09-14 10:03:08