PostgreSQL 的表膨胀(Bloat)是每个 DBA 迟早要面对的问题。本文从原理到实战,完整梳理表膨胀的成因、诊断、应急与长期治理方案。

一、Vacuum 原理:为什么 PostgreSQL 需要 Vacuum

PostgreSQL 采用多版本并发控制(MVCC)机制。执行 UPDATE 或 DELETE 时,旧版本数据(死元组)不会立即被物理删除,而是保留在数据页中,以确保事务并发读的一致性。UPDATE 本质上可以理解为先 DELETE 再 INSERT。

这些死元组在被清理前会一直占用空间。vacuum 通过删除死元组并将空间标记为可重用,从而回收数据库文件中的空间。但 vacuum 不会重新组织活元组的存储位置,而是维护空闲空间映射(FSM)供后续 INSERT 复用。这意味着常规 vacuum 能阻止空间继续增长,但通常不会减小表的物理文件大小

二、Vacuum 阻塞根因:为什么死元组清理不掉

Vacuum 正常运行时,死元组数量会下降。如果 n_dead_tup 持续攀升,说明 vacuum 被阻塞了。阻塞 Vacuum 的核心原因是 xmin horizon 被卡住——一个更早的事务 ID 仍然活跃,导致 Vacuum 无法清理该事务之后产生的任何死元组。

根因一:长事务。无论活跃还是空闲状态,未提交的长事务都会持有旧快照(backend_xmin),阻止死元组清理-。Vacuum 扫描表、消耗 I/O 并报告成功,但死元组数就是不降-。

诊断 SQL

SELECT pid, datname, usename, state, backend_xmin, backend_xid,
       clock_timestamp() - xact_start AS xact_age
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL OR backend_xid IS NOT NULL
ORDER BY COALESCE(age(backend_xmin), age(backend_xid)) DESC;

找到后可通过 pg_cancel_backend 或 pg_terminate_backend 终止。

根因二:废弃的复制槽。物理或逻辑复制槽未及时消费时,会卡住主库的 xmin。检查方式:

SELECT slot_name, active, age(xmin) FROM pg_replication_slots WHERE xmin IS NOT NULL;

不再使用的复制槽用 pg_drop_replication_slot 删除。

根因三:Prepared 事务残留。两阶段提交的事务若长期处于 prepared 状态,同样会阻塞 Vacuum-25。检查 pg_prepared_xacts 表并清理。

根因四:Hot Standby 反馈。备库开启 hot_standby_feedback 时,备库的长查询会将 xmin 传回主库,导致主库死元组无法清理。

三、应急处理:表已经膨胀了怎么办

方案一:VACUUM FULL(停机方案)

VACUUM FULL 重写整张表,将有效数据写入全新紧凑数据页,物理回收空间并返还操作系统。但它申请 ACCESS EXCLUSIVE 锁,执行期间完全阻塞表的读写。执行过程需约 2 倍表大小的临时磁盘空间

使用原则:仅作为最后手段,在业务停机维护窗口执行。官方文档明确建议:管理员应尽量使用标准 VACUUM,避免 VACUUM FULL。

方案二:pg_repack(生产推荐)

pg_repack 通过创建新表存储有效数据,利用日志同步增量变更,最后原子化交换元数据。全程不阻塞读写,仅在最后交换瞬间申请极短暂锁(毫秒级)。执行过程需约 1 倍表大小的额外临时空间。可同时在线重组索引,消除索引膨胀。对于 GIN 等特殊索引,还需配合 REINDEX 处理。

云数据库通常内置该插件,自建库需安装 pg_repack 扩展。

四、长期预防方案

1. 调优 autovacuum 参数

autovacuum 触发阈值公式:

触发阈值 = autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × n_live_tup

对于频繁更新的表,默认 scale_factor=0.2 可能过高——20% 的死元组占比意味着大量空间已被浪费。针对特定表单独调优

ALTER TABLE your_table SET (autovacuum_vacuum_scale_factor = 0.05);
ALTER TABLE your_table SET (autovacuum_vacuum_threshold = 1000);

可调整的参数还包括:autovacuum_max_workers(增加并行清理能力)、autovacuum_work_mem(单次处理更多死元组)、autovacuum_naptime(检查间隔)等。

2. 配置事务超时

防止长事务阻塞 Vacuum:

ALTER SYSTEM SET idle_in_transaction_session_timeout = '5min';
SELECT pg_reload_conf();

3. 监控表膨胀

使用 pgstattuple 扩展获取精确的膨胀统计:

CREATE EXTENSION IF NOT EXISTS pgstattuple;
SELECT * FROM pgstattuple('your_table_name');

日常监控可使用系统统计视图:

SELECT relname, n_live_tup, n_dead_tup,
       round(n_dead_tup::numeric / nullif(n_live_tup + n_dead_tup, 0), 3) AS dead_ratio
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC LIMIT 20;

4. 表设计与运维优化

  • 分区表:将大表按时间或业务键分区,可单独对老旧分区进行 repack,避免全表操作-
  • 合并更新:将多次更新合并为单次批量更新,减少死元组产生速度-
  • 定期巡检:检查 last_autovacuum 时间戳,确保维护任务正常运行-

表膨胀的治理不是一次性工作,而是需要纳入日常运维的长期工程。把监控做在前面,把参数调优做在平时,把应急方案备在手中——这才是治理表膨胀的正确姿势。