PostgreSQL依靠MVCC机制实现多版本并发控制,更新和删除操作并不会直接修改旧数据,而是留下大量死元组等待清理。如果autovacuum工作不正常,或者表更新频繁超出了自动清理的能力,死元组会不断堆积,最终导致表膨胀、索引效率下降、查询变慢。要避免这种被动局面,最有效的办法就是对死元组比例建立监控,一旦超过设定阈值就自动触发告警,把问题扼杀在早期阶段。

一、死元组是怎么产生的,如何查询比例
PostgreSQL的MVCC实现方式决定了每一次UPDATE都会产生一个新的元组版本,旧版本被标记为dead tuple;DELETE同样只是标记,不会立刻回收空间。这些死元组的清理工作依赖autovacuum后台进程,但autovacuum的触发条件是基于阈值计算的,默认情况下要到表内死元组数量达到相当可观的规模才会启动,对于更新量极大的表来说,中间的空档期就可能积累大量死元组。
查询死元组的统计信息主要依赖pg_stat_user_tables视图,其中n_dead_tup字段记录了死元组数量,n_live_tup记录活元组的估算值。两者相加可以近似得到表的逻辑总行数,死元组占比就是用n_dead_tup除以这个总和。下面是一条常用的巡检SQL:
-- 查询各表死元组数量与占比,按比例降序排列
SELECT
schemaname,
relname,
n_live_tup,
n_dead_tup,
round(n_dead_tup * 100.0 / nullif(n_live_tup + n_dead_tup, 0), 2) AS dead_ratio
FROM pg_stat_user_tables
WHERE n_dead_tup > 0
ORDER BY dead_ratio DESC
LIMIT 20;需要特别说明的是,n_live_tup和n_dead_tup都是统计信息,不是精确计数,存在一定的滞后性,但对于监控告警场景来说精度完全够用。另外,如果希望掌握更精确的膨胀情况,可以结合pgstattuple扩展做抽样分析,不过它对大表的开销较大,一般只用于事后排查,不适合高频巡检。
二、编写定时巡检脚本实现自动告警
有了巡检SQL,下一步就是把它变成定时任务。思路很简单:用cron或者systemd timer每隔几分钟执行一次脚本,脚本连接数据库查出超过阈值的表,一旦结果非空,就通过邮件、企业微信或者钉钉webhook推送告警。这里以一个Shell脚本为例,假设死元组比例阈值设定为20%:
#!/bin/bash
# pg_dead_tuple_alert.sh 死元组比例巡检脚本
THRESHOLD=20
DB_NAME="mydb"
WEBHOOK_URL="https://oapi.dingtalk.com/robot/send?access_token=xxxx"
# 查询超过阈值的表
RESULT=$(psql -d "$DB_NAME" -At -F'|' -c "
SELECT schemaname || '.' || relname,
n_dead_tup,
round(n_dead_tup * 100.0 / nullif(n_live_tup + n_dead_tup, 0), 2)
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
AND n_dead_tup * 100.0 / nullif(n_live_tup + n_dead_tup, 0) > $THRESHOLD
ORDER BY n_dead_tup DESC;")
if [ -n "$RESULT" ]; then
MESSAGE="死元组比例告警,以下表超过${THRESHOLD}%阈值:\n${RESULT}"
curl -s -H "Content-Type: application/json" \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"${MESSAGE}\"}}" \
"$WEBHOOK_URL"
fi脚本中有两个细节值得注意。一是加了n_dead_tup > 1000的过滤条件,避免小表因为几个死元组就触发误报,例如一张只有几十行的配置表,即便比例很高也没有实际处理价值。二是告警消息里最好带上具体的表名、死元组数量和比例,方便接手的人直接判断严重程度,而不是只收到一条干巴巴的告警。
把脚本放到cron中每五分钟执行一次即可:*/5 * * * * /opt/scripts/pg_dead_tuple_alert.sh >> /var/log/pg_alert.log 2>&1。如果环境更复杂,也可以直接用Prometheus配合postgres_exporter采集stats_user_tables_n_dead_tup指标,在Grafana里配置告警规则,这样图表和告警一体化,更适合规模较大的数据库集群。
三、收到告警之后该怎么处理
告警只是手段,最终还是要解决问题。收到死元组告警后,第一步应该确认autovacuum是否正常运行。查询pg_stat_activity看是否有autovacuum worker正在处理该表,同时检查日志中是否出现autovacuum被跳过的记录。常见的原因包括长事务阻塞了死元组清理——只要有一个未提交的事务持有旧快照,其后产生的死元组都无法回收。这时可以用下面的SQL找出运行时间异常的会话:
-- 查找长事务,它们会阻碍死元组回收
SELECT pid,
state,
now() - xact_start AS xact_duration,
now() - query_start AS query_duration,
query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
AND now() - xact_start > interval '30 minutes'
ORDER BY xact_duration DESC;如果确认没有长事务阻塞,但autovacuum清理速度跟不上产生速度,就需要考虑调参。适当调低autovacuum_vacuum_scale_factor(例如针对大表单独设置为0.02),可以让vacuum更频繁地触发;调高autovacuum_vacuum_cost_limit则能提升清理吞吐。这些参数可以按表粒度通过ALTER TABLE ... SET (autovacuum_vacuum_scale_factor = 0.02)设置,比全局修改更精准。
最后还有一种情况:表已经严重膨胀,单纯VACUUM只能标记空间复用,无法归还给操作系统,此时需要对表执行VACUUM FULL或者使用pg_repack在线重建。前者会锁表,业务低峰期才能做;后者不阻塞读写,是生产环境更推荐的选择。处理完毕后,记得观察几天监控曲线,确认死元组比例回归到正常水位,并总结膨胀的根因,是业务写入模式问题还是配置不合理,从源头上降低再次发生的概率。
PostgreSQL死元组自动告警autovacuum修改时间:2026-09-13 11:56:32