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 = 1 和 SELECT * 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_activity 和 pg_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.query 和 s.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 |
auto 或 on 都行 |
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 模式让这个决定变得不那么纠结。