Skip to content

pg_stat_statements SQL 统计 ​

pg_stat_statements 是 PostgreSQL 生态中最重要的性能诊断扩展,没有之一。它按「归一化后的 SQL」聚合统计执行信息:每条语句被调用了多少次、总共花了多少时间、平均/最大耗时多少、读了多少块、写了多少 WAL、产生多少临时文件。有了它,才能回答「这个数据库到底慢在哪些 SQL 上」这个根本问题——前面两页的实时视图只能告诉你「此刻谁在跑」,而它能告诉你「这段时间里谁最重」。

它的工作方式是把每条 SQL 归一化成参数化的「语句模板」(常量替换成 $1、$2),然后以模板为粒度累计计数。这带来两个特性:一是相似语句(比如只有 WHERE 值不同)会合并成一条统计,统计维度不会爆炸;二是它统计的是「自上次重置以来」的累计值,属于历史聚合而非实时快照。

本页覆盖三块内容:安装与配置、视图字段的完整解读、以及一组拿来即用的 Top SQL 查询模板。生产环境的性能巡检、容量评估、慢查询治理,都以它为数据源。

前置知识

建议先阅读:pg_locks 锁监控

基础概念 ​

理解 pg_stat_statements,先要接受它的三个核心设计:

概念说明
归一化(normalization)常量被替换成 $n 占位符,使同类语句合并统计
queryid归一化语句的哈希标识,同一模板共享一个 queryid
统计粒度按 (userid, dbid, queryid, toplevel) 聚合,一行 = 一个用户在某个库上执行的一个语句模板
统计周期从上次 pg_stat_statements_reset()(或实例启动)到现在的累计值
top / all / none统计范围开关:只统计顶层语句 / 连函数内部语句一起 / 完全关闭

它统计什么、不统计什么 ​

  • 统计:DML、查询、部分工具语句(track_utility = on 时包括 COPY、VACUUM、CREATE TABLE 等)、以及 track = all 时存储过程/函数内部的嵌套语句。
  • 不统计:track = none 时全部关闭;跨连接的 PREPARE/EXECUTE 细节、简单查询协议与扩展协议统一归一化处理(对使用者透明)。
  • 不提供:逐次执行明细、语句执行计划、执行时的等待事件——这些分别要靠日志、auto_explain 与 pg_stat_activity 补充。

归一化示例 ​

sql
-- 应用发的两条语句
SELECT * FROM orders WHERE customer_id = 42 AND status = 'paid';
SELECT * FROM orders WHERE customer_id = 99 AND status = 'shipped';

-- 归一化后合并为一条统计,query 列显示为:
SELECT * FROM orders WHERE customer_id = $1 AND status = $2
sql
-- 查看实际归一化结果
SELECT queryid, calls, query
FROM pg_stat_statements
ORDER BY calls DESC
LIMIT 5;

需要注意:queryid 是「语句分析树」的哈希,不是查询文本的哈希——等价但写法不同的 SQL 可能得到不同 queryid,而同一条 SQL 在大版本升级后 queryid 也可能变化。不要把 queryid 当作跨版本稳定的持久键。

语法与用法 ​

安装两步走 ​

第一步,把扩展加入预加载列表并重启(shared_preload_libraries 的修改只能重启生效):

conf
# postgresql.conf
shared_preload_libraries = 'pg_stat_statements'

# 可选:计算内核级 query_id(PG14+,auto 即「有消费者时自动开启」)
compute_query_id = auto

# 可选:让块读写时间可统计(默认 off,开启后有少量额外开销)
track_io_timing = on
bash
# 重启实例使 shared_preload_libraries 生效
pg_ctl restart -D /var/lib/postgresql/18/main

第二步,在需要观测的数据库里创建扩展:

sql
CREATE EXTENSION pg_stat_statements;
text
CREATE EXTENSION
sql
-- 验证可用
SELECT * FROM pg_stat_statements LIMIT 1;

说明:统计是按数据库分别记录的,扩展本身可以在多个库里各建一份(常见做法是只在业务库建);shared_preload_libraries 是集簇级的,设置一次即可。

