PostgreSQL表文件不断变大、查询缓存命中正常但扫描越来越慢,常被概括为表膨胀。MVCC会保留旧版本行,普通 VACUUM 负责把不再需要的空间标记为可复用,但空间通常不会立即归还操作系统。
判断膨胀不能只看表大小,也不能一上来执行VACUUM FULL。 应先确认死元组是否持续累积、autovacuum有没有运行、长事务是否阻止清理,再选择影响最小的处理方式。

先找出增长最快的表
从统计视图筛选大表和死元组:
SELECT schemaname,
relname,
n_live_tup,
n_dead_tup,
last_autovacuum,
autovacuum_count,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size
FROM pg_stat_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 30;
n_dead_tup 是估算值,适合排序和观察趋势,不应当作精确字节数。将结果与历史监控、业务写入量和最近发布变更结合,区分合理增长与异常膨胀。
优先处理体积大、死元组比例高且仍在快速增长的表,而不是按单个百分比机械排序。 小表即使比例很高,释放收益也可能有限。
确认autovacuum是否真正运行
检查全局开关和核心阈值:
SHOW autovacuum;
SHOW autovacuum_vacuum_threshold;
SHOW autovacuum_vacuum_scale_factor;
SHOW autovacuum_max_workers;
SHOW autovacuum_naptime;
再查看当前维护进程:
SELECT pid, datname, relid::regclass, phase,
heap_blks_total, heap_blks_scanned, heap_blks_vacuumed
FROM pg_stat_progress_vacuum;
大表使用默认scale factor时,触发所需变更行数可能非常高;更新频繁的少数表可以设置表级参数,而不必先全局提高维护强度。没有last_autovacuum不等于守护进程一定关闭,也可能是阈值未到、统计被重置或任务一直被阻塞。
查找阻止旧版本回收的长事务
只要某个旧快照仍可能看到历史行,VACUUM就不能清理相关版本。查询长事务和空闲事务:
SELECT pid,
usename,
application_name,
client_addr,
state,
now() - xact_start AS xact_age,
wait_event_type,
wait_event,
left(query, 120) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;
重点关注 idle in transaction、长时间备份、逻辑复制和长期运行的报表。不要只按持续时间直接终止进程,要先确认业务影响和回滚成本。
长事务不结束,反复手工VACUUM也可能几乎没有效果。 应从应用连接管理、事务边界和超时策略解决源头问题。
先使用普通VACUUM恢复空间复用
在确认没有异常长事务后,可针对单表执行:
VACUUM (VERBOSE, ANALYZE) public.orders;
普通VACUUM可以与常规读写并行,但仍会消耗I/O和CPU,并可能与部分DDL冲突。执行前记录表大小、死元组和业务延迟,执行中查看 pg_stat_progress_vacuum。
完成后表文件不缩小并不代表失败:被清理的页会留在关系文件中供未来INSERT或UPDATE复用。普通VACUUM的首要目标是恢复可复用空间和维护可见性信息,不是让df立即多出磁盘。
为高频更新表设置更合适的阈值
若一张大表持续产生死元组,可使用表级存储参数降低触发门槛,例如:
ALTER TABLE public.orders SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_vacuum_threshold = 5000,
autovacuum_analyze_scale_factor = 0.01
);
具体数值应根据表行数、写入速率、维护耗时和磁盘能力计算。阈值过低会让维护过于频繁;过高则会让每轮工作量和膨胀继续增加。调整后至少观察数个业务周期,确认autovacuum次数、死元组峰值和查询延迟变化。
优先做单表精细调优,再决定是否修改全局参数。 这能避免低配置VPS上的多个worker同时造成I/O竞争。
VACUUM FULL什么时候才应该使用
VACUUM FULL 会重写整张表并把空闲空间归还操作系统,但需要额外磁盘空间,还会取得排他锁,期间业务无法正常访问该表。它适合已经显著膨胀、短期不会重新填满且具备维护窗口的表。
执行前估算表与索引总大小,确认磁盘能同时容纳重写过程,并准备回滚或恢复方案:
SELECT pg_size_pretty(pg_table_size('public.orders')),
pg_size_pretty(pg_indexes_size('public.orders'));
磁盘快满时不要仓促运行VACUUM FULL,它可能因临时空间不足让故障进一步扩大。 大表可评估在线重组工具,但仍需测试锁行为和复制影响。
同时检查索引膨胀和HOT更新
表清理后索引未必同步缩小。高频UPDATE若修改了索引列,会产生更多索引版本;合理设置fillfactor并尽量利用HOT更新,可以减少后续写放大。先用 pg_stat_user_indexes 观察扫描使用情况,再决定是否重建。
对单个索引可在支持的版本中考虑:
REINDEX INDEX CONCURRENTLY public.orders_created_at_idx;
并发重建降低长时间阻塞,但会增加I/O、WAL和临时空间,仍需安排容量余量。不要把所有慢查询都归因于膨胀,执行计划、统计信息和索引设计应一起验证。
需要先演练维护窗口和磁盘峰值时,可以使用 萤光云 建立同版本测试库,也可通过 LightNode 准备临时节点。测试数据应脱敏,参数与磁盘性能要接近生产环境。
修复后的监控与验收
持续采集 n_dead_tup、last_autovacuum、autovacuum_count、表与索引大小、长事务数量和数据库延迟。对关键表设置趋势告警,而不是等磁盘达到90%才处理。
验收时确认死元组峰值回落、autovacuum能在下一次阈值到达后按时启动、长事务已受控,并且业务高峰期延迟没有明显恶化。一次手工清理只解决存量,阈值、事务和容量监控稳定后才算完成治理。
常见问题
VACUUM后表文件为什么没有变小?
普通VACUUM把页内空间标记为可复用,通常不重写整个文件。后续写入能复用这些空间,若必须归还操作系统空间才考虑重写型操作。
可以关闭autovacuum改成夜间脚本吗?
通常不建议。autovacuum还承担防止事务ID回卷等重要工作,固定夜间脚本也难覆盖突发写入和不同表的节奏。
n_dead_tup很高就一定要VACUUM FULL吗?
不一定。先处理阻塞清理的事务并运行普通VACUUM,结合实际大小、未来复用概率和维护窗口再决定。
温馨提示
表膨胀治理同时涉及锁、I/O、WAL、复制和可用空间。任何重写大表或并发重建索引的计划,都应提前验证磁盘余量、最长锁等待和备份恢复能力。


