为何PostgreSQL索引不存储t_xmin、t_xmax等可见性信息?
为什么PostgreSQL不将
t_xmin、t_xmax存储在二级索引中 目前PostgreSQL的默认物理存储结构符合以下特征:
- 堆表(heap table)为每个元组附带隐藏的版本控制字段:
t_xmin标记插入该元组版本的事务ID,t_xmax标记删除/覆盖该元组版本的事务ID- 常规二级索引仅存储索引键值、对应元组的物理行标识(RID),不携带元组的版本可见性信息
- 这种索引与版本信息解耦的设计,是Index-Only Scan需要借助Visibility Map判断元组可见性的核心原因。
将事务ID字段直接存入索引的设计思路没有被PostgreSQL采用,核心原因有几点:
- 索引体积膨胀的代价远大于收益
每个索引条目额外增加两个事务ID字段,单条目固定要多占用至少16字节的存储开销(PG 10之后事务ID为64位长度)。对千万行级的表来说,单表的单个索引就要多出近160MB的纯冗余存储;如果一张表上建了多个索引,累计的额外存储开销会非常可观。更关键的是,索引整体体积膨胀后,B树索引的树高会上升、缓存命中率会下降,反而会拖慢原本要优化的查询性能,完全违背索引的设计初衷。 - MVCC一致性同步的写放大完全不可接受
现有设计下,堆表的元组版本变化(比如旧版本被VACUUM清理、事务提交/回滚状态变更),索引不需要跟着同步修改条目内容,仅需要在死元组达到可清理条件时,批量删除对应RID的索引引用。
如果把t_xmin、t_xmax存进索引,每当元组的版本状态发生变化(比如被更新、被删除),就需要同步修改所有关联索引上对应的条目信息。原本VACUUM的工作只需要扫描堆表清理死元组、批量删除对应索引引用,现在变成要跨所有索引定位对应条目,逐行更新存储的事务ID字段,这种量级的写放大,会彻底毁掉PostgreSQL的写入性能。 - 就算存了事务ID,也解决不了核心的可见性判断问题
元组可见性判断从来不是只看t_xmin、t_xmax两个值就够的:你还是需要查询pg_xact(事务提交状态日志)里的事务提交/回滚结果,对照当前事务的快照范围,才能判断某个版本是否对当前查询可见。把两个ID字段存进索引,根本省不掉这部分查询开销,更没法处理长快照、版本链遍历这类复杂的MVCC逻辑。 - 现有Visibility Map方案的性价比已经极高
目前用来支撑Index-Only Scan的Visibility Map,是按堆表块为单位存储的,每个堆块仅用2个bit标记块内元组是否全部对所有事务可见,全表的Visibility Map总大小通常只有几MB到几十MB,相比把事务ID塞进所有索引条目的方案,存储开销差了几个数量级,而且状态更新是批量完成的,没有逐条目修改的性能损耗。
内容的提问来源于stack exchange,提问作者Yriuns
相关产品推荐
相关产品推荐