常用配置参数 ​

以下参数都带 pg_stat_statements. 前缀,多数支持 reload 生效(max 与 save 例外,需重启):

conf
# postgresql.conf 或 ALTER SYSTEM

# 统计范围:top(默认,只统计顶层语句) / all(连函数内语句) / none(关闭)
pg_stat_statements.track = top

# 是否统计工具语句(COPY、VACUUM、CREATE 等),默认 on
pg_stat_statements.track_utility = on

# 最多保留多少条语句模板,默认 5000;超出时淘汰最少使用的
pg_stat_statements.max = 10000

# 关机时把统计保存到 $PGDATA/pg_stat_statements.save,重启后延续,默认 on
pg_stat_statements.save = on

# 是否统计语句的规划(planning)耗时与次数,PG13+,默认 off
pg_stat_statements.track_planning = off
sql
-- 在线查看与修改(部分参数)
SHOW pg_stat_statements.track;
ALTER SYSTEM SET pg_stat_statements.track = all;
SELECT pg_reload_conf();

基础查询 ​

sql
-- 按总耗时排 Top 10
SELECT queryid,
       calls,
       round(total_exec_time::numeric, 1) AS total_ms,
       round(mean_exec_time::numeric, 3) AS mean_ms,
       rows,
       left(query, 80) AS query_head
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

-- 只看当前数据库、排除扩展自身的查询
SELECT *
FROM pg_stat_statements
WHERE dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
  AND query NOT LIKE '%pg_stat_statements%'
ORDER BY mean_exec_time DESC
LIMIT 10;

详细说明 ​

视图字段全览(一):标识与执行 ​

字段类型说明
useridoid执行该语句的角色 OID,关联 pg_roles 取用户名
dbidoid数据库 OID
toplevelboolean是否顶层语句(PG13+;track = all 时函数内语句此列为 false)
queryidbigint语句模板的哈希标识
querytext归一化后的语句文本
callsbigint累计调用次数
rowsbigint累计返回或影响的行数(不含函数内部处理)
total_exec_timedouble precision累计执行耗时(毫秒),含所有子操作
min_exec_time / max_exec_timedouble precision单次执行的最小/最大耗时(毫秒)
mean_exec_timedouble precision平均单次耗时(毫秒)
stddev_exec_timedouble precision耗时标准差,越大说明执行差异越大

total_exec_time 是旧版 total_time 在 PG13 的更名——网上老文章如果出现 total_time,说明针对 PG12 及更早版本。max_exec_time 值得单独盯:平均值健康但最大值巨大的语句,往往意味着偶发的慢执行(参数敏感、锁等待、计划翻转)。

视图字段全览(二):规划统计(PG13+) ​

字段类型说明
plansbigint累计规划次数(prepared statement 复用计划时不重复计)
total_plan_timedouble precision累计规划耗时(毫秒)
min_plan_time / max_plan_time / mean_plan_time / stddev_plan_timedouble precision规划耗时统计

规划统计由 pg_stat_statements.track_planning 控制(默认 off)。普通 OLTP 的规划耗时通常远小于执行耗时,只有 mean_plan_time 显著时才值得打开它分析,因为规划统计会带来额外开销。

视图字段全览(三):缓冲块统计 ​

字段类型说明
shared_blks_hitbigint命中共享缓冲区(shared_buffers)的块数
shared_blks_readbigint需要从磁盘读入共享缓冲区的块数
shared_blks_dirtiedbigint被该语句弄脏的共享块数
shared_blks_writtenbigint由该语句写出的共享块数
local_blks_hit / local_blks_read / local_blks_dirtied / local_blks_writtenbigint临时表(本地缓冲)的对应统计
temp_blks_read / temp_blks_writtenbigint排序、哈希、物化等操作溢出到临时文件的块读写量

shared_blks_read 大的语句是磁盘 I/O 的主要嫌疑人;temp_blks_written 大说明工作内存不够、大量数据落盘,通常该调 work_mem 或优化排序/去重逻辑(见《work_mem 工作内存》一页)。

