pg_stat_statements 把语句执行归组并累计代价。它既能找出调用很频繁、单次很短但总耗时很大的查询,也能帮助定位单次特别慢的查询。这是两个不同问题,任何一种排名都不能单独证明某项修改会改善业务延迟。执行时间也不等于处理器消耗、存储等待或完整请求时长。先明确要优化的现象,再选择统计列,才能避免把排行榜本身当成优化结论。
归组有用,但有明确边界
PostgreSQL 18 通常把代表性查询文本中的常量替换为参数符号,并按分析后的结构归组。条目身份包含用户、数据库、查询标识,以及是否为顶层执行;查询文本并不是完整身份。对象解析不同,可能让相同文本进入不同条目;相关分组范围内,也存在哈希碰撞把无关结构合并的微小可能。因此监控应保留足够身份字段,而不是只截取一段 SQL 当作永远可靠的主键。
代表性文本用于诊断,不是每条原始请求的审计流水。有些工具语句或归一化失效场景仍会保留字面量,应限制访问并避免把秘密嵌进 SQL。查询标识不承诺跨大版本或任意目录变化保持稳定。PostgreSQL 18 还会对某些长度不同的常量列表归一化;换到另一个版本时,需要查对应手册,不能把当前显示形式当成所有历史版本的统一行为。
根据问题选择排名
总执行时间适合寻找累计数据库工作量大的对象。平均值、最大值与离散程度描述单次调用的分布特点,但它们不是分位数,也不能单独指出原因。波动可能来自参数、缓存状态、竞争或计划变化。看到标准差较大时,应继续检查计划和等待,不能直接宣布发生了锁竞争;平均较高也不自动意味着规划器选择错误。
并发会话的执行区间会重叠,所以累计执行时长可以超过统计窗口的墙钟时间。这不证明服务器重启过,也不证明数据无效。启用计划统计时,规划耗时另有字段。共享块读取表示数据库缓冲管理器发起的读取,仍可能由操作系统缓存满足。乘以服务器块大小后得到的是请求读取量,不是物理设备实际流量;开启输入输出计时增加耗时测量,基础块计数并不依赖它。
坏掉的样子
模块需要在服务器启动时通过 shared_preload_libraries 加载,检查所用数据库也需要创建 SQL 扩展。查询标识计算必须可用,通常由 compute_query_id 的 auto 或 on 提供,也可以来自外部模块。缺少配置、权限限制和关闭跟踪是不同失败,应读取实际错误,而不是把任何缺失条目都解释成没有负载发生。监控查询能够运行,也不代表它有权看见所有其他用户的文本。
可靠增量要求条目身份与采集窗口都可比较。PostgreSQL 18 提供每条记录的 stats_since、最小最大值的独立起点,以及全局重置和淘汰信息。定向重置或淘汰可以只破坏一个条目的连续性,而不重置整个视图。差值非负也不足以证明窗口连续:重置之后积累足够工作,可能把异常掩盖。新条目和窗口变化应重新建立基线,不能跨越它们直接做减法。
快速验证
使用全新且没有其他负载的 PostgreSQL 18 可丢弃集群和数据库,预先加载模块、启用内置查询标识与顶层语句跟踪,并让测试角色能够看见和使用或安装扩展。保持角色、搜索路径与对象不变,实验期间不得有并发负载、重置或条目淘汰。在一个全新会话中执行下面 SQL,任何错误都应作为失败处理。成功时删除测试表和临时视图;错误可能留下这些对象,新安装的扩展也会保留。它不重置任何已有统计,因此不会为了演示排名而破坏监控窗口。
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
CREATE TABLE pgss_demo (id integer);
INSERT INTO pgss_demo SELECT generate_series(1,1000);
CREATE TEMP TABLE pgss_key (
userid oid NOT NULL, dbid oid NOT NULL,
toplevel boolean NOT NULL, queryid bigint NOT NULL CHECK (queryid <> 0),
PRIMARY KEY (userid,dbid,toplevel,queryid)
);
DO $$
DECLARE first_plan json; second_plan json; first_id bigint; second_id bigint;
BEGIN
IF current_setting('compute_query_id') NOT IN ('auto','on')
OR current_setting('pg_stat_statements.track') <> 'top' THEN
RAISE EXCEPTION 'requires built-in query identifiers and top-level tracking';
END IF;
EXECUTE 'EXPLAIN (VERBOSE, FORMAT JSON) SELECT sum(id) FROM pgss_demo WHERE id < 100'
INTO STRICT first_plan;
EXECUTE 'EXPLAIN (VERBOSE, FORMAT JSON) SELECT sum(id) FROM pgss_demo WHERE id < 500'
INTO STRICT second_plan;
first_id := (first_plan->0->>'Query Identifier')::bigint;
second_id := (second_plan->0->>'Query Identifier')::bigint;
IF first_id IS NULL OR first_id = 0 OR second_id IS DISTINCT FROM first_id THEN
RAISE EXCEPTION 'expected matching nonzero query identifiers';
END IF;
INSERT INTO pgss_key
SELECT (SELECT oid FROM pg_roles WHERE rolname=current_user),
(SELECT oid FROM pg_database WHERE datname=current_database()),
true,first_id;
END $$;
CREATE TEMP VIEW pgss_target AS
SELECT s.* FROM pg_stat_statements AS s JOIN pgss_key AS k
USING (userid,dbid,toplevel,queryid);
CREATE TEMP TABLE pgss_window AS
SELECT dealloc,stats_reset FROM pg_stat_statements_info;
SELECT sum(id) FROM pgss_demo WHERE id < 100;
CREATE TEMP TABLE pgss_first AS
SELECT userid,dbid,toplevel,queryid,calls,stats_since FROM pgss_target;
SELECT sum(id) FROM pgss_demo WHERE id < 500;
SELECT userid,dbid,toplevel,queryid,query,calls,rows,
round(total_exec_time::numeric,3) AS total_ms,
round(mean_exec_time::numeric,3) AS mean_ms,stats_since
FROM pgss_target;
DO $$ BEGIN
IF (SELECT count(*) FROM pgss_first) <> 1
OR (SELECT count(*) FROM pgss_target) <> 1
OR (SELECT count(*) FROM pgss_window) <> 1 THEN
RAISE EXCEPTION 'expected one identity-bound entry and collection baseline';
END IF;
IF NOT EXISTS (
SELECT 1 FROM pgss_first AS a JOIN pgss_target AS b
USING (userid,dbid,toplevel,queryid)
CROSS JOIN pgss_window AS w CROSS JOIN pg_stat_statements_info AS i
WHERE a.calls=1 AND b.calls=2 AND b.calls-a.calls=1
AND b.stats_since=a.stats_since
AND i.dealloc=w.dealloc AND i.stats_reset=w.stats_reset) THEN
RAISE EXCEPTION 'expected calls 1 then 2 in one unchanged collection window';
END IF;
END $$;
SELECT userid,dbid,toplevel,queryid,calls,
round(total_exec_time::numeric,3) AS total_ms,
round(mean_exec_time::numeric,3) AS mean_ms
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;
SELECT userid,dbid,toplevel,queryid,calls,
round(mean_exec_time::numeric,3) AS mean_ms
FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;
SELECT dealloc,stats_reset FROM pg_stat_statements_info;
SELECT round((sum(shared_blks_read)::numeric *
current_setting('block_size')::numeric)/1073741824,6) AS requested_read_gib
FROM pg_stat_statements;
DROP VIEW pgss_target;
DROP TABLE pgss_first,pgss_window,pgss_key;
DROP TABLE pgss_demo;
SELECT 'pgss fixture completed' AS completion;
两个求和结果应分别是四千九百五十和十二万四千七百五十。不带 ANALYZE 的 EXPLAIN 在不执行求和的情况下取得两个查询标识,要求二者相同且非零。目标视图使用该标识、当前用户、数据库和顶层标志做完整身份连接,不用代表性文本匹配。两份快照要求先一次、后两次调用,并核对 stats_since 与全局重置、淘汰元数据未变。这些检查依赖无其他负载的前提,不保证哈希永不碰撞,也不是并发监控方案。两个通用排名完全可能相同,它们展示的是排序依据的区别,不制造必然不一致的承诺。对双精度耗时先转换为 numeric,才可以使用指定小数位数的两参数 round。
保留的 PostgreSQL 18.6 日志只记录了前一版文本过滤 SQL 的单用户适配结果,没有执行本次修订的完整身份绑定夹具。纯源码与合成数据检查不增加 PostgreSQL 运行证据。客户端传输、并发会话、生产开销和监控权限仍未测试;耗时只作为观察,不作为稳定基准。根据生产排名采取行动之前,应采集窗口可比的快照,明确用户和数据库范围,理解条目对应的真实负载,并在修改之后重新测量应用结果。身份字段和窗口检查各有职责,不能用满足一项来代替另一项,也不能把一组示例期望当成数据库已经实际返回的结果。
来源
PostgreSQL 18 pg_stat_statements,归组键、耗时类型、归一化、重置与条目窗口,已核验 2026-10-15。
PostgreSQL 18 monitoring statistics,累计观察及缓冲读取与物理输入输出的区别,已核验 2026-10-15。
PostgreSQL 18 statistics configuration,查询标识与可选输入输出计时,已核验 2026-10-15。
PostgreSQL 18 EXPLAIN,VERBOSE 查询标识及不带 ANALYZE 的计划检查,已核验 2026-10-19。
PostgreSQL 18 PL/pgSQL statements,动态 EXPLAIN 与严格单行结果捕获,已核验 2026-10-19。