pg_stat_statements 把语句执行归组并累计代价。它既能找出调用很频繁、单次很短但总耗时很大的查询,也能帮助定位单次特别慢的查询。这是两个不同问题,任何一种排名都不能单独证明某项修改会改善业务延迟。执行时间也不等于处理器消耗、存储等待或完整请求时长。先明确要优化的现象,再选择统计列,才能避免把排行榜本身当成优化结论。
归组有用,但有明确边界 PostgreSQL 18 通常把代表性查询文本中的常量替换为参数符号,并按分析后的结构归组。条目身份包含用户、数据库、查询标识,以及是否为顶层执行;查询文本并不是完整身份。对象解析不同,可能让相同文本进入不同条目;相关分组范围内,也存在哈希碰撞把无关结构合并的微小可能。因此监控应保留足够身份字段,而不是只截取一段 SQL 当作永远可靠的主键。
代表性文本用于诊断,不是每条原始请求的审计流水。有些工具语句或归一化失效场景仍会保留字面量,应限制访问并避免把秘密嵌进 SQL。查询标识不承诺跨大版本或任意目录变化保持稳定。PostgreSQL 18 还会对某些长度不同的常量列表归一化;换到另一个版本时,需要查对应手册,不能把当前显示形式当成所有历史版本的统一行为。
根据问题选择排名 总执行时间适合寻找累计数据库工作量大的对象。平均值、最大值与离散程度描述单次调用的分布特点,但它们不是分位数,也不能单独指出原因。波动可能来自参数、缓存状态、竞争或计划变化。看到标准差较大时,应继续检查计划和等待,不能直接宣布发生了锁竞争;平均较高也不自动意味着规划器选择错误。
并发会话的执行区间会重叠,所以累计执行时长可以超过统计窗口的墙钟时间。这不证明服务器重启过,也不证明数据无效。启用计划统计时,规划耗时另有字段。共享块读取表示数据库缓冲管理器发起的读取,仍可能由操作系统缓存满足。乘以服务器块大小后得到的是请求读取量,不是物理设备实际流量;开启输入输出计时增加耗时测量,基础块计数并不依赖它。
坏掉的样子 模块需要在服务器启动时通过 shared_preload_libraries 加载,检查所用数据库也需要创建 SQL 扩展。查询标识计算必须可用,通常由 compute_query_id 的 auto 或 on 提供,也可以来自外部模块。缺少配置、权限限制和关闭跟踪是不同失败,应读取实际错误,而不是把任何缺失条目都解释成没有负载发生。监控查询能够运行,也不代表它有权看见所有其他用户的文本。
可靠增量要求条目身份与采集窗口都可比较。PostgreSQL 18 提供每条记录的 stats_since、最小最大值的独立起点,以及全局重置和淘汰信息。定向重置或淘汰可以只破坏一个条目的连续性,而不重置整个视图。差值非负也不足以证明窗口连续:重置之后积累足够工作,可能把异常掩盖。新条目和窗口变化应重新建立基线,不能跨越它们直接做减法。
归一化受条目身份和采集连续性约束。修订夹具绑定四项身份字段;执行仍有保留,未声明新的 SQL 或耗时结果。 快速验证 使用全新且没有其他负载的 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。
...