PostgreSQL 14 的 compute_query_id:统一查询标识背后的性能权衡

2026-05-30 32 预计阅读时间:1 分钟
来源:postgr.es AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:8 分钟

PostgreSQL 在 14 版本之前,各子系统对"这条查询到底是谁"的回答并不一致。pg_stat_activity 有自己的计算方式,pg_stat_statements 有另一套,日志里又是一种——同一个 SQL 在不同地方拿到不同的 ID,关联分析几乎不可能。14 版本用 compute_query_id 这个 GUC 把计算逻辑统一到核心层,但默认值是 off,不是 on。原因很简单:每次计算都要吃一点 CPU,而 PostgreSQL 的设计哲学是"不用的功能不该有开销"。

统一之前:三套 ID 各自为政

14 之前,查询 ID 的生成散落在多个模块:

  • pg_stat_statements:在扩展模块内部用 hash 生成 query_id,只有加载了该扩展才会计算。
  • pg_stat_activity:部分版本有 query 字段,但没有稳定的 query_id 可供跨视图关联。
  • 日志系统log_line_prefix 支持 %q 等占位符,但本身不产出 query_id。

结果是:你想把 pg_stat_statements 里的高耗时语句和 pg_stat_activity 里的当前运行会话对应起来,只能靠文本匹配——去掉空白、折叠常量后做字符串比较,既不可靠又低效。

compute_query_id 做了什么

14 版本把 query_id 的计算收归核心,由 compute_query_id 控制:

  • off(默认):不做任何计算,零开销。
  • on:对每条进入执行的查询计算一个基于 parse tree 的 hash ID,写入 pg_stat_activity.query_id,同时供 pg_stat_statements 和日志使用。
  • auto:仅在某个需要 query_id 的子系统被激活时自动开启——比如加载了 pg_stat_statements,或配置了依赖 query_id 的日志前缀。

核心算法是对查询的 parse tree 做标准化后取 hash,相同逻辑结构的 SQL 会得到相同 ID,常量差异不影响结果。这意味着 SELECT * FROM t WHERE id = 1SELECT * FROM t WHERE id = 999 的 query_id 一致——这正是性能分析需要的聚合粒度。

为什么默认是 off

Christophe Pettus 在演讲中强调了这一点:如果默认 on,每一个 backend 进程的每一次查询执行都要额外走一遍 hash 计算。对于短查询密集的场景(OLTP、连接池高频轮询),这笔开销累积起来不可忽视。PostgreSQL 的原则是——功能默认关闭,需要时显式开启,让用户自己决定要不要付这个代价。

auto 模式是折中方案:你只要加载了 pg_stat_statements,query_id 就自动算起来,不需要手动设 on;但如果你没用到任何依赖它的功能,它就安静地待在 off,不收一分税。

实践:开启 query_id 并跨视图关联

下面是一个可以直接跑的示例,演示开启 compute_query_id 后如何把 pg_stat_activitypg_stat_statements 通过 query_id 关联起来。

1. 配置开启

postgresql.conf 中(或用 ALTER SYSTEM):

-- 方式一:显式开启(所有查询都计算 ID)
ALTER SYSTEM SET compute_query_id = on;
-- 方式二:自动模式(仅当 pg_stat_statements 等被加载时才计算)
-- ALTER SYSTEM SET compute_query_id = auto;

-- 同时确保 pg_stat_statements 可用
ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements';
SELECT pg_reload_conf();

改完 shared_preload_libraries 需要重启:

pg_ctl restart -D /var/lib/postgresql/data

重启后创建扩展并启用统计:

CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

2. 执行几条测试查询

-- 在会话 A 中执行
SELECT * FROM pg_class WHERE relname = 'pg_stat_activity';
SELECT * FROM pg_class WHERE relname = 'pg_stat_statements';

-- 在会话 B 中执行(不同常量,相同结构)
SELECT * FROM pg_class WHERE relname = 'pg_type';

3. 通过 query_id 关联两个视图

SELECT
  a.query_id,
  a.query AS current_query_text,
  s.calls,
  s.total_exec_time,
  s.mean_exec_time
FROM pg_stat_activity a
JOIN pg_stat_statements s
  ON a.query_id = s.query_id
WHERE a.state = 'active'
  AND a.query_id != 0;

在 14 之前,这个 JOIN 做不到——pg_stat_activity 没有 query_id 列,你只能拿 a.querys.query 做文本比对,常量不同就匹配不上。现在,上面三条 SELECT 的 query_id 中,前两条相同(常量不同但 parse tree 结构一致的是后两条),关联变得可靠且精确。

4. 在日志中输出 query_id

ALTER SYSTEM SET log_line_prefix = '%m [%p] qid=%Q ';
SELECT pg_reload_conf();

%Q 是 14 新增的 log_line_prefix 占位符,直接输出当前查询的 query_id。日志中出现:

2024-06-15 10:23:45.123 CST [12345] qid=6835270199357356800 LOG: statement: SELECT ...

拿着这个 qid 值去 pg_stat_statements 里查,就能把日志里的慢查询和统计视图里的历史聚合数据对上。

什么时候该开,什么时候别动

场景 建议 理由
已在用 pg_stat_statements autoon 都行 auto 下加载扩展会自动启用,无需额外配置
短查询高频 OLTP,暂不需要统计 保持 off 每条查询多一次 hash,QPS 极高时累积开销可观
需要日志与统计关联排查慢查询 on + %Q 日志前缀 这是 query_id 最直接的价值点
只偶尔做性能分析 auto 用的时候开,不用的时候零开销

一个容易忽略的细节:compute_query_id = auto 时,如果你后来 DROP EXTENSION pg_stat_statements 且日志前缀不再引用 %Q,query_id 计算会自动停掉。这是 auto 的语义——"有人需要我才干活,没人需要我就休息"。

留意边界

  • query_id 是 hash 值,存在碰撞可能,不同 SQL 结构碰出同一个 ID 的概率极低但非零。做精确文本归类时,仍需结合 query 字段复核。
  • PL/pgSQL 内的动态 SQL(EXECUTE)每次执行可能生成不同 query_id,因为字符串是运行时拼接的,parse tree 不稳定。
  • 14 之前的 pg_stat_statements 有自己的 query_id 生成逻辑,升级后旧数据中的 query_id 与新系统不兼容,统计需要重置:SELECT pg_stat_statements_reset();

compute_query_id 不起眼,但它解决的是一个长期痛点——让 PostgreSQL 的观测子系统终于能说同一种语言。开不开启取决于你的观测需求是否值得那一点 CPU 税,auto 模式让这个决定变得不那么纠结。


相关推荐