视图字段全览(四):I/O 时间与 WAL ​

字段类型说明
blk_read_time / blk_write_timedouble precision该语句花在共享/本地块读写上的时间(毫秒),需 track_io_timing = on
temp_blk_read_time / temp_blk_write_timedouble precision临时文件块读写时间(PG15+ 拆分出来单独统计)
wal_recordsbigint累计产生的 WAL 记录数(PG13+)
wal_fpibigint累计产生的全页镜像(FPI)数(PG13+)
wal_bytesdouble precision累计产生的 WAL 字节数(PG13+)
wal_buffers_fullbigintWAL 缓冲区写满的次数(PG14+)

WAL 三兄弟是「谁在制造复制压力与归档压力」的最佳答案。wal_fpi 偏高的语句往往涉及大量页面首次触碰(如整表 UPDATE),结合 wal_buffers_full 还能判断 WAL 缓冲区是否偏小。

视图字段全览(五):JIT 与重置时间 ​

字段类型说明
jit_functionsbigint该语句累计 JIT 编译的函数数
jit_generation_timedouble precision累计 JIT 代码生成耗时
jit_inlining_count / jit_inlining_timebigint / double precisionJIT 内联的次数与耗时
jit_deforming_count / jit_deforming_timebigint / double precisionJIT 元组变形的次数与耗时
stats_resettimestamptz整个视图的统计最后重置时间(所有行相同)

JIT 统计只在 jit = on 且语句达到成本阈值时才有意义。jit_generation_time 相对执行总耗时不划算时,就是「关掉这条语句 JIT」的论据。

Top SQL 查询模板 ​

以下模板覆盖了性能巡检的绝大多数场景,建议原样入库:

sql
-- Top 1:总耗时排行(整体优化收益最大的语句)
SELECT round(total_exec_time::numeric / 1000, 1) AS total_sec,
       calls,
       round(mean_exec_time::numeric, 2) AS mean_ms,
       round((100 * total_exec_time /
              sum(total_exec_time) OVER ())::numeric, 1) AS pct_total,
       left(query, 100) AS query_head
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

-- Top 2:平均耗时排行(单次最慢的语句)
SELECT calls,
       round(mean_exec_time::numeric, 2) AS mean_ms,
       round(max_exec_time::numeric, 2) AS max_ms,
       left(query, 100) AS query_head
FROM pg_stat_statements
WHERE calls > 100
ORDER BY mean_exec_time DESC
LIMIT 10;

-- Top 3:缓冲区命中差(磁盘读最重的语句)
SELECT shared_blks_read,
       shared_blks_hit,
       round(100.0 * shared_blks_hit /
             nullif(shared_blks_hit + shared_blks_read, 0), 1) AS hit_ratio,
       calls,
       left(query, 100) AS query_head
FROM pg_stat_statements
WHERE shared_blks_read > 0
ORDER BY shared_blks_read DESC
LIMIT 10;

-- Top 4:临时文件大户(排序/哈希溢出)
SELECT temp_blks_written,
       pg_size_pretty((temp_blks_written *
                       current_setting('block_size')::bigint)::bigint) AS temp_written,
       calls,
       left(query, 100) AS query_head
FROM pg_stat_statements
WHERE temp_blks_written > 0
ORDER BY temp_blks_written DESC
LIMIT 10;

-- Top 5:WAL 制造机(复制与归档压力来源)
SELECT pg_size_pretty(wal_bytes::bigint) AS wal_total,
       wal_records,
       wal_fpi,
       calls,
       left(query, 100) AS query_head
FROM pg_stat_statements
WHERE wal_bytes > 0
ORDER BY wal_bytes DESC
LIMIT 10;

-- Top 6:调用次数最高(QPS 主力,最值得做缓存或索引微优化)
SELECT calls,
       rows,
       round(rows::numeric / nullif(calls, 0), 2) AS rows_per_call,
       round(mean_exec_time::numeric, 3) AS mean_ms,
       left(query, 100) AS query_head
