SQL pg_stat_statements 的 queryid 与慢 SQL 归类分析模板

3次阅读

queryid是postgresql对归一化sql(去空格、常量、注释等)用哈希算法生成的64位整数,相同sql因空格、explain前缀、绑定变量形式不同($1 vs ?)或驱动差异可能导致queryid不同。

SQL pg_stat_statements 的 queryid 与慢 SQL 归类分析模板

queryid 是怎么算出来的,为什么相同 SQL 有时 queryid 不同

queryid 是 PostgreSQL 用内部哈希算法对归一化后的查询文本(去掉空格、常量、注释等)计算出的 64 位整数。不是按字面匹配,也不是按执行计划生成——所以哪怕只差一个空格或一个 EXPLAIN 前缀,queryid 就可能不同。

常见错误现象:pg_stat_statements 里看到同一条业务 SQL 出现多个 queryid,误以为是“重复采集”或“统计异常”。其实是客户端拼 SQL 时带了不同字面量(比如 WHERE id = 123WHERE id = 456),被归一化后仍保留参数占位结构,但若使用了不同形式的绑定(如 $1 vs ? 字符串拼接),归一化逻辑就失效。

  • 确保应用层统一使用预编译语句(PREPARE / $n 占位符),避免字符串拼接 SQL
  • 检查是否混用了不同驱动:libpq、psycopg2、pgx 对归一化的实现细节略有差异,特别是处理注释和换行的方式
  • pg_stat_statements.track 必须设为 alltop,否则嵌套函数内联的 SQL 不会被捕获,自然也没有 queryid

如何用 queryid 关联慢 SQL 的原始文本和执行计划

queryid 查不到原始 SQL —— pg_stat_statements 默认只存归一化后的 query 字段(带 $1 占位符),不存真实参数值。想还原某次慢调用的具体语句,得结合日志或采样。

实操建议:

  • 开启 log_min_duration_statement = 1000 并设置 log_line_prefix = '%m [%p] %q[queryid:%u] '(需 14+ 版本支持 %u 输出 queryid),这样慢日志里每条都带 queryid,可反查具体参数
  • EXPLAIN (ANALYZE, BUFFERS) 手动执行某 queryid 对应的归一化 SQL(替换占位符为实际值),注意加 /*+ SET enable_hashjoin=off */ 这类 hint 会影响计划,导致跟线上不一致
  • 别直接依赖 pg_stat_statements.query 显示的内容做根因判断——它不反映真实绑定值,可能掩盖类型隐式转换问题(比如 $1 实际传的是 text,但字段是 uuid

按 queryid 聚合慢 SQL 时容易漏掉的三类情况

单纯按 queryidmax(total_time)avg(mean_time),会掩盖很多真实问题。

  • 同一 queryid 在不同数据分布下表现差异极大:比如 select * FROM orders WHERE user_id = $1,当 $1 是高频用户(百万级订单)vs 长尾用户(个位数订单),执行时间差两个数量级,但 pg_stat_statements 只给一个平均值
  • union 或 CTE 的查询,可能被拆成多个子计划,每个子计划有独立 queryid,但主 SQL 的 queryid 只对应顶层归一化结果,子节点统计被分散
  • queryid = 0 的记录代表无法归一化的动态 SQL(如拼接列名、表名),这类必须单独捞日志分析,不会出现在常规 queryid 分析模板里

一个轻量但实用的慢 SQL 归类分析 SQL 模板

这个模板不追求大而全,重点解决“哪个 queryid 最值得优化”和“它最近一次慢在哪”的问题:

SELECT   queryid,   substring(query, 1, 80) AS query_snippet,   calls,   round(total_time::numeric, 2) AS total_time_ms,   round(mean_time::numeric, 2) AS mean_time_ms,   rows,   shared_blks_hit - shared_blks_read AS net_cache_hit FROM pg_stat_statements WHERE total_time > 10000  -- 总耗时超 10 秒   AND calls > 5 ORDER BY total_time DESC LIMIT 20;

关键点:

  • 过滤掉 calls ≤ 5 的,避免把偶发毛刺当瓶颈;也别只看 mean_time,有些 SQL 调用少但单次极长(如报表导出)
  • net_cache_hit 是个快速信号:负值说明大量读磁盘,配合 shared_blks_read 看是否缺索引或缓存不足
  • 别在生产库直接跑 SELECT query 全字段——某些归一化 SQL 很长,可能触发网络或客户端截断;先用 substring 截取再人工确认

真正难的不是写这个 SQL,而是确认你看到的 queryid 是否覆盖了所有执行路径,以及有没有被 pg_stat_statements.max 容量挤掉旧记录。

text=ZqhQzanResources