Skip to content

pg_stat_statements 深度使用 ​

核心问题:系统视图告诉我们"数据库整体怎么样",但无法回答"哪条 SQL 最吃资源"。pg_stat_statements 通过归一化 SQL 文本并聚合执行统计,让我们精准定位 Top N 问题 SQL。

1. 安装和启用 ​

postgresql.conf 配置 ​

ini
# 必须在 shared_preload_libraries 中加载
shared_preload_libraries = 'pg_stat_statements'

# 跟踪的 SQL 条数上限(建议 10000)
pg_stat_statements.max = 10000

# 按什么归一化:none / all / top / ltop
# top: 保留 top-level 常量值;ltop: 对 top-level 保留,嵌套子查询归一化
pg_stat_statements.track = top

# 跟踪所有语句还是仅 top-level
pg_stat_statements.track_utility = on

# 计时精度
pg_stat_statements.track_planning = off   # 是否跟踪计划生成时间(PG 13+)

启用扩展 ​

sql
-- 创建扩展(每个数据库需要单独启用)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- 验证安装
SELECT * FROM pg_stat_statements LIMIT 1;

Java 开发者注意:如果使用连接池(如 HikariCP),确保所有连接共享同一数据库实例的 pg_stat_statements 数据。它是实例级视图,不绑定单个连接。

2. Top N 慢查询分析 ​

核心字段说明 ​

字段含义分析维度
calls执行次数高频调用
total_exec_time总执行时间(毫秒)总耗时排名
mean_exec_time平均执行时间单次慢查询
rows返回/影响的总行数大量数据扫描
shared_blks_hit共享缓冲区命中缓存效率
shared_blks_read磁盘读取的缓冲区数IO 热点
wal_recordsWAL 记录数写入压力

实用分析 SQL ​

sql
-- 1. Top 10 总耗时最高的 SQL(优化收益最大)
SELECT queryid,
       LEFT(query, 120) AS query_preview,
       calls,
       ROUND(total_exec_time::numeric, 2) AS total_time_ms,
       ROUND(mean_exec_time::numeric, 2) AS mean_time_ms,
       ROUND(stddev_exec_time::numeric, 2) AS stddev_ms,
       rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

-- 2. Top 10 单次最慢的 SQL(排除低频调用)
SELECT queryid,
       LEFT(query, 120) AS query_preview,
       calls,
       ROUND(mean_exec_time::numeric, 2) AS mean_time_ms,
       ROUND(max_exec_time::numeric, 2) AS max_time_ms,
       rows
FROM pg_stat_statements
WHERE calls > 10
ORDER BY mean_exec_time DESC
LIMIT 10;

-- 3. Top 10 调用频率最高的 SQL(QPS 热点)
SELECT queryid,
       LEFT(query, 120) AS query_preview,
       calls,
       ROUND(calls::numeric / EXTRACT(EPOCH FROM now() - stats_reset) * 1, 2) AS calls_per_second,
       ROUND(mean_exec_time::numeric, 2) AS mean_time_ms
FROM pg_stat_statements, pg_stat_statements_info()
ORDER BY calls DESC
LIMIT 10;

3. 逻辑读/写分析 ​

sql
-- 4. Top 10 逻辑读最高(IO 消耗最大)的 SQL
SELECT queryid,
       LEFT(query, 120) AS query_preview,
       calls,
       shared_blks_hit + shared_blks_read AS total_blks_accessed,
       shared_blks_hit,
       shared_blks_read,
       ROUND(shared_blks_hit::numeric / NULLIF(shared_blks_hit + shared_blks_read, 0) * 100, 2) AS hit_ratio_pct,
       ROUND(blk_read_time::numeric, 2) AS blk_read_time_ms,
       ROUND(blk_write_time::numeric, 2) AS blk_write_time_ms
