数据库越跑越慢,往往不是硬件不够用,而是几条写得很差的SQL在悄悄消耗资源。PostgreSQL提供了一个官方扩展pg_stat_statements,它会记录数据库中所有SQL语句的执行统计信息,包括执行次数、累计耗时、返回行数、IO开销等。学会用它排序分析,你就能在几分钟内从海量查询中找到最值得优化的那几条语句,而不是凭感觉去猜。

一、安装并启用pg_stat_statements扩展
pg_stat_statements是一个需要预加载的扩展,安装分两步。第一步修改postgresql.conf配置文件,把pg_stat_statements加入shared_preload_libraries参数,同时建议配置pg_stat_statements.track参数控制统计范围:
# postgresql.conf 中修改或添加以下配置 shared_preload_libraries = 'pg_stat_statements' # track参数可选值:none / top / all # top 表示只统计顶层语句,all 包括嵌套语句,一般用top即可 pg_stat_statements.track = top # 最多跟踪多少条不同的SQL,超出后淘汰最少使用的语句 pg_stat_statements.max = 10000 # 重启数据库生效 systemctl restart postgresql
第二步是在目标数据库中创建扩展。注意重启数据库是必须的,因为shared_preload_libraries只在启动时加载,执行reload命令不会生效:
-- 在需要分析的数据库中执行 CREATE EXTENSION pg_stat_statements; -- 验证是否安装成功 SELECT * FROM pg_available_extensions WHERE name = 'pg_stat_statements';
安装完成后,PostgreSQL会开始默默记录每条SQL的统计数据。需要说明的是,统计视图是从安装时刻开始累积的,刚装好时数据为空,需要运行一段时间才有分析价值。另外,pg_stat_statements会把参数化的SQL归并到一起统计,比如WHERE id = $1这种预处理语句会被算作同一条查询,这正是我们想要的效果。
二、核心字段含义与常用排序查询
视图pg_stat_statements的字段比较多,先搞清楚几个关键指标,否则排序出来也不知道怎么解读。在PG13及以上的版本中,时间字段以毫秒为单位,主要字段如下:
query:被规范化后的SQL文本,超长会被截断calls:执行次数,判断语句是高频小查询还是低频大查询的关键total_exec_time:累计执行时间,衡量这条SQL对系统的总体拖累程度mean_exec_time:平均单次执行时间,等于总时间除以次数rows:累计返回或影响的行数shared_blks_read:从磁盘读取的数据块数,越大说明缓存命中率越差temp_blks_written:临时文件写入块数,大于零通常意味着排序或哈希操作溢出到磁盘
最常用的排序是按总耗时降序,直接反映哪条SQL吃掉了最多的数据库时间:
SELECT query,
calls,
round(total_exec_time::numeric, 2) AS total_ms,
round(mean_exec_time::numeric, 2) AS avg_ms,
rows,
round(100 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0), 2) AS cache_hit_pct,
temp_blks_written
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
但只看总耗时容易误判。一条执行一万次、每次一毫秒的语句,总耗时可能高居榜首,却已经没有多少优化空间;而一条每次执行三十秒的报表SQL可能因为只跑了几次而排在后面。所以实际分析时要多个维度交叉看,下面几个排序各有用途:
-- 按平均耗时排序,找出单次执行最慢的语句
SELECT query, calls,
round(mean_exec_time::numeric, 2) AS avg_ms
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
-- 按标准差排序,找出执行时间波动大的语句,可能存在执行计划抖动
SELECT query, calls,
round(stddev_exec_time::numeric, 2) AS stddev_ms
FROM pg_stat_statements
WHERE calls > 10
ORDER BY stddev_exec_time DESC
LIMIT 10;
-- 按磁盘读取排序,找出缓存命中率差、IO压力大的语句
SELECT query, calls,
shared_blks_read,
temp_blks_written
FROM pg_stat_statements
ORDER BY shared_blks_read DESC
LIMIT 10;
给一个解读建议:先用总耗时排序圈定前十条,再看每条的cache_hit_pct,如果命中率低于百分之九十,说明这条SQL的IO开销偏大,索引可能缺失;如果temp_blks_written很大,说明SQL内部有大排序或大哈希,需要检查ORDER BY、GROUP BY的列有没有索引,或者JOIN条件是否合理。
三、从排序结果到真正的优化落地
找出问题SQL只是第一步,接下来的优化才是价值所在。针对pg_stat_statements暴露的不同特征,处理思路也不同。
第一种情况:mean_exec_time高且shared_blks_read大,典型的是缺索引。把这条SQL单独拿出来,在前面加上EXPLAIN (ANALYZE, BUFFERS)执行一遍,观察执行计划里是否出现了Seq Scan全表扫描。确认后为过滤条件和连接条件的列建立合适的索引,建立前先用hypopg扩展或手工评估写入代价,避免为了查询拖垮写入性能。
第二种情况:temp_blks_written大,说明排序或哈希溢出磁盘。这时候要么给排序列建索引让计划走索引扫描,要么增大work_mem让排序在内存中完成。注意work_mem是按每个排序操作单独分配的,设置过大在并发场景下容易把内存耗尽,一般从几MB逐步调整并观察效果。
第三种情况:calls极高但mean_exec_time不算离谱,属于高频小查询被反复执行。优化方向不是改SQL本身,而是减少调用次数:检查应用代码是否存在循环内查库的问题,能否用批量查询或IN子句一次取回;也可以在应用层加缓存,把重复查询挡在数据库之外。
最后提醒一点,pg_stat_statements的统计是累积的,分析完一个阶段后建议执行pg_stat_statements_reset()清零重新统计,避免旧数据干扰新一轮判断:
-- 重置统计数据,从当前时刻重新开始累计 SELECT pg_stat_statements_reset();
把这个分析流程固化下来,定期跑一遍排序查询,把总耗时前十的SQL做成趋势监控,你就能在性能问题爆发之前提前发现苗头。慢查询优化从来不是玄学,用对工具加上系统化的分析方法,大部分性能问题都有清晰的解决路径。
PostgreSQL慢查询优化pg_stat_statementsSQL性能调优修改时间:2026-09-13 02:14:31