3.1 元组上的版本信息
在PostgreSQL数据库中,每条元组都会在头部记录版本信息,它包含生成元组的事务ID、删除元组的事务ID、元组的CommandID及一些Hints信息。PostgreSQL的可见性判断函数就根据这些信息,以及clog、快照来综合判断元组的可见性。
PostgreSQL数据库采用页面存储的方式,每个表都会有多个页面,页面的组织结构如图3-1所示,它从前向后保存元组的偏移量,从后向前摆放元组,中间的部分是这个页面的空闲空间(Free Space),当空闲空间不足以保存一个元组时,这个页面会变满。
图3-1 页面的组织结构
每个元组都在自己的头部保存版本信息,为了提高存储空间的利用率,这些头部信息的排列尽量考虑了字节对齐的问题,每个变量也尽量考虑了复用,例如t_cid变量在不同的生命周期中,分别用来代表cmin、cmax、xvac。
• cmin代表的是生成元组时的CommandID。
• cmax代表的是删除元组时的CommandID。
• xvac是元组被Vacuum时设置的,这时元组已经脱离了原来的事务。
如果一个元组在同一个事务中被生成和删除,那么t_cid就无法表达这个元组的生命周期。也就是说,无法同时保存生成元组的CommandID和删除元组的CommandID。在这种情况下,t_cid中记录的是ComboID。ComboID与一组{cmin,cmax}相对应,这个映射关系被保存到hash表中,因此如果t_cid中保存的是ComboID(元组头部的Hints中包含HEAP_COMBOCID标记),就需要去hash表中获得cmin或cmax。
为了更好地观察元组上版本信息的含义,我们来看一些示例,读者需要安装一个PostgreSQL的插件——pageinspect,该插件可以展示数据页面的内容。
新创建一个t1表,并插入一个元组,可以通过pageinspect插件读取t1表目前的数据情况。
从示例可以看出,在元组插入后,事务产生了真正的事务ID 7000,也就是产生这个元组的事务ID是7000,这个事务ID也被记录在元组的xmin中,表示该元组的生成“时间”。
这时元组还没有被删除或加锁,因此它的xmax是0,表示当前元组还是活跃状态。
从infomask也可以看出来,当前的infomask中的标记位是HEAP_XMAX_INVALID,表示还没有设置xmax值。
当前还没有infomask2的标记,infomask2中的数字2代表当前元组共有两列。
从data中可以看出,数据中存储的是两个1。
这时候将事务做Commit操作,发现元组的状态依然没有改变。
对元组做一次查询操作,发现元组的infomask的标记位中增加了HEAP_XMIN_COMMITTED标记位。这个标记位提供了元组可见性判断的快速方法。每次对元组进行查询时,如果发现元组所在的事务已经提交,就设置这个快速判断的标记位,避免每次可见性判断都去clog中查询事务状态。
这时启动一个新的事务,对元组做更新操作,可以发现当前事务的事务ID发生了变化,而且在数据页面中也增加了一个新的元组。从这两个元组的data列中可以看出,它们分别是更新前的旧元组和更新后的新元组两个版本。
在旧元组中,infomask中的标记位只保留了HEAP_XMIN_COMMITTED,去掉了之前的HEAP_XMAX_INVALID,因为xmax中已经记录了删除这个元组的事务ID。在MVCC中,对元组的更新操作可以理解为对旧元组的删除和对新元组的插入。
旧元组中的infomask2则增加了HEAP_HOT_UPDATED标记,表示元组已经被更新,而且更新产生的新元组和旧元组处于同一个页面中。旧元组的HEAP_HOT_UPDATED通常和新元组的HEAP_ONLY_TUPLE成对出现。
新元组中的xmin记录的是产生新元组的事务ID,新元组刚刚产生,所以没有xmax。
新元组中的infomask的标记为HEAP_XMAX_INVALID和HEAP_UPDATED,表示这是一个由更新操作产生的新版本元组。
新元组中的infomask2中的标记位记录了HEAP_ONLY_TUPLE标记位,表示这个元组目前还只是一个HOT元组(这种情况更适用于有索引的情况,可以防止产生重复的索引项,更容易清理)。
从infomask2中可以看出当前的元组有两列。
再次做更新操作,页面内目前有3个元组。
第1个元组已经被删除,它的t_ctid列指向了第2个元组。
第2个元组已经被删除,它的t_ctid列指向了第3个元组。
第3个元组是当前事务元组,处于活跃状态,因此它的t_ctid列指向自己。
Vacuum之后发现页面已经被清空,其中:
第1个元组中的lp_flags为LP_REDIRECT,表示这个元组已经被重定向到了第3个元组。
第2个元组中的lp_flags为LP_UNUSED,表示这个槽位目前没有被使用。
第3个元组目前为正常状态。
插入新的元组,可以看出新元组占用了第2个槽位。