关于元组的基础知识,曾经在 pg事务篇(一)—— 事务与多版本并发控制MVCC
一、 页与元组的组织结构
1. 页的组织结构
pg数据库采用页面存储的方式,每个表都会有多个页面,每页大小是8K。
页从前往后保存头信息(Header Info,绿色部分)、元组偏移量(红色部分);从后往前存放元组信息(灰色的tuple);中间为页面空闲空间(Free Space,白色部分)。当空闲空间不足以保存一个元组时,该页面变满。
2. 元组的组织结构
把下面那一长条放大,这就是元组的结构,在元组头部分保存了大量的版本信息,用于元组可见性判断。为了提高存储空间利用率,这些头部信息的排列尽量考虑到了字节对齐的问题,每个变量也尽量考虑了复用。例如t_cid变量在不同生命周期中分别代表cmin,cmax,xvax。
HeapTupleHeaderData的实现在htup_details.h中,下面我们就来看一看。
二、 源码实现
1. HeapTupleFields
/* We store five "virtual" fields Xmin, Cmin, Xmax, Cmax, and Xvac in three
* physical fields. Xmin and Xmax are always really stored, but Cmin, Cmax
* and Xvac share a field.
*/
typedef struct HeapTupleFields
{
TransactionId t_xmin; /* inserting xact ID,插入该元组的事务id */
TransactionId t_xmax; /* deleting or locking xact ID,删除或锁定该元组的事务id */
union
{
CommandId t_cid; /* inserting or deleting command ID, or both,插入或删除(或者两种兼有)该元组的命令id */
TransactionId t_xvac; /* old-style VACUUM FULL xact ID,对该元组执行vacuum full操作的事务id */
} t_field3;
} HeapTupleFields;
2. DatumTupleFields
记录元组长度、类型、oid等信息
typedef struct DatumTupleFields
{
int32 datum_len_; /* varlena header (do not touch directly!),变长header */
int32 datum_typmod; /* -1, or identifier of a record type,-1或者表示记录类型 */
Oid datum_typeid; /* composite type OID, or RECORDOID,复合类型oid或者记录oid */
} DatumTupleFields;
3. HeapTupleHeaderData
struct HeapTupleHeaderData
{
union
{
HeapTupleFields t_heap; // 主要用于元组可见性检查
DatumTupleFields t_datum;
} t_choice;
ItemPointerData t_ctid;
/* Fields below here must match MinimalTupleData! */
#define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK2 2
uint16 t_infomask2; /* number of attributes + various flags */
#define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK 3
uint16 t_infomask; /* various flag bits, see below */
#define FIELDNO_HEAPTUPLEHEADERDATA_HOFF 4
uint8 t_hoff; /* sizeof header incl. bitmap, padding */
#define FIELDNO_HEAPTUPLEHEADERDATA_BITS 5
bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs,23 bytes */
/* MORE DATA FOLLOWS AT END OF STRUCT */
};
字段说明:参考 http://blog.itpub.net/31493717/viewspace-2220463/
① t_choice:具有两个成员的联合类型:
- t_heap:用于记录对元组执行插入/删除操作的事务ID和命令ID,这些信息主要用于并发控制时检查元组对事务的可见性。
- t_datum:当一个新元组在内存中形成的时候,我们并不关心其事务可见性,因此在t_choice中只需用DatumTupleFields结构来记录元组的长度等信息。但在把该元组插入到表文件时,需要在元组头信息中记录插入该元组的事务和命令ID,故此时会把t_choice所占用的内存转换为HeapTupleFields结构并填充相应数据后再进行元组的插入。
② t_ctid:一个指针,保存指向自身或新元组的元组的标识符(tid)。当更新该元组时,t_ctid会指向新版本元组。若元组被更新多次,则该元组会存在多个版本,各版本通过t_cid串联,形成一个版本链。
③ t_infomask2:其低11位表示当前元组的属性个数,其他位则用于包括用于HOT技术及元组可见性的标志位。
④ t_infomask:用于标识元组当前的状态,比如元组是否具有OID、是否有空属性等,t_infomask的每一位对应不同的状态,共16种状态。pg事务篇(三)—— 事务状态与Hint Bits(t_infomask)_Hehuyi_In
⑤ t_hoff:该元组头的大小。
⑥ t_bits[]数组:标识该元组哪些字段为空。
4. ItemIdData
typedef struct ItemIdData
{
unsigned lp_off:15, /* offset to tuple (from start of page),元组在页面的偏移量 */
lp_flags:2, /* state of line pointer, see below,元组的line pointer状态,参考下面 */
lp_len:15; /* byte length of tuple,元组长度 */
} ItemIdData;
typedef ItemIdData *ItemId;
/*
* lp_flags 有以下状态. UNUSED状态的line pointer可以立刻重用,其他状态的则不可以
*/
#define LP_UNUSED 0 /* unused (should always have lp_len=0),未使用 */
#define LP_NORMAL 1 /* used (should always have lp_len>0),正在正常使用 */
#define LP_REDIRECT 2 /* HOT redirect (should have lp_len=0),这个line pointer重定向到其他line pointer */
#define LP_DEAD 3 /* dead, may or may not have storage,被删除的元组,处于可vacuum状态 */
三、 查看page header内容
pg提供了pageinspect插件,可查看指定表对应的page header内容。
CREATE EXTENSION pageinspect;
创建一个测试表并插入一条数据
CREATE TABLE tbl (a int,b int);
begin;
INSERT INTO tbl VALUES(1,1);
select txid_current(); -- 看到事务id为737
javascript:void(0)
文章里提供了一个函数可以转换t_infomask和t_infomask2标记值,这里先创建该函数。
SELECT lp, t_xmin, t_xmax, t_field3 as t_cid, t_ctid,t_infomask, infomask(t_infomask,1) as infomask,t_infomask2,infomask(t_infomask2,2) as infomask2,t_data
FROM heap_page_items(get_raw_page('tbl', 0));
做一个提交操作,再查看元组信息
Commit;
发现没有变化
查询一下tbl表
Select * from tbl;
发现infomask变成了XMAX_INVALID|XMIN_COMMITTED。
XMIN_COMMITTED提供了元组可见性的快速判断方法。每次对元组进行查询时,如果发现元组所在事务已提交,就设置该标记位,避免每次都要去clog查询事务状态。
启动一个新事务,做更新操作
Begin;
Update tbl set a=2 where a=1;
select txid_current(); -- 看到事务id为738,增加了1
SELECT lp, t_xmin, t_xmax, t_field3 as t_cid, t_ctid,t_infomask, infomask(t_infomask,1) as infomask,t_infomask2,infomask(t_infomask2,2) as infomask2,t_data
FROM heap_page_items(get_raw_page('tbl', 0));
此时发现多了一条记录(页面内多了一个元组),从t_data列可以看出,它们分别是新旧两个版本。
在旧元组中:
- infomask标记只留下了XMIN_COMMITTED去掉了XMAX_INVALID,因为xmax中已经记录了删除这个元组的事务id。在pg中,对元组的update相当于删除旧元组并插入新元组。
- infomask2标记变为了HOT_UPDATED,表示元组已被更新,且更新产生的新元组与旧元组在同一页面中。这个标记通常与新元组的HEAP_ONLY_TUPLE标记成对出现。
在新元组中:
- infomask标记为UPDATED|XMAX_INVALID,表示这是一个由更新操作产生的新版本元组。
- infomask2标记为HEAP_ONLY_TUPLE,表示它目前还是一个HOT元组(可以防止产生重复的索引项,更容易清理)
再更新一次,然后提交
Update tbl set a=3 where a=2;
Commit;
SELECT lp, t_xmin, t_xmax, t_field3 as t_cid, t_ctid,t_infomask, infomask(t_infomask,1) as infomask,t_infomask2,infomask(t_infomask2,2) as infomask2,t_data
FROM heap_page_items(get_raw_page('tbl', 0));
- 元组1已被删除,其t_ctid指向元组2
- 元组2已被删除,其t_ctid指向元组3
- 元组3是当前事务元组,其t_ctid指向自己
执行一个vacuum操作
Vacuum;
SELECT lp,lp_off,lp_flags,t_xmin,t_xmax,t_field3 as t_cid, t_ctid,t_infomask, infomask(t_infomask,1) as infomask,t_infomask2,infomask(t_infomask2,2) as infomask2
FROM heap_page_items(get_raw_page('tbl', 0));
- 元组1中lp_flags=2(LP_REDIRECT),lp_off=3,说明它被重定向到了元组3
- 元组2中lp_flags=0(LP_UNUSED),说明这个槽位目前未使用
- 元组3中lp_flags=1(LP_NORMAL),说明在正常使用中
插入一个新元组
INSERT INTO tbl VALUES(3,3);
可以看到,它占用了原来的2号槽位
参考
《PostgreSQL技术内幕:事务处理深度探索》第3章