FROM pg_stat_statements
ORDER BY calls DESC
LIMIT 10;

track = all 与 toplevel ​

默认 track = top 只统计顶层语句。存储过程、函数内部执行的语句不会被单独统计——它们的耗时全部归到「调用那个函数的那条语句」头上。

sql
-- 打开嵌套语句统计(reload 生效)
ALTER SYSTEM SET pg_stat_statements.track = all;
SELECT pg_reload_conf();

打开后,函数内语句会以 toplevel = false 出现,可以单独分析:

sql
-- 只看函数内部的慢语句
SELECT queryid, calls, round(mean_exec_time::numeric, 3) AS mean_ms, query
FROM pg_stat_statements
WHERE NOT toplevel
ORDER BY total_exec_time DESC
LIMIT 10;

all 模式统计开销更大,一般只在深入剖析函数性能时临时开启。

重置与 pg_stat_statements_info ​

sql
-- 全量重置
SELECT pg_stat_statements_reset();

-- PG13+ 支持选择性重置:指定用户、数据库、语句
SELECT pg_stat_statements_reset(
    (SELECT oid FROM pg_roles WHERE rolname = 'app'),
    (SELECT oid FROM pg_database WHERE datname = 'appdb'),
    NULL
);

PG14+ 提供了 pg_stat_statements_info 视图,记录统计机制自身的运行信息:

sql
SELECT dealloc, stats_reset FROM pg_stat_statements_info;
字段说明
dealloc因超出 pg_stat_statements.max 而被淘汰的语句统计条数
stats_reset统计最后一次重置的时间

dealloc 持续增长说明 max 设小了——热语句统计被反复挤出再重建,Top 排行会失真,应调大 max 或增加采样频率。

版本演进速览 ​

  • PG13:视图大改版——total_time 更名 total_exec_time;新增 toplevel、plans 与四个规划耗时字段;新增 wal_records / wal_fpi / wal_bytes;pg_stat_statements_reset() 支持按用户/库/语句选择性重置。
  • PG14:queryid 改由内核统一计算(compute_query_id),pg_stat_activity.query_id 与本扩展打通;新增 wal_buffers_full;新增 pg_stat_statements_info 视图。
  • PG15:新增 temp_blk_read_time / temp_blk_write_time,临时文件 I/O 时间与共享/本地块 I/O 时间分开统计。
  • 更早版本(PG12 及以前)没有 WAL、规划、toplevel 字段,旧列名 total_time 需要改写为 total_exec_time 才能在新版本运行。

多用户与多数据库视角 ​

pg_stat_statements 按 (userid, dbid, queryid, toplevel) 聚合,意味着同一条语句在不同用户、不同库上的统计各占一行。利用这一点可以做「责任划分」:

sql
-- 用户名 + 库名联查,定位「谁在哪个库跑重查询」
SELECT r.rolname,
       d.datname,
       s.calls,
       round(s.total_exec_time::numeric / 1000, 1) AS total_sec,
       round(s.mean_exec_time::numeric, 2) AS mean_ms,
       left(s.query, 60) AS query_head
FROM pg_stat_statements s
JOIN pg_roles r ON r.oid = s.userid
JOIN pg_database d ON d.oid = s.dbid
ORDER BY s.total_exec_time DESC
LIMIT 15;
sql
-- 只看某个用户的负载画像(如报表账号)
SELECT calls,
       round(total_exec_time::numeric / 1000, 1) AS total_sec,
       left(query, 80) AS query_head
FROM pg_stat_statements
WHERE userid = (SELECT oid FROM pg_roles WHERE rolname = 'report_user')
ORDER BY total_exec_time DESC;

两个实用结论:

  • 给报表、ETL 分配专用角色后,这条联查能直接回答「重查询是谁家的」——这是权限分账与优化责任划分的基础。所有业务共用一个应用角色的话,这些信息就被抹平了。
  • 统计按库隔离,pg_stat_statements_reset() 也支持按库重置(PG13+);跨库对比负载时逐库取数再汇总。

