PostgreSQL 使用行版本化(Row Versioning)来处理并发事务,并在物理存储中使用元组(Tuple)来表示版本化后的行数据。
元组的开头包含多个字段,用于存储一些元数据:
xmin:表示创建该行版本的事务 ID。xmax:表示删除该行版本的事务 ID。infomask:用于存储行版本的状态信息。ctid:指向同一行的下一个更新版本的指针。null bitmap:用于标记行中各列是否包含空值。
元组头之后紧跟着数据列,每个列都有自己的数据类型和长度。
每个元组至少需要 23 个字节,并且由于空值位图以及用于数据对齐的强制填充,通常情况下会超过此值。在列数较少的表中,各种元数据的大小很容易超过实际存储数据的大小。
PostgreSQL 使用 xmin 和 xmax 字段来标记元组的有效性,对于可串行化隔离级别而言,事务 xid 而言仅当 时元组才可能有效1。
元组的 xmin 和 xmax 字段在创建、删除和更新时会被设置为相应的事务 ID。
xmin字段在创建元组时被设置为当前事务的 ID。xmax字段在删除元组时被设置为当前事务的 ID。UPDATE操作会被视为先删除原先的元组,再插入新的元组。因此xmax字段在更新元组时被设置为当前事务的 ID,并且新元组的xmin字段会被设置为当前事务的 ID。
xmin 和 xmax 字段仅表示了创建和删除元组的事务 ID,但是在多数事务隔离级别下,事务的状态(如提交或回滚)也会影响元组的有效性。
对此,元组还拥有 xmin_committed、xmin_aborted、xmax_committed 和 xmax_aborted 字段,用于表示事务的状态。
这些字段实际存储在 infomask 的特定比特位中。
在引入 xmax_aborted 字段之后,未被删除的元组的 xmin=0, xmax_aborted=true。
因为对于其他事务而言,元组未被任何事务删除和删除事务已中止的效果是一样的,该元组均有效。
因此将 xmax 字段的值设置为 0 可以统一处理逻辑。
以读已提交隔离级别的事务为例,元组视为可见的充分条件是1:
xmin_committed为true,表示创建元组的事务已经提交。xmax_aborted为true,表示元组尚未被删除或删除元组的事务已经回滚。
xmin_commited、xmin_aborted、xmax_commited 和 xmax_aborted 字段的值实际上只是一个标志位,可能会滞后于实际的事务状态。
因为在事务提交时元组所属的页面可能已经不在缓存中,更新这些字段的代价过于昂贵。
在事务的执行过程中记录所有需要更新的元组也需要大量的内存和 I/O 操作。
实际上,在事务提交时 PostgreSQL 会将事务的状态写入 CLOG(提交日志)中,需要通过事务 ID 来查询事务的实时状态。
当事务插入一个元组时,PostgreSQL 会将 xmin 字段设置为当前事务的 ID,并将 xmin_committed 和 xmin_aborted 字段的值设置为 false。
当其他事务 xid 需要访问元组时,PostgreSQL 会先检查 xmin_committed 和 xmin_aborted 字段的值。
如果这两个字段的值都没有被设置,那么 PostgreSQL 会通过 CLOG 来查询 xmin 事务的状态,确认是因为标志位还未被更新还是因为事务确实还未完成(提交或中止)。
如果 xmin 事务确实还未完成,那么 PostgreSQL 会忽略该元组,并保持 xmin_committed 和 xmin_aborted 字段的值为 false。
下次访问该元组时,PostgreSQL 会再次检查 CLOG。
而如果 xmin 事务已经完成,那么 PostgreSQL 会更新 xmin_committed 和 xmin_aborted 字段的值。
由于 xid 事务已经找到这个元组,因此元组所属的页面已经在缓存中,更新这两个字段的代价非常小。
而下次访问该元组时,PostgreSQL 会直接使用这两个字段的值,而不需要再次查询 CLOG。
xmax_committed 和 xmax_aborted 字段的值也是如此。
由于未被删除的元组 xmax_aborted 字段的值为 true,因此大部分情况下 PostgreSQL 都不需要查询 CLOG。
当事务删除一个元组时,PostgreSQL 会将 xmax 字段设置为当前事务的 ID,并将 xmax_aborted 字段的值从 true 修改为 false。
此后其他事务访问该元组时,均必须检查 CLOG 确认删除事务的状态才能确认该元组是否有效。
直到删除事务提交或回滚,xmax_committed 和 xmax_aborted 字段的值才会被更新,不再需要查询 CLOG。