PostgreSQL 的慢查询诊断通常从 pg_stat_statements 开始,但它只回答哪条 SQL 总耗时最长,很难解释为什么同一条 SQL 会突然从几十毫秒飙升到几秒。改表结构、加索引、调参数之前,需要先知道到底是执行计划变了、等待事件集中还是硬件资源紧张。pg_stat_monitor 在保留归一化统计的基础上,把时间桶、执行计划哈希、等待事件、CPU 时间与 IO 指标放进同一张表里,让慢查询定位从单点时间值变成多维度对比。

一、pg_stat_monitor 与 pg_stat_statements 的核心差异
很多人第一反应是它与 pg_stat_statements 有什么区别。简单说,后者擅长回答哪些查询累计消耗最高,前者则进一步回答某个时间段内发生了什么。它的统计以 bucket 为粒度,每个 bucket 默认 60 秒,可以按时间切片观察调用次数、总耗时、平均耗时、影响行数等指标的变化。当一条 SQL 在某个时间点开始变慢时,直接对比前后两个 bucket 的数据,比看一条全局平均值更有意义。
另一个关键差异是 plan_hash。查询文本归一化后相同的 SQL,只要执行计划发生变化,就会生成不同的 plan_hash。很多性能劣化来自统计信息过期或参数倾斜导致的计划跳变,这一维度能直接确认。再加上 wait_event_type 和 wait_event,例如大量 ClientRead 可能说明应用消费结果慢,而 DataFileRead 则指向磁盘 IO。这些信息在原生 pg_stat_statements 中都没有。
下面列出两者在几个关键点上的差别:
- 时间粒度:pg_stat_statements 仅聚合累计值,pg_stat_monitor 支持 bucket 时间序列。
- 执行计划:pg_stat_statements 不记录,pg_stat_monitor 通过 plan_hash 标识计划版本。
- 等待事件:pg_stat_statements 无,pg_stat_monitor 记录等待事件类型与名称。
- 资源拆分:pg_stat_monitor 增加 cpu_user_time、cpu_sys_time、shared_blks_read 等指标。
因此,当需要寻找变慢原因而不是选择 Top SQL 时,pg_stat_monitor 提供的信息密度更高。代价是它需要额外内存保存 bucket 历史数据,必须根据实例负载合理设置保留数量和长度。
二、安装与关键配置:数据粒度由参数决定
安装 pg_stat_monitor 通常在编译或使用二进制包后,在 postgresql.conf 中把它加入 shared_preload_libraries。因为统计收集需要挂到查询执行器上,必须随服务启动加载,不能只通过 CREATE EXTENSION 动态加载。修改配置后需要重启实例,再在目标库执行扩展创建。
-- postgresql.conf shared_preload_libraries = 'pg_stat_statements, pg_stat_monitor' -- 在数据库中创建扩展 CREATE EXTENSION IF NOT EXISTS pg_stat_monitor;
关键参数中,pgsm_bucket_time 控制每个时间桶的秒数,默认 60 秒。对于高频交易类系统,可以调小到 10 或 15 秒,代价是数据量增大;对于报表库,60 秒通常足够。另一个重要参数是 pgsm_max_buckets,它限制每个归一化查询保留多少个 bucket,默认 10。若需要回溯更长时间,应适当调大,但会增加内存消耗。使用 pg_stat_monitor_settings 函数可以确认生效值。
还需要关注 pgsm_enable_query_plan,开启后会记录查询执行计划文本,方便直接查看当时使用的计划。执行计划可能很长,生产环境建议只对排查阶段开启,或者配合 pgsm_max_plan_text 限制长度。另一个参数 pgsm_normalized_query 默认开启,会把常量替换为 $1,避免因为输入值不同而拆成大量条目。
配置完成后,通过 SHOW shared_preload_libraries; 确认加载顺序。加载失败时查看日志中的 pg_stat_monitor 相关错误,常见原因是扩展未编译到当前 PostgreSQL 大版本,需要安装对应版本包。
三、核心增强维度拆解:bucket、plan_hash 与 wait_event
先看 bucket。它把统计数据按固定时间片切开,表结构中的 bucket 表示该条记录属于哪个编号的桶,bucket_start_time 给出桶的起始时间。对比故障窗口前后的 bucket,能快速发现 calls 是否突增、total_exec_time 是否异常、shared_blks_read 与 temp_blks_written 是否同步恶化。例如某个查询在某个时间点后每次执行时间变长,但调用次数没变,更多指向资源或计划问题,而不是流量增加。
再看 plan_hash。同一个归一化查询如果优化器选择不同关联顺序或访问路径,这个哈希值会改变。要定位计划跳变,可以按 query 分组后查看是否出现多个 plan_hash。发现计划变化后,最直接的修复是重新收集相关表统计信息,或调整 random_page_cost 等开销参数让优化器回到预期路径。
然后是 wait_event_type 与 wait_event。当查询被锁阻塞时,等待类型会成为 Lock,事件名如 transactionid 或 tuple;当等待数据文件读取时,会出现 IO 和 DataFileRead。把它们和 bucket 放在一起观察,可以看到某一时刻是否发生大面积锁等待,或者存储 IO 延迟突然升高。这比单纯分析 SQL 文本更能反映外部环境变化。
以下 SQL 可以从这三个维度对某条查询做初步画像:
SELECT bucket_start_time,
calls,
total_exec_time / calls AS avg_exec_ms,
plan_hash,
wait_event_type,
wait_event,
shared_blks_hit,
shared_blks_read,
temp_blks_written
FROM pg_stat_monitor
WHERE query ~* 'FROM orders'
AND bucket_start_time >= now() - interval '1 hour'
ORDER BY bucket_start_time DESC;
实际使用中建议先用 query 字段模糊匹配定位目标 SQL,再使用 queryid 精确过滤。由于 query 文本可能很长,输出时截断即可。若只看某一类等待事件,直接在 WHERE 中增加条件。
四、优化实战:从多维数据定位根因
假设一条订单汇总 SQL 在业务高峰期突然变慢,pg_stat_statements 显示总耗时上升,但无法判断原因。第一步按时间桶查看该 SQL 的 mean_exec_time 与 calls,确认是从某个时间点开始劣化,还是随调用量线性增加。如果 calls 基本不变但平均耗时骤增,继续看 plan_hash 是否改变。
第二步,如果发现两个 plan_hash 对应不同时间桶,说明执行计划发生跳变。此时可以从扩展保存的计划文本取出旧计划与新计划对比,也可以在测试库用相同参数分别 EXPLAIN (ANALYZE, BUFFERS) 模拟。常见原因是统计信息过期后优化器低估行数,错误选择嵌套循环而不是哈希关联。此时执行 ANALYZE orders 或扩大相关列的 n_distinct 统计值,往往能恢复。
第三步,如果 plan_hash 没变,就重点看 wait_event_type。例如大量 IO 等待与 temp_blks_written 上升,说明查询临时文件增多,可能因为 work_mem 不够,排序或哈希溢出到磁盘。适当调大当前会话的 work_mem 并重试,能明显缩短时间。如果等待事件以 Lock 为主,则需要抓取阻塞链路,优化长事务或调整更新顺序。
还有一个常被忽略的维度是 CPU 时间拆分。cpu_user_time 与 cpu_sys_time 可以帮助区分 CPU 消耗与等待消耗。假设总执行时间很高,但 CPU 时间占比很低,说明大部分时间在等待,应从锁、IO、网络等方向排查;反之,CPU 时间占比接近总时间,说明计算本身重,需要改写 SQL 或增加过滤条件。
最后给出两个常用诊断 SQL。查找执行计划在 30 分钟内发生变化的查询:
SELECT query,
count(DISTINCT plan_hash) AS plan_versions,
max(calls) AS max_calls,
max(total_exec_time) AS max_exec_time
FROM pg_stat_monitor
WHERE bucket_start_time > now() - interval '30 minutes'
GROUP BY query
HAVING count(DISTINCT plan_hash) > 1
ORDER BY max_exec_time DESC;
检测最近一小时内等待事件分布:
SELECT wait_event_type,
wait_event,
count(*) AS bucket_count,
sum(total_exec_time) AS total_time
FROM pg_stat_monitor
WHERE bucket_start_time >= date_trunc('hour', now()) - interval '1 hour'
AND bucket_start_time < date_trunc('hour', now())
GROUP BY wait_event_type, wait_event
ORDER BY total_time DESC;
排查完成后,可以调用 SELECT pg_stat_monitor_reset(); 清空历史统计,避免旧数据干扰下一次分析。持续优化时,建议把关键指标接入监控系统,当 mean_exec_time 分位数或等待事件出现异常时自动告警,而不是等用户反馈才介入。
PostgreSQL慢查询优化pg_stat_monitor查询性能监控修改时间:2026-10-06 15:44:21