如何用 pg_stat_statements 分析最耗时 SQL

8次阅读

pg_stat_statements默认不统计执行时间,需同时开启track_io_timing=on且pg_stat_statements.track设为’all’或’top’,否则total_time和mean_time恒为0;修改后须重启数据库

如何用 pg_stat_statements 分析最耗时 SQL

pg_stat_statements 默认不统计执行时间,必须开启 track_io_timing = on 且确保 pg_stat_statements.track 包含 'all''top',否则 total_timemean_time 始终为 0。

确认 pg_stat_statements 已正确启用并采集耗时数据

很多情况下查不到耗时,根本原因是没开底层计时开关:

  • track_io_timing 必须设为 on(默认 off),否则 total_time 永远是 0 —— 这个参数控制是否测量语句级 CPU + I/O 时间,不是可选优化项
  • pg_stat_statements.track 推荐设为 'all',避免漏掉 PL/pgsql 函数内嵌的 SQL;若只设 'top',函数体内的语句不会被单独统计
  • 修改后需重启 postgresqltrack_io_timing 是 superuser-only 的 postmaster 参数,不能 SET 动态生效)
  • 验证是否生效:select * FROM pg_stat_statements LIMIT 1;total_time 列是否非零

查最耗时 SQL:按 total_time 还是 mean_time?

直接 ORDER BY total_time DESC LIMIT 10 容易误判 —— 一条慢但只执行 1 次的 SQL 可能不如一条快但执行上万次的 SQL 对整体负载影响大:

  • total_time 找「总资源吞噬者」:适合定位拖慢整个实例的语句,例如某报表 SQL 单次跑 30 秒、每天跑 2 次,total_time 就是 60 秒
  • mean_time 找「单次性能瓶颈」:适合发现低效写法,比如本该走索引却全表扫描,哪怕只执行 1 次,mean_time 也会异常高
  • 务必结合 calls 看频次:如果 mean_time 高但 calls = 1,优先检查是否为偶发问题(如锁等待、临时磁盘不足)

还原原始 SQL 时要注意 normalized_queryid 和 query 字段的区别

pg_stat_statements 会把字面量参数自动替换为 ,形成归一化语句(query 字段),但同一类查询可能因参数不同导致执行计划差异:

  • query 是归一化后的模板,例如 SELECT * FROM users WHERE id = $1; —— 用于聚合计数,但看不出实际执行了哪些具体值
  • 想定位某次真实慢查询,得结合 pg_stat_activity 或日志:开启 log_min_duration_statement = 1000,再用 query_id 关联 pg_stat_statements 中的 queryid
  • queryid 是哈希值,不可逆;不要试图靠它反推原始 SQL,只能靠日志或应用层打点对齐

常见干扰项和性能陷阱

即使数据看起来合理,也可能被以下情况误导:

  • 事务中多个 SQL 共享一次 total_time 计算?否 —— pg_stat_statements 统计粒度是单条语句,不是事务
  • VACUUM / ANALYZE 会被统计吗?会,但它们本身不进 pg_stat_statements(属于内部命令),只有显式发出的 SQL 才计入
  • 连接池(如 pgbouncer)会影响统计吗?会 —— 如果用事务池模式(transaction pooling),queryid 无法准确对应到应用端真实调用,因为语句在池中被复用和重写
  • 扩展版本不匹配:9.4+ 自带,但 13+ 新增 blk_read_time/blk_write_time,若用旧客户端连新服务,可能读不到这些字段

真正难的不是排序取 top 10,而是判断哪条 SQL 的耗时变化是计划劣化、参数倾斜、还是统计本身被缓存/连接池扭曲了。别只盯着数字,先确认数据来源干净,再看趋势。

text=ZqhQzanResources