数据库突然变慢,十有八九是几条慢 SQL 在拖后腿。与其对着监控图瞎猜,不如直接打开 PostgreSQL 自带的 pg_stat_statements 扩展——它能把每条 SQL 的执行次数、总耗时、平均耗时、读写块数都记下来,三句查询就能把“元凶”按耗时排好队。本文用一套可复制的命令,带你从零启用到精准定位慢查询。
一、先确认扩展已加载
pg_stat_statements 不是开箱即用的视图,而是一个需要预加载的共享库。最易踩的坑就是只建了扩展却没在配置文件里登记,结果查询报错。先检查 postgresql.conf:
# postgresql.conf 必须包含下面这行,且只能在启动时生效
shared_preload_libraries = 'pg_stat_statements'
# 改完配置要重启,仅 reload 不生效
pg_ctl restart -D /var/lib/pgsql/15/data
# 进入 psql 后建扩展
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
确认是否加载成功,查一眼视图是否存在即可:SELECT count(*) FROM pg_stat_statements; 能返回 0 行(说明已就绪,只是还没统计)就对了。
二、三句 SQL 把慢查询揪出来
定位慢 SQL 有三个常用排序维度,对应不同排查场景,别只会按总耗时排:
| 排序维度 | 适用场景 | ORDER BY 字段 |
|---|---|---|
| 总耗时 | 吃资源的大户、整体变慢 | total_exec_time DESC |
| 平均耗时 | 单次就很慢的尖刺 SQL | mean_exec_time DESC |
| 读写块数 | 全表扫、缺索引的 I/O 杀手 | (shared_blks_read+shared_blks_written) DESC |
最常用的“按总耗时排”查询如下,注意时间字段单位是毫秒:
SELECT query,
calls,
round(total_exec_time / 1000, 1) AS total_s,
round(mean_exec_time / 1000, 1) AS mean_ms,
rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
如果怀疑是某条“单次就很慢”的 SQL(比如报表导出的偶发卡顿),把 ORDER BY 换成 mean_exec_time DESC,往往能挖出被总耗时掩盖的尖刺。
三、看 I/O 与缓存命中率
慢不一定在 CPU,也可能在磁盘。下面这句按读写块数排序,专门找全表扫描、缺索引的语句;同时算缓存命中率,命中率低于 95% 就要警惕:
SELECT query,
calls,
round((shared_blks_read + shared_blks_written) / 1024.0, 1) AS mb,
100.0 * shared_blks_hit
/ nullif(shared_blks_hit + shared_blks_read, 0) AS hit_pct
FROM pg_stat_statements
ORDER BY shared_blks_read + shared_blks_written DESC
LIMIT 20;
踩坑实录
上周一台 PG15 自建库,我照文档在 postgresql.conf 加了 shared_preload_libraries = 'pg_stat_statements',图省事只执行了 pg_ctl reload,没重启。然后 CREATE EXTENSION 直接报 ERROR: pg_stat_statements must be loaded via shared_preload_libraries。我一开始以为是扩展没装 contrib 包,折腾半小时重装 rpm,还是同样的错。最后才反应过来:这个参数只能启动时加载,reload 不生效。补了一句 pg_ctl restart -D /var/lib/pgsql/15/data,扩展秒建成功,视图立刻能查。教训——改完共享库参数,永远先 restart 再建扩展,别想走捷径。
四、拿到慢 SQL 后下一步做什么
定位到具体 SQL 只是第一步,真正的优化要靠执行计划:
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE user_id = 123 AND created_at > '2026-01-01';
重点看 Seq Scan(全表扫描,通常意味着缺索引)、actual time 与 rows 是否严重不符(统计信息过期,跑 ANALYZE 表名;)。一个常见补丁是在过滤列上加联合索引。
⚠️ 版本坑:PostgreSQL 13 把视图里的 total_time / mean_time 改名成了 total_exec_time / mean_exec_time。如果你抄的是老教程(12 及以前),查询会报 column total_time does not exist。用 SELECT version(); 确认大版本再选字段名。
常见问题
Q:托管云数据库(阿里云 RDS / 腾讯云)也要改 postgresql.conf 吗?
A:不能直接改配置文件。shared_preload_libraries 要在控制台“参数设置”里加 pg_stat_statements 并重启实例,之后 CREATE EXTENSION 即可,扩展本身一般已预装。
Q:pg_stat_statements 视图是空的,怎么办?
A:先确认扩展已建(\dx 能看到),再确认配置里确实预加载了且实例已重启;若之前只 reload,补一次重启即可。
Q:统计会一直累积吗,要不要定期清?
A:会一直累积到 max 上限。排查某段时间的问题时,先 pg_stat_statements_reset() 清空再观察,数据更聚焦。
如果你在定位慢 SQL 时遇到其他报错,欢迎在 fenij.com 留言,一起把排障过程补完整。