与 auto_explain、慢语句日志的分工 ​

三件工具回答三个不同的问题,混用不如分工:

工具回答的问题数据形态成本
pg_stat_statements哪些语句长期最重(聚合画像)归一化聚合低,可常开
log_min_duration_statement超阈值语句的逐条流水(含原始参数)日志行中,看日志量
auto_explain慢语句当时的执行计划长什么样日志中的计划中,只记慢的

组合套路:用 pg_stat_statements 找到优化目标 → 怀疑计划问题时对目标角色临时开启 auto_explain 抓计划 → 需要还原真实参数值(归一化后的 $1)时回到慢语句日志找原始行。慢语句日志的配置细节由慢查询章节展开,本章日志部分也会涉及。

容量与开销评估 ​

扩展的内存占用与语句模板数线性相关(每条记录含归一化文本等固定开销)。评估口径:

sql
-- 当前模板条数与淘汰情况
SELECT (SELECT count(*) FROM pg_stat_statements) AS entries,
       i.dealloc,
       i.stats_reset
FROM pg_stat_statements_info i;
  • entries 长期逼近 pg_stat_statements.max 且 dealloc 在涨,说明槽位不足——热语句被挤出后重新统计,Top 榜单会失真。调大 max(需重启)或靠周期采样弥补。
  • 反过来,把 max 设到几十万也没有收益:绝大多数库的有效模板数在数千到数万之间,多余槽位只是浪费内存。
  • track = all 与 track_planning = on 都会增加每语句的处理开销,按需临时开启是更稳妥的姿势。

与 pg_stat_activity.query_id 的联动边界 ​

PG14 起,内核可以用 compute_query_id 统一计算 query id,pg_stat_activity.query_id 与本扩展的 queryid 同源,于是「正在跑的语句 ↔ 历史统计」可以直接关联(示例四演示过)。但要知道这条链路的三个边界:

  1. compute_query_id = off 时 pg_stat_activity.query_id 恒为 NULL,联动失效;auto 表示「有消费者才计算」,要稳定联动就显式设 on。
  2. 关联键只在同一次实例生命周期内可靠;重启后 queryid 理论上可能变化,不要把它当跨重启的持久键。
  3. 匹配是「模板级」的——同一模板的不同次执行共享一个 id,这正好是想要的粒度;归一化文本相同的两条语句也会合并,解读时知道即可。

实战示例 ​

示例一:每日巡检报告 SQL ​

sql
-- 一条查询同时给出「总耗时、平均耗时、读盘、临时文件」四个维度
WITH top_total AS (
    SELECT queryid,
           row_number() OVER (ORDER BY total_exec_time DESC) AS rn
    FROM pg_stat_statements
),
top_mean AS (
    SELECT queryid,
           row_number() OVER (ORDER BY mean_exec_time DESC) AS rn
    FROM pg_stat_statements
    WHERE calls > 50
),
top_io AS (
    SELECT queryid,
           row_number() OVER (ORDER BY shared_blks_read DESC) AS rn
    FROM pg_stat_statements
)
SELECT s.queryid,
       t.rn AS rank_total,
       m.rn AS rank_mean,
       i.rn AS rank_io,
       s.calls,
       round(s.total_exec_time::numeric / 1000, 1) AS total_sec,
       round(s.mean_exec_time::numeric, 2) AS mean_ms,
       s.shared_blks_read,
       s.temp_blks_written,
       left(s.query, 60) AS query_head
FROM pg_stat_statements s
LEFT JOIN top_total t USING (queryid)
LEFT JOIN top_mean m USING (queryid)
LEFT JOIN top_io i USING (queryid)
WHERE t.rn <= 10 OR m.rn <= 10 OR i.rn <= 10
ORDER BY coalesce(t.rn, 99), s.total_exec_time DESC;

三个榜单同时进前十的语句,就是当日最值得投入的优化目标。

示例二:定位「偶发慢」语句 ​

平均耗时不高、但最大耗时巨大的语句,通常是偶发慢的来源:

