PostgreSQL 执行 UPDATE 时不会原地改写行。它在堆里写一个新版本,把旧版本的 xmax 标成当前事务号,新版本填上 xmin;DELETE 也只是把旧版本标记为已删除。同一行在磁盘上可以同时存在多个版本。
读操作拿到的是一个快照,包含当时的 xmin、xmax 和活跃事务列表。可见性判断按行头的事务号逐个比对:只有当插入事务在快照前已提交、删除事务在快照后或未提交时,这行才可见。整个过程不加行锁,读不会等写,写也不会等读,这是 MVCC 能减少读写冲突的直接原因。写与写之间仍会冲突:两个事务修改同一行,后一个要等前一个提交,或者在可串行化隔离级别下报错。
代价则在存储侧。旧版本不会随提交消失,它留在堆页里,索引条目也仍指向它。回收交给 VACUUM:它找出对所有活跃快照都不可见的元组,标记空间可复用;索引中的死条目再由 index vacuum 或 cleanup 处理。这里有一条硬性约束,即 xmin horizon:最老的、仍持有快照的事务决定了哪些死元组不能动。一个开了几小时的长事务、一个迟迟不推进的复制槽,都能让整张表的回收停在那里。
于是表的物理大小不等于活数据量,扫描成本也不等于活行数。顺序扫描要跳过大量死元组,走索引的查询可能回表命中已被删除的版本,再丢弃。这类开销不会报错,只会让延迟慢慢抬高,容易在容量规划里被漏掉。
比起检查 autovacuum 有没有在跑,更直接的信号是最老快照的年龄:
SELECT pid, state, xact_start, now() - xact_start AS age
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY age DESC
LIMIT 5;
这个查询列出仍然持有 backend_xmin 的会话及其持续时长。若最老的一条长期不变,表膨胀只是时间问题,调 autovacuum 参数也解决不了。
可以把 MVCC 理解成把并发冲突转化成空间与清理延迟的欠账,账单到期时间由最老快照决定。需要确认的是长事务上限和复制槽的保留策略;等到表明显膨胀再去处理,往往已经多付了一轮 I/O。
评论
还没有公开评论,欢迎留下第一条讨论。