qnt0

定位 PostgreSQL 慢查询的一个固定流程

2026-07-02

每次有人问"数据库慢",我都会按同一个顺序走一遍。顺序很重要,跳过哪一步都容易做无用功。

第一步:先看总量,别看单条

pg_stat_statements 需要提前开,改 postgresql.conf:

shared_preload_libraries = 'pg_stat_statements'

重启后建扩展:

CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

然后按总耗时排序:

SELECT calls,
       round(total_exec_time::numeric, 1) AS total_ms,
       round(mean_exec_time::numeric, 2)  AS mean_ms,
       rows,
       left(query, 80) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

注意排序用的是 total_exec_time,不是 mean_exec_time。一条 2ms 的查询跑一百万次,比一条 800ms 的查询跑十次更值得优化——但大多数人第一反应是去找那条 800ms 的。

第二步:看单条的执行计划

EXPLAIN (ANALYZE, BUFFERS)
SELECT ...;

BUFFERS 一定要加。没有它你只能看到"慢",不知道为什么慢。shared read 高说明在真读盘,shared hit 高说明是内存命中。

ANALYZE 会真正执行,写操作记得包在事务里回滚。

第三步:对比预估行数和实际行数

这是最容易被忽略的一步。有张表 800 万行,查询条件是两列的组合,走了 seq scan。看执行计划发现 rows=1200 预估,实际返回 24 万行——差了两百倍。

这不是索引的问题,是统计信息太旧了:

ANALYZE orders;

跑完之后计划立刻变了,从 seq scan 换成了 bitmap index scan,耗时从 1.8s 降到 40ms。

顺序总结

  1. pg_stat_statements 按总耗时排序,找到真正值得优化的查询
  2. EXPLAIN (ANALYZE, BUFFERS) 看计划和实际读了多少块
  3. 对比预估行数和实际行数,不一致先 ANALYZE
  4. 以上都正常,才考虑加索引或者改 SQL

很多被叫作"慢查询"的问题,其实是统计信息的问题。上来就加索引,往往只是把问题往后推了几个月。