因为 pg_stat_statements 会把同一条查询形态的每一次执行都聚合起来,「这个页面慢」就变成了一张排好序的表。代价只是一行配置加一次重启;回报是你能知道上万种查询形态里,到底是哪一种在占用时钟。

真正重要的决定只有一个:按哪一列排序。按平均耗时排,只是把慢日志已经告诉过你的事再说一遍;按总耗时排,才能找出那个一直悄悄吃掉数据库四分之一时间的查询。

先归一化,再聚合

这个扩展并不原样保存你的 SQL。常量会被折叠成参数符号,于是 WHERE tenant_id = 42 和 WHERE tenant_id = 8712 合并成同一条记录,展示出来的文本是第一个产生该 queryid 的查询。编号规则是位置式的——符号从当前文本里最大 $n 的下一个数字开始,所以 PL/pgSQL 里的 SELECT i + 1 INTO j 会显示成 SELECT i + $2。列表的行为一样:只差 IN 元素个数的两条查询会被压成 IN ($1 /*, ... */),调用次数记为 2。

效果很夸张。一份建模负载里 1420052 次执行、每次字面量都不同,最后塌缩成 6 行——平均每行统计代表 236675 条原始语句。随之而来的有两点注意:queryid 是对解析后语法树做的哈希,所以 search_path 不同时,文本完全相同的查询可能落进不同记录;不相干的查询也可能碰撞进同一条;而且它并不承诺跨大版本稳定。把 queryid 当成比文本更好的标识符,而不是一个永久主键。

文档还提醒一个反向情况:当归一化没能生效时——比如条目被频繁淘汰——你会在 pg_stat_statements 里看到带着具体常量值的查询,同一种形态散成很多行。把 pg_stat_statements.max 调大可以降低这种概率,而 pg_stat_statements_info 里的淘汰计数就是判断依据。

看总耗时,不看平均耗时

真正该看的列是 calls、total_exec_time、mean_exec_time、rows 和几个块计数器。min/max/stddev_exec_time 则告诉你这条查询是一直慢还是偶尔特别长,前者指向坏计划,后者指向锁竞争。

按平均排序和按总量排序回答的是两个不同问题,而两者的差距很大。同一份负载里,平均耗时最差的那条(9689 ms,12 次调用)占总测量时间的 22.4%;总耗时最高的那条占 25.9%,但平均每次只要 0.749 ms。总耗时第一的这条查询,在任何合理的 log_min_duration_statement 下都一次也不会被记进慢日志;而总耗时前三名加起来占了全部测量时间的 65.5%。一条 0.099 ms 的查询被调用 900000 次之后,平均排名第六,总量排名第三,合计 89217 ms。

计数器只增不减,所以要按窗口比较

计数器从统计建立起就一直累积,所以历史上慢过的查询不会自己从榜首淡出。解法有两种:要么做定向重置——pg_stat_statements_reset(userid, dbid, queryid, minmax_only) 四个参数都收,你可以只清一条查询、只清一个库,或者只清 min/max 字段;要么取两个快照做增量。

块计数器值得单独读一遍。shared_blks_hit 和 shared_blks_read 是一直在统计的;track_io_timing 打开的是 *_blk_read_time 与 *_blk_write_time 这两列时间,不是那两个计数。命中率是诊断指标,不是代价:负载里命中率最高的一条(98.89%)同时也是块消耗量最大的一条,81000000 个块;而命中率最低的一条(62.22%)只搬了它的三十七分之一。

坏掉的样子

第一种失败是这个扩展根本不在。它必须写进 shared_preload_libraries,因为它需要额外的共享内存,所以增删都要重启;compute_query_id 必须是 auto 或 on;视图也只在你执行过 CREATE EXTENSION pg_stat_statements 的那个库里存在。任缺一条,你写的每一条查询都会失败——也就是说你以为有的那套监控,已经空了好几个月。

第二种失败是重置毁掉了对比。在窗口中间把计数器清零,之后每个差值都是负的:夹具里 6 种查询形态全部报出负耗时。算术没错,错的是窗口。pg_stat_statements_info.stats_reset 就是为此存在的——做任何减法之前先看它;如果重置是故意的,就把窗口起点挪到重置之后,而不是硬去跨越它做对账。

顺带一个容易踩的坑:如果监控系统把 queryid 当作长期基线写进时序库,一次大版本升级就可能让所有基线错位——文档明确不承诺这个标识符跨大版本稳定。

快速验证

把两个排名都跑一遍,看看它们有多不一致:

psql -c "SELECT calls, round(total_exec_time) AS total_ms, round(mean_exec_time,3) AS mean_ms, rows FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;"
psql -c "SELECT calls, round(mean_exec_time,2) AS mean_ms, round(total_exec_time) AS total_ms FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;"
psql -c "SELECT stats_reset FROM pg_stat_statements_info;"
psql -c "SELECT sum(calls) AS calls, round(sum(total_exec_time)/1000) AS total_s, round(sum(shared_blks_read)*8.0/1024/1024,1) AS read_gib FROM pg_stat_statements;"
psql -c "SELECT pg_stat_statements_reset(0, 0, queryid) FROM pg_stat_statements WHERE query LIKE 'SELECT%';"

前两条列表应当对不上,而该据以动手的是第一条。stats_reset 决定一个增量窗口是否有效。第三条汇总用来核对量级——如果 total_s 大于统计信息存在的实际时长,说明你跨越了一次重启在读数。最后一条是定向重置,要刻意去跑,因为它恰好就是让下一次对比失真的那个动作。

来源