PostgreSQL采用多版本并发控制(MVCC)来保证读写操作不会互相阻塞。当一行数据被更新或删除时,旧版本并不会立刻从物理文件中移除,而是被打上删除标记,成为死元组。这些死元组如果长期得不到清理,就会占用大量磁盘空间,同时拖慢全表扫描和索引扫描的速度,这就是常说的表膨胀和索引膨胀。要解决膨胀问题,需要先理解死元组产生的机制,再选择合适的清理手段,并持续优化自动清理参数。

PostgreSQL膨胀是怎么产生的
PostgreSQL的MVCC机制下,每次UPDATE操作并不会修改原来的行,而是插入一个新版本的行,并把旧版本标记为无效。DELETE操作同样不会立即删除数据,而是把对应行标记为已删除。这些被标记但尚未被回收的行就是死元组。只要事务还在使用旧版本的数据,PostgreSQL就不能直接清除它们。只有当VACUUM进程确认没有任何活动事务还需要这些旧版本时,才会把空间标记为可重用。
表膨胀的本质是数据文件中积累了大量死元组,而新写入的数据又不断填充到文件末尾或者可重用空间不足。对于频繁更新的表,如果autovacuum没有及时触发,或者清理速度跟不上写入速度,表文件就会持续增长。索引也会发生类似问题,因为索引条目同样存在多版本,更新频繁的索引容易产生大量指向旧堆元组的无效索引项。值得注意的是,即使死元组被清理,表文件的物理大小通常也不会缩小,空间只会被标记为可复用,真正缩小文件需要更重量级的操作。
除了更新和删除,长事务也是膨胀的重要推手。一个未提交的长事务会持有旧版本快照,导致VACUUM无法清理这些事务开始之后产生的死元组。如果某个连接开启事务后长时间不提交也不回滚,死元组就会在数据库里不断堆积,即使autovacuum参数设置得再合理也无法回收。因此排查膨胀时,要同时关注慢查询、长事务和空闲事务。
如何定位表和索引的膨胀程度
定位膨胀最直接的方式是查看系统视图pg_stat_user_tables。这个视图会统计每个用户表的活跃元组数n_live_tup和死元组数n_dead_tup。当n_dead_tup远大于n_live_tup时,说明该表需要尽快执行VACUUM。下面的SQL可以按死元组占比排序,找出问题最严重的表。
SELECT schemaname, relname,
n_live_tup, n_dead_tup,
round(n_dead_tup::numeric / greatest(n_live_tup, 1), 4) AS dead_ratio
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY dead_ratio DESC
LIMIT 20;
如果需要更精确地评估表和索引的实际膨胀空间,可以安装pgstattuple扩展。该扩展通过扫描物理文件来统计死元组占比、空闲空间和每行平均大小等指标。创建扩展后,对单张表执行pgstattuple函数,会返回dead_tuple_percent字段,表示死元组占所有元组的比例。这个比例超过30%时,通常建议安排清理或重建。对于大表,扫描过程会消耗一定I/O资源,建议在业务低峰期执行。
CREATE EXTENSION IF NOT EXISTS pgstattuple;
SELECT * FROM pgstattuple('public.orders');
索引膨胀的评估可以结合pg_stat_user_indexes中的idx_scan、idx_tup_read和idx_tup_fetch等统计信息,也可以通过pgstattuple针对索引执行pgstatindex函数。当索引的avg_leaf_density明显低于正常水平,或者索引大小远大于表大小时,就需要考虑重建索引。另一个经验判断是:如果一个索引很少被扫描,但体积却很大,说明它可能已经膨胀,可以考虑删除或重建后观察业务影响。
清理膨胀的几种方案及对比
普通VACUUM是最轻量的清理方式。它只标记死元组占用的空间为可复用,不会缩小表文件,也不会阻塞读写操作。执行VACUUM时可以加上ANALYZE同时更新统计信息,帮助优化器生成更好的执行计划。常规命令如下:
VACUUM (VERBOSE, ANALYZE) public.orders;
VACUUM FULL则会把表重写成一个紧凑的新文件,能够真正释放磁盘空间。但它在执行期间会获取表上的ACCESS EXCLUSIVE锁,阻塞所有读写操作,因此不适合在业务高峰期使用。对于小型表或者可以容忍短暂停写的场景,VACUUM FULL是最直接的回收空间方式。除了锁粒度大以外,VACUUM FULL还会消耗额外磁盘空间来存放重写过程中的临时数据,所以执行前需要确认磁盘余量。
VACUUM FULL public.orders;
如果表很大且不能长时间停写,推荐使用pg_repack或pg_squeeze这类在线重组工具。pg_repack通过创建新表、复制数据、切换表名的方式,在尽量不阻塞业务的前提下完成表空间回收和索引重建。它只会在切换阶段短暂获取锁,执行过程中允许正常读写。pg_repack的典型用法是在操作系统命令行中指定数据库和表名:
pg_repack -d mydb -t public.orders
使用pg_repack前需要确认目标表有主键或唯一索引,因为工具需要通过这些约束来保证数据复制期间的增量同步。它还需要在数据库中安装相应扩展。对于没有主键又无法添加主键的表,可以考虑用逻辑复制或分区表迁移等方式重建。无论选择哪种在线重组方案,都建议先在测试环境验证执行时间和资源消耗,并准备回滚方案。
索引膨胀的清理相对简单,可以使用REINDEX INDEX CONCURRENTLY在线重建指定索引,这种方式不会阻塞该索引上的并发写入。如果需要重建表上的所有索引,可以执行REINDEX TABLE CONCURRENTLY。重建索引能够显著减少索引文件的大小,并提升依赖索引的查询性能。需要留意的是,CONCURRENTLY方式会消耗更多系统资源,且不能在一个事务块中执行。
REINDEX INDEX CONCURRENTLY idx_orders_order_no;
通过autovacuum参数预防膨胀反弹
只做一次清理并不能彻底解决问题,关键要调整autovacuum的触发频率和清理速度。默认情况下,当表中的死元组数量超过autovacuum_vacuum_threshold加上autovacuum_vacuum_scale_factor乘以活跃元组数的结果时,autovacuum就会启动。对于大表,默认的scale_factor为0.2,意味着表数据增长到很大时,可能要积累20%的死元组才会触发清理,这时膨胀已经比较严重。很多生产环境会把该参数调小到0.05甚至0.01,让清理更频繁地发生。
ALTER SYSTEM SET autovacuum_vacuum_scale_factor = 0.05; ALTER SYSTEM SET autovacuum_vacuum_threshold = 50; SELECT pg_reload_conf();
autovacuum_vacuum_cost_limit控制清理过程能消耗多少I/O资源。默认值为200,在写入频繁的实例上往往偏低,导致清理速度跟不上死元组产生速度。可以适当提高到1000或2000,并调低autovacuum_vacuum_cost_delay。如果单表更新特别频繁,还可以通过ALTER TABLE设置该表独立的autovacuum参数,例如对订单表设置更激进的清理策略,而不影响其他表。
ALTER TABLE public.orders SET ( autovacuum_vacuum_scale_factor = 0.02, autovacuum_vacuum_threshold = 100, autovacuum_vacuum_cost_delay = 10 );
监控方面,可以定时记录pg_stat_user_tables中的n_dead_tup和last_autovacuum字段,如果发现某些表频繁出现在死元组排行榜前列,就说明需要单独调优。同时要关注pg_stat_activity中的长事务,idle in transaction状态的连接超过一定时间应当及时告警或断开。一个常见的维护脚本是每小时统计死元组占比,并输出到监控平台,这样可以在膨胀影响性能之前收到预警。
从整体思路上看,膨胀治理分为发现、清理和预防三个阶段。先通过统计视图和扩展定位膨胀对象,再根据停机窗口和表大小选择VACUUM、VACUUM FULL、pg_repack或索引重建,最后调整autovacuum参数并建立监控。只有把清理命令和参数调优结合起来,才能避免数据库陷入膨胀、清理、再膨胀的循环。
PostgreSQL膨胀死元组VACUUM修改时间:2026-08-28 10:49:59