sql
SELECT calls,
       round(mean_exec_time::numeric, 2) AS mean_ms,
       round(max_exec_time::numeric, 2) AS max_ms,
       round((max_exec_time / nullif(mean_exec_time, 0))::numeric, 1) AS max_over_mean,
       stddev_exec_time,
       left(query, 100) AS query_head
FROM pg_stat_statements
WHERE calls > 10
  AND max_exec_time > 1000        -- 单次超过 1 秒
  AND mean_exec_time < 50         -- 平均却不到 50ms
ORDER BY max_over_mean DESC
LIMIT 10;

进一步用 EXPLAIN ANALYZE 复现单条语句,确认是否参数敏感计划(见《EXPLAIN ANALYZE 实际执行》一页)。

示例三:全量重置后观察一个周期 ​

统计周期越长,「平均值」越容易被历史稀释。做优化验证时常用「重置 → 观察一小时 → 读数」的循环:

bash
# 定时任务示例:每小时归档 Top SQL 到自建表
psql -c "INSERT INTO sql_stats_history
         SELECT now(), * FROM pg_stat_statements;"
psql -c "SELECT pg_stat_statements_reset();"
sql
-- 归档表
CREATE TABLE sql_stats_history (
    captured_at  timestamptz,
    dbid         oid,
    userid       oid,
    queryid      bigint,
    query        text,
    calls        bigint,
    total_exec_time double precision,
    mean_exec_time  double precision,
    rows            bigint
);

这样既保住了长期趋势,又让重置后的统计保持「新鲜」。

示例四:关联 pg_stat_activity 验证 ​

实时视图与累计统计相互印证:巡检时发现某条慢语句正在执行,去 pg_stat_statements 查它的历史画像:

sql
-- 当前正在跑的语句的历史统计
SELECT a.pid,
       a.query_start,
       now() - a.query_start AS running_for,
       s.calls,
       s.mean_exec_time,
       s.max_exec_time,
       left(a.query, 80) AS current_query
FROM pg_stat_activity a
LEFT JOIN pg_stat_statements s ON s.queryid = a.query_id
WHERE a.state = 'active'
  AND a.query_id IS NOT NULL
ORDER BY running_for DESC;

PG14+ 两个视图通过 queryid 打通,这种「实时 + 历史」的组合排查是标配手法。

示例五:优化前后基线对比 ​

目标:验证给 orders 加了复合索引后,那条高频查询真的变快了。

sql
-- 1) 优化前:锁定目标语句的基线(记录下来)
SELECT queryid, calls,
       round(mean_exec_time::numeric, 3) AS mean_ms,
       shared_blks_read,
       round(100.0 * shared_blks_hit /
             nullif(shared_blks_hit + shared_blks_read, 0), 1) AS hit_pct
FROM pg_stat_statements
WHERE query LIKE 'SELECT * FROM orders WHERE customer_id%';
text
      queryid      | calls | mean_ms | shared_blks_read | hit_pct
-------------------+-------+---------+------------------+---------
  -371940268581990 | 12043 | 142.551 |         18402213 |    61.2
(1 row)
sql
-- 2) 建索引(CONCURRENTLY,业务不受阻)
CREATE INDEX CONCURRENTLY orders_customer_created_idx
    ON orders (customer_id, created_at DESC);

-- 3) 低峰期重置统计,让新基线从零开始
--    (代价是全库其他语句的历史统计清零,评估后再做)
SELECT pg_stat_statements_reset();

-- 4) 积累足够样本量后再次读取同一条语句
text
      queryid      | calls | mean_ms | shared_blks_read | hit_pct
-------------------+-------+---------+------------------+---------
  -371940268581990 |  9812 |   3.204 |            20413 |    99.8
(1 row)

平均耗时 142ms → 3.2ms、读块量降三个数量级——数据说话,优化生效。把「优化前 / 优化后」两行快照贴进变更记录,是性能工作最可复用的交付物。

注意事项 ​

统计粒度是「模板」不是「单次执行」。 mean_exec_time 掩盖了单次波动,max_exec_time 与 stddev_exec_time 才能暴露偶发慢。只看平均值的优化常常会漏掉真正的痛点。

