适用于 PostgreSQL 13+、psql 13+。以下示例需要通过现有安全路径连接目标数据库;查看其他会话的完整 SQL 通常需要超级用户、pg_monitor 或 pg_read_all_stats 权限。示例以只读取证为主,先记录故障窗口,不要一开始就终止会话、重建索引、清缓存或修改全局参数。
1. 先确认慢在数据库内还是连接路径上
从与应用相同的网络和认证路径记录连接、服务端时间与版本:
date -u
psql "$DATABASE_URL" -X -v ON_ERROR_STOP=1 -c 'select clock_timestamp(), version();'
连接建立本身就慢时,应先拆分 DNS、TCP、TLS、认证和连接池等待;已经进入数据库且单条语句耗时升高,才继续看活动会话。命令行参数和进程列表可能泄露连接信息,应使用现有凭据文件或受控环境变量,并在共享记录前脱敏。
2. 用等待事件区分“正在执行”和“正在等”
SELECT clock_timestamp() AS observed_at,
pid, usename, application_name, client_addr,
state, wait_event_type, wait_event,
xact_start, query_start, state_change,
clock_timestamp() - query_start AS query_age,
clock_timestamp() - xact_start AS xact_age,
left(query, 300) AS query_sample
FROM pg_stat_activity
WHERE datname = current_database()
AND pid <> pg_backend_pid()
ORDER BY query_start NULLS LAST;
active 不等于正在消耗 CPU:非空的 wait_event_type 表示后端当前在等待。Lock 指向锁竞争,IO 指向存储读取或写入,Client 可能是服务端在等待客户端收发数据;应与应用超时和主机指标对齐。idle in transaction 不是慢 SQL,但长事务会持有锁、阻碍 VACUUM,并使问题持续。不要在公共记录中粘贴未脱敏的 query。
3. 锁等待先找阻塞链,不要只处理最慢的会话
SELECT a.pid AS waiting_pid,
pg_blocking_pids(a.pid) AS blocking_pids,
a.wait_event, a.xact_start, a.query_start,
left(a.query, 300) AS waiting_query
FROM pg_stat_activity AS a
WHERE cardinality(pg_blocking_pids(a.pid)) > 0
ORDER BY a.query_start;
再用阻塞 PID 回查 pg_stat_activity 的事务开始时间、状态、应用名与查询。最老的阻塞事务可能已经处于 idle in transaction,而排在队尾的语句只是受害者。终止后端会回滚事务并影响业务,必须先确认所有者、影响范围和既定处置流程;没有授权时只保留 PID、时间线和阻塞关系。
4. 比较实际计划前,先确认统计窗口与查询身份
若已安装并启用 pg_stat_statements,可查看累计成本较高或平均耗时异常的查询:
SELECT d.stats_reset
FROM pg_stat_database AS d
WHERE d.datname = current_database();
SELECT queryid, calls, total_exec_time, mean_exec_time, rows,
shared_blks_hit, shared_blks_read, temp_blks_read, temp_blks_written,
left(query, 300) AS query_sample
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
pg_stat_statements 是可选扩展,未预加载时不要为一次故障直接重启数据库。累计值必须结合 stats_reset、调用次数和业务窗口解释;平均值也会掩盖少量极慢请求。queryid 聚合的是规范化语句,参数分布不同仍可能使用不同计划或产生不同耗时。
先用不执行语句的计划确认访问路径:
EXPLAIN (VERBOSE, COSTS, SETTINGS)
SELECT ...;
只有在受控环境、只读副本或确认负载可承受时,才考虑:
EXPLAIN (ANALYZE, BUFFERS, WAL, SETTINGS)
SELECT ...;
ANALYZE 会真实执行语句,可能读取大量数据、持锁或改变数据;不要对生产写语句直接使用。比较时要保留参数、角色、search_path、相关会话参数和 schema,因为这些都可能影响计划。重点看估算行数与实际行数的偏差、扫描方式、循环次数、排序或哈希是否落盘,以及 buffer 读取,而不是只盯总成本。
5. 将计划问题与存储、膨胀和统计信息分开
SELECT relname, seq_scan, idx_scan,
n_live_tup, n_dead_tup,
last_analyze, last_autoanalyze,
last_vacuum, last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC NULLS LAST
LIMIT 20;
SELECT datname, blks_read, blks_hit, temp_files, temp_bytes,
deadlocks, stats_reset
FROM pg_stat_database
WHERE datname = current_database();
统计信息陈旧或数据分布变化会造成行数估算偏差,但 n_dead_tup 是估算值,缓存命中率也是累计口径,都不能单独证明根因。PostgreSQL 16+ 可进一步查看 pg_stat_io;旧版本应结合等待事件、pg_statio_*、数据库所在文件系统的延迟和吞吐判断。不要在线执行无范围的 VACUUM FULL 或盲目重建索引,它们可能长时间持锁并放大故障。
6. 验证修复要覆盖同一负载条件
修复后至少在可比参数和业务窗口下确认:应用端 p95/p99 与超时率恢复;阻塞链消失且没有新的长事务;目标 queryid 的调用次数、平均耗时和块读取增量符合预期;实际计划的估算行数、执行时间与临时文件使用改善;主机 I/O 与数据库等待事件没有转移成新的瓶颈。
一次执行变快不足以闭环,缓存预热、参数分布和并发度都会影响结果。应记录修复前后的版本、计划、统计重置时间和观察窗口,让结论可以复核。