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

queryid 是怎么算出来的,为什么相同 SQL 有时 queryid 不同
queryid 是 PostgreSQL 用内部哈希算法对归一化后的查询文本(去掉空格、常量、注释等)计算出的 64 位整数。不是按字面匹配,也不是按执行计划生成——所以哪怕只差一个空格或一个 EXPLAIN 前缀,queryid 就可能不同。
常见错误现象:pg_stat_statements 里看到同一条业务 SQL 出现多个 queryid,误以为是“重复采集”或“统计异常”。其实是客户端拼 SQL 时带了不同字面量(比如 WHERE id = 123 和 WHERE id = 456),被归一化后仍保留参数占位结构,但若使用了不同形式的绑定(如 $1 vs ? 或字符串拼接),归一化逻辑就失效。
- 确保应用层统一使用预编译语句(
PREPARE/$n占位符),避免字符串拼接 SQL - 检查是否混用了不同驱动:libpq、psycopg2、pgx 对归一化的实现细节略有差异,特别是处理注释和换行的方式
-
pg_stat_statements.track必须设为all或top,否则嵌套函数内联的 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 时容易漏掉的三类情况
单纯按 queryid 求 max(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 容量挤掉旧记录。