FROM pg_stat_statements
WHERE shared_blks_hit + shared_blks_read > 0
ORDER BY shared_blks_hit + shared_blks_read DESC
LIMIT 10;

-- 5. Top 10 写入最重的 SQL(DML 语句)
SELECT queryid,
       LEFT(query, 120) AS query_preview,
       calls,
       rows,
       shared_blks_written,
       shared_blks_dirtied,
       wal_records,
       wal_bytes
FROM pg_stat_statements
WHERE shared_blks_written > 0
ORDER BY shared_blks_written DESC
LIMIT 10;

-- 6. 缓存命中率最低的 SQL(可能缺少索引或 shared_buffers 不足)
SELECT queryid,
       LEFT(query, 120) AS query_preview,
       calls,
       shared_blks_hit,
       shared_blks_read,
       ROUND(shared_blks_hit::numeric / NULLIF(shared_blks_hit + shared_blks_read, 0) * 100, 2) AS hit_ratio_pct
FROM pg_stat_statements
WHERE shared_blks_hit + shared_blks_read > 100
ORDER BY hit_ratio_pct ASC
LIMIT 10;

4. 执行计划变化检测 ​

同一 queryid 可能因统计信息变化而切换执行计划。通过监控 mean_exec_time 的标准差可以发现计划抖动。

sql
-- 7. 执行时间抖动最大的 SQL(可能发生了计划变化)
SELECT queryid,
       LEFT(query, 120) AS query_preview,
       calls,
       ROUND(mean_exec_time::numeric, 2) AS mean_ms,
       ROUND(stddev_exec_time::numeric, 2) AS stddev_ms,
       ROUND(stddev_exec_time::numeric / NULLIF(mean_exec_time, 0) * 100, 2) AS cv_pct,
       ROUND(min_exec_time::numeric, 2) AS min_ms,
       ROUND(max_exec_time::numeric, 2) AS max_ms
FROM pg_stat_statements
WHERE calls > 50
  AND stddev_exec_time > 0
ORDER BY cv_pct DESC
LIMIT 10;

cv_pct(变异系数) > 100% 说明执行时间极不稳定,值得用 EXPLAIN ANALYZE 检查当前执行计划。

sql
-- 8. 检查特定 SQL 的当前执行计划
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) 
SELECT * FROM orders WHERE user_id = 12345 ORDER BY created_at DESC LIMIT 10;

5. 定期清理和重置 ​

sql
-- 重置所有统计(在已知的时间点重置,便于计算增量)
SELECT pg_stat_statements_reset();

-- 查看统计信息重置时间(PG 13+)
SELECT stats_reset FROM pg_stat_statements_info();

-- 建议定期重置(如每天凌晨),避免累计数据掩盖近期问题
-- 可通过 cron 或 pg_cron 实现:
-- SELECT cron.schedule('reset-pgss', '0 2 * * *', 'SELECT pg_stat_statements_reset()');

实践建议:在每次重大发布前重置统计,发布后对比优化效果。保留至少 7 天的数据用于趋势分析。

6. 综合分析 SQL ​

sql
-- 9. SQL 资源消耗综合排名(加权评分)
SELECT queryid,
       LEFT(query, 100) AS query_preview,
       calls,
       ROUND(total_exec_time::numeric, 2) AS total_time_ms,
       ROUND(mean_exec_time::numeric, 2) AS mean_time_ms,
       rows,
       shared_blks_hit + shared_blks_read AS total_blks,
       -- 综合评分:总时间权重 40% + 总IO权重 40% + 调用频率权重 20%
       ROUND(
         (total_exec_time / NULLIF(MAX(total_exec_time) OVER(), 0)) * 40 +
         ((shared_blks_hit + shared_blks_read)::numeric / NULLIF(MAX(shared_blks_hit + shared_blks_read) OVER(), 0)) * 40 +
         (calls::numeric / NULLIF(MAX(calls) OVER(), 0)) * 20
       , 2) AS composite_score
