MVCC 与 VACUUM
PostgreSQL 的堆表更新不是原地覆盖旧行,而是创建新的 Tuple Version。事务通过快照和版本元数据判断自己能看到哪个版本,这构成 Multi-Version Concurrency Control(MVCC)。
行版本与快照
行版本包含创建它的事务标识和使它失效的事务信息。查询快照综合事务状态判断可见性:已提交且早于快照的版本通常可见,未提交、已回滚或在快照之后产生的版本不可见。
MVCC 的直接结果是:
- 普通读取和行更新通常不互相阻塞。
- UPDATE 与 DELETE 会留下 Dead Tuple,空间不会立即归还。
- 长事务会长期保留旧快照,阻止旧版本清理。
- 索引条目也可能指向已经失效的 Heap Tuple,需要配合清理。
隔离级别
| 隔离级别 | 快照特点 | 需要注意 |
|---|---|---|
| Read Committed | 每条语句获取新快照 | 同一事务两次查询可能看到不同已提交结果 |
| Repeatable Read | 事务首次需要快照后复用 | 防止不可重复读,但写入仍可能发生序列化失败 |
| Serializable | 在快照隔离基础上检测读写依赖 | 可能主动中止事务,应用必须支持重试 |
PostgreSQL 的 Repeatable Read 会防止标准所定义的幻读,但仍不等于所有并发执行都可串行化。涉及跨行、跨表业务约束时,应选择 Serializable,或使用约束、显式锁与可重试事务设计。
VACUUM 的职责
普通 VACUUM 不会把整张表重写为紧凑文件,主要完成:
- 标记不再被任何事务需要的 Dead Tuple 空间,使其可被同一 Relation 后续复用。
- 清理或标记失效的索引条目。
- 更新 Visibility Map,使 Index-Only Scan 可以跳过部分 Heap 访问。
- 冻结足够老的事务标识,防止 Transaction ID Wraparound。
| 操作 | 空间效果 | 并发影响 |
|---|---|---|
| VACUUM | 回收为 Relation 内部可复用空间,通常不归还操作系统 | 可与普通读写并行,但仍消耗 I/O 和锁资源 |
| VACUUM FULL | 重写 Relation,把多余空间归还文件系统 | 需要 Access Exclusive Lock,代价高 |
| ANALYZE | 采样并更新优化器统计信息 | 不清理 Dead Tuple |
| Autovacuum | 自动调度 VACUUM 与 ANALYZE | 阈值需要按高变更表单独调整 |
HOT Update
当更新没有修改任何索引引用的列,并且同一 Heap Page 有足够空间时,PostgreSQL 可以使用 Heap-Only Tuple(HOT):新版本留在同一页,通过版本链定位,而不为所有索引生成新条目。
HOT 能降低索引写放大,但受页内空闲空间约束。适当降低高更新表的 Fillfactor,可以为页内更新保留空间;代价是表初始占用更大、每页容纳行更少。
膨胀与冻结
表或索引文件大不等于一定发生异常膨胀,应区分活数据、可复用空闲空间、尚不可清理的旧版本和真正低效的物理布局。
常见原因包括:
- Autovacuum 速度追不上更新和删除速度。
- 长事务、Idle in Transaction 会话或旧复制槽维持过老的可见性边界。
- 大批量更新产生大量版本和索引条目。
- 表级 Autovacuum 阈值不适合大表或高频变更表。
Transaction ID 是有限空间,旧行版本需要被 Freeze。防回卷 VACUUM 是数据可用性要求,不是可选的性能维护;积压严重时数据库会限制产生新事务标识的写操作以避免错误可见性。