queryid 不是稳定主键。 它来自分析树哈希:SQL 写法变化、大版本升级、参数化差异都可能改变 queryid。拿它做长期对比时要理解这个边界,跨周期对比用「归一化文本」更稳妥。

共享缓冲区命中率只统计 shared_buffers 层面。 shared_blks_read 表示「没在 shared_buffers 命中、需要从存储读入」,但操作系统的页缓存仍可能让这次「磁盘读」实际发生在内存里。命中率低于预期时,先确认 shared_buffers 是否偏小,再谈存储。

统计在实例崩溃时会丢失。 非 save 正常关机场景(如崩溃后恢复)统计从零开始;把「统计重置时间」纳入巡检输出,避免误读异常小的累计值。

track = all 有可见的开销。 每条嵌套语句都要做一次归一化与统计,函数调用密集的负载下开销可感。默认 top 就够日常使用,深挖函数时临时切 all。

max 太小会让热语句被淘汰。 出现 pg_stat_statements_info.dealloc 持续增长时,Top 榜单会缺少真正的热语句;调整后旧统计不清零,但已淘汰的不会回来。

工具语句也会占统计槽位。 track_utility = on 时 VACUUM、COPY、DDL 都会进入统计——这对容量分析有价值,但批量 COPY 可能长期霸榜,解读时要区分业务 SQL 与运维 SQL。

它不记录执行计划与等待原因。 「这条语句平均 3 秒」只是起点,为什么慢要交给 EXPLAIN ANALYZE、pg_stat_activity.wait_event 与日志系统。工具组合使用才闭环。

最佳实践 ​

  1. 所有生产实例标配 pg_stat_statements。 预加载 + 建扩展的开销极低,而它提供的信息无可替代。新实例初始化清单里应该有这两行配置与一条 CREATE EXTENSION。
  2. 开启 track_io_timing = on。 毫秒级 I/O 时间是区分「CPU 型慢」与「IO 型慢」的关键,开销在绝大多数硬件上可以忽略。
  3. 把 Top SQL 模板做成定时采样落库。 每小时快照一次 pg_stat_statements 到自建历史表(或接监控系统的 exporter),既保留趋势,又支持「优化前后对比」。
  4. 巡检看四个榜单:总耗时、平均耗时、读盘、临时文件。 单看任何一个都会跑偏;四个榜单的交集就是优化优先级。
  5. 对 calls 极高、单次极快的语句做「微优化」。 平均 1ms、每天百万次的语句,砍掉 0.2ms 就是每天几十秒的 CPU——这类收益 EXPLAIN 里看不明显,全靠统计视图发现。
  6. 用「重置 → 观察 → 读数」验证每次优化。 优化前重置统计,观察足够样本量后对比 calls、mean_exec_time、shared_blks_read,用数据证明优化有效,而不是凭感觉。
  7. pg_stat_statements.max 按库规模设余量。 高并发多租户库的语句模板数轻松过万,默认 5000 可能不够,配合 dealloc 计数确认容量是否合理。
  8. 升级大版本前导出统计基线。 queryid 与部分字段跨版本会变,升级前把当时的 Top SQL 快照存档,升级后重新采样对比,判断性能回归。
  9. 权限上给监控账号只读视图的访问即可。 视图的查询文本里可能含业务敏感信息(即便常量已参数化),监控账号用普通只读权限 + pg_read_all_stats 精细控制,不要顺手给超级用户。
  10. 与 auto_explain、慢日志组合使用。 统计视图找「哪条最慢」,日志与 auto_explain 拿「慢时的现场」,两者互补而非互替。
  11. 把 wal_bytes 榜纳入复制与归档容量规划。 WAL 生成大户决定了归档带宽与备机恢复速度,扩容、做大批量数据迁移前先看这张榜。
  12. 解读统计数据永远带上 stats_reset 时间。 「总量」没有「时间窗」就没有意义,报告里注明统计周期是基本功。

参考链接 ​