FROM pg_stat_statements
WHERE calls > 0
ORDER BY composite_score DESC
LIMIT 20;

-- 10. 每日增量分析(需要定期采集数据到历史表)
-- 建表
CREATE TABLE IF NOT EXISTS pg_stat_statements_history (
    snapshot_time timestamptz DEFAULT now(),
    queryid bigint,
    query text,
    calls bigint,
    total_exec_time numeric,
    mean_exec_time numeric,
    rows bigint,
    shared_blks_hit bigint,
    shared_blks_read bigint
);

-- 采集快照
INSERT INTO pg_stat_statements_history
    (queryid, query, calls, total_exec_time, mean_exec_time, rows, shared_blks_hit, shared_blks_read)
SELECT queryid, query, calls, total_exec_time, mean_exec_time, rows, shared_blks_hit, shared_blks_read
FROM pg_stat_statements;

-- 查询昨日 vs 今日的 Top SQL 变化
SELECT queryid,
       LEFT(query, 80) AS query_preview,
       SUM(CASE WHEN snapshot_time >= date_trunc('day', now()) THEN calls ELSE 0 END) -
       SUM(CASE WHEN snapshot_time < date_trunc('day', now()) THEN calls ELSE 0 END) AS calls_delta,
       SUM(CASE WHEN snapshot_time >= date_trunc('day', now()) THEN total_exec_time ELSE 0 END) -
       SUM(CASE WHEN snapshot_time < date_trunc('day', now()) THEN total_exec_time ELSE 0 END) AS time_delta_ms
FROM pg_stat_statements_history
GROUP BY queryid, query
ORDER BY time_delta_ms DESC
LIMIT 10;

7. 扩展生态配合 ​

pg_stat_kcache:OS 级资源监控 ​

pg_stat_kcache 通过 getrusage() 获取每个 SQL 的实际 CPU 时间和系统调用数据。

sql
-- 安装
CREATE EXTENSION IF NOT EXISTS pg_stat_kcache;

-- 查看 SQL 级 CPU 和 IO 消耗
SELECT s.queryid,
       LEFT(s.query, 100) AS query_preview,
       s.calls,
       ROUND(k.read_bytes::numeric / 1024 / 1024, 2) AS read_mb,
       ROUND(k.write_bytes::numeric / 1024 / 1024, 2) AS write_mb,
       ROUND(k.user_time::numeric * 1000, 2) AS user_cpu_ms,
       ROUND(k.system_time::numeric * 1000, 2) AS system_cpu_ms
FROM pg_stat_statements s
JOIN pg_stat_kcache() k USING (queryid)
ORDER BY k.read_bytes + k.write_bytes DESC
LIMIT 10;

pg_wait_sampling:等待事件采样 ​

pg_wait_sampling 对等待事件进行持续采样,比 pg_stat_activity 的瞬时快照更准确。

sql
-- 安装
CREATE EXTENSION IF NOT EXISTS pg_wait_sampling;

-- 查看等待事件分布
SELECT event_type,
       event,
       count
FROM pg_wait_sampling_profile
WHERE count > 0
ORDER BY count DESC
LIMIT 20;

-- 与 pg_stat_statements 关联,找出哪些 SQL 在等什么
SELECT s.queryid,
       LEFT(s.query, 80) AS query_preview,
       w.event_type,
       w.event,
       w.count AS wait_count
FROM pg_wait_sampling_profile w
JOIN pg_stat_statements s ON s.queryid = w.queryid
ORDER BY w.count DESC
LIMIT 20;

小结 ​

pg_stat_statements 是 SQL 级性能分析的核心工具。配合 pg_stat_kcache 和 pg_wait_sampling,可以构建从"哪条 SQL 慢 → 慢在 CPU 还是 IO → 等在什么事件"的完整诊断链路。建议定期采集并持久化统计数据,建立 SQL 性能基线。

Released under the MIT License.