表的页中有两种堆元组,一种是普通元组,还有一种是TOAST元组。以下讲讲普通元组。
表元组结构图:
分为三部分。堆元组头部数据,null 值位图,用户数据。
note: src/include/access/htup_details.h
TransactionId t_xmin; /* inserting xact ID 插入本行的事务ID */
TransactionId t_xmax; /* deleting or locking xact ID 删除或者正在锁住本行的事务ID*/
CommandId t_cid; /* inserting or deleting command ID, or both 插入或者删除的命令ID,也就是这行命令在一个事务中顺序*/
Oid datum_typeid; /* composite type OID, or RECORDOID 这个地方暂时比较迷。*/
ItemPointerData t_ctid; /* current TID of this or newer tuple (or a speculative insertion token) 当前行的TID 或者是这一行的最新版本的ID*/
uint16 t_infomask2; /* number of attributes + various flags 存疑*/
uint16 t_infomask; /* various flag bits, see below 行的状态标记位 具体见下*/
uint8 t_hoff; /* sizeof header incl. bitmap, padding 存疑*/
bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs null 值位图*/
/*
* information stored in t_infomask:
*/
#define HEAP_HASNULL 0x0001 /* has null attribute(s) 有空值属性*/
#define HEAP_HASVARWIDTH 0x0002 /* has variable-width attribute(s) 有可变长属性*/
#define HEAP_HASEXTERNAL 0x0004 /* has external stored attribute(s) 有外部存储属性,也就是说跨行*/
#define HEAP_HASOID 0x0008 /* has an object-id field 有obect_id*/
#define HEAP_XMAX_KEYSHR_LOCK 0x0010 /* xmax is a key-shared locker xmax 是值分享锁*/
#define HEAP_COMBOCID 0x0020 /* t_cid is a combo cid t_cid 是组合cid*/
#define HEAP_XMAX_EXCL_LOCK 0x0040 /* xmax is exclusive locker xmax 是排他锁 */
#define HEAP_XMAX_LOCK_ONLY 0x0080 /* xmax, if valid, is only a locker xmax,如果有效,共享loker*/
/* xmax is a shared locker */
#define HEAP_XMAX_SHR_LOCK (HEAP_XMAX_EXCL_LOCK | HEAP_XMAX_KEYSHR_LOCK)
#define HEAP_LOCK_MASK (HEAP_XMAX_SHR_LOCK | HEAP_XMAX_EXCL_LOCK | \HEAP_XMAX_KEYSHR_LOCK)
#define HEAP_XMIN_COMMITTED 0x0100 /* t_xmin committed t_xmin 提交*/
#define HEAP_XMIN_INVALID 0x0200 /* t_xmin invalid/aborted t_xmin 无效或者中断*/
#define HEAP_XMIN_FROZEN (HEAP_XMIN_COMMITTED|HEAP_XMIN_INVALID) /*位或运算符,表明已经frozen */
#define HEAP_XMAX_COMMITTED 0x0400 /* t_xmax committed * t_xmax 提交/
#define HEAP_XMAX_INVALID 0x0800 * t_xmax invalid/aborted t_xmax 无效*/
#define HEAP_XMAX_IS_MULTI 0x1000 /* t_xmax is a MultiXactId t_xmax 是多事务ID*/
#define HEAP_UPDATED 0x2000 /* this is UPDATEd version of row 是一个更新版本的行*/
#define HEAP_MOVED_OFF 0x4000 /* moved to another place by pre-9.0 VACUUM FULL; kept for binary upgrade support 移动去了其他地方,被9.0版本之前的vacuum full 使用,为了支持binary升级留下 */
#define HEAP_MOVED_IN 0x8000 /* moved from another place by pre-9.0 VACUUM FULL; kept for binary upgrade support 从其他地方移动来,被9.0之前版本使用,为支持binary升级留下 */
#define HEAP_MOVED (HEAP_MOVED_OFF | HEAP_MOVED_IN)
#define HEAP_XACT_MASK 0xFFF0 /* visibility-related bits 可见性相关位*/
/*
* A tuple is only locked (i.e. not updated by its Xmax) if the
* HEAP_XMAX_LOCK_ONLY bit is set; or, for pg_upgrade's sake, if the Xmax is
* not a multi and the EXCL_LOCK bit is set.
*
* See also HeapTupleHeaderIsOnlyLocked, which also checks for a possible
* aborted updater transaction.
*
* Beware of multiple evaluations of the argument.
*/
#define HEAP_XMAX_IS_LOCKED_ONLY(infomask) \
(((infomask) & HEAP_XMAX_LOCK_ONLY) || \
(((infomask) & (HEAP_XMAX_IS_MULTI | HEAP_LOCK_MASK)) == HEAP_XMAX_EXCL_LOCK))
/*
* Use these to test whether a particular lock is applied to a tuple
*/
#define HEAP_XMAX_IS_SHR_LOCKED(infomask) \
(((infomask) & HEAP_LOCK_MASK) == HEAP_XMAX_SHR_LOCK)
#define HEAP_XMAX_IS_EXCL_LOCKED(infomask) \
(((infomask) & HEAP_LOCK_MASK) == HEAP_XMAX_EXCL_LOCK)
#define HEAP_XMAX_IS_KEYSHR_LOCKED(infomask) \
(((infomask) & HEAP_LOCK_MASK) == HEAP_XMAX_KEYSHR_LOCK)
/* turn these all off when Xmax is to change */
#define HEAP_XMAX_BITS (HEAP_XMAX_COMMITTED | HEAP_XMAX_INVALID | \
HEAP_XMAX_IS_MULTI | HEAP_LOCK_MASK | HEAP_XMAX_LOCK_ONLY)
复制
所以,其实不用大费周章去从clog 里面获取事务状态(当然数据库还是要去内存里的clog里面检查事务状态的),直接从t_infomask 就可以知道。
下面就做实验来验证一下。
这里需要特别提到一个扩展(extension) pageinspect,因为这个插件,很多实验才能方便的进行下去。
insert 操作
ivandb=# create table t1 (age int);
CREATE TABLE
ivandb=# begin
ivandb-# ;
BEGIN
ivandb=# select txid_current();
txid_current
--------------
706
(1 row)
ivandb=# insert into t1 values(10);
INSERT 0 1
ivandb=# commit;
COMMIT
ivandb=#
ivandb=# select * from heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
1 | 8160 | 1 | 28 | 706 | 0 | 0 | (0,1) | 1 | 2048 | 24 | | | \x0a000000
(1 row)
复制
这里可以看到t_infomask值为2048。即:t_xmax invalid/aborted
t_xmin 是706, 插入本行的事务ID。
但是我们明明提交了,为什么这里看不到xmin 提交呢?这个地方涉及到一个类似Oracle的延迟块清除(猜测)。也就是说,只管写入数据成功,然后返回事务,告知事务管理器,然后等其他操作执行完毕,事务提交。就不再回来在行上写入事务的状态,等下一次操作本行的时候,自然会检查事务状态,再这时候写入,耗费较小。毕竟,插入行的那个事务里面,很有可能其他操作会失败,所以不知道情况。万一真的失败要回滚。到时候又要回来修改事务状态不划算,不如只记录事务状态。
所以,此处我们只要select 一下。
ivandb=# select * from t1;
age
-----
10
(1 row)
ivandb=# select * from heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
1 | 8160 | 1 | 28 | 706 | 0 | 0 | (0,1) | 1 | 2304 | 24 | | | \x0a000000
(1 row)
复制
2304 即(2048+256) t_xmax invalid/aborted + t_xmin committed
另外,我们也可以看到t_ctid 是(0,1)也就是他自己这行本身。
t_field3 也就是cid 此时是0, 也就是第一个命令。(只有一个insert)
delete 操作
ivandb=# begin;
BEGIN
ivandb=# select txid_current();
txid_current
--------------
708
(1 row)
ivandb=# delete from t1 where age=10;
DELETE 1
ivandb=# commit;
COMMIT
ivandb=# select * from heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
1 | 8160 | 1 | 28 | 706 | 708 | 0 | (0,1) | 8193 | 256 | 24 | | | \x0a000000
(1 row)
复制
此时可以看到 t_xmax 也出现了, t_infomask 修改为256 t_xmin committed.
查询一下表,刷一下事务状态。(以下不表)
ivandb=# select * from t1;
age
-----
(0 rows)
ivandb=# select * from heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
1 | 8160 | 1 | 28 | 706 | 708 | 0 | (0,1) | 8193 | 1280 | 24 | | | \x0a000000
(1 row)
复制
此时的事务状态是:(256+1024)t_xmax committed + t_xmin committed, 也就是这行已经是提交,不存在了的。在他之后的事务都无法查询到该行。
所以,暂且可以认为,如果一行的t_xmin 以及t_xmax 都已经提交,且t_ctid 等于他自己,那么这行是已删除状态(dead tuple)。
update 操作。
ivandb=# insert into t1 values(10);
INSERT 0 1
ivandb=# commit;
WARNING: there is no transaction in progress
COMMIT
ivandb=#
ivandb=# begin;
BEGIN
ivandb=# select txid_current();
txid_current
--------------
710
(1 row)
ivandb=# update t1 set age=11;
UPDATE 1
ivandb=# commit;
COMMIT
ivandb=# select * from t1;
age
-----
11
(1 row)
ivandb=# select * from heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
1 | 8160 | 1 | 28 | 706 | 708 | 0 | (0,1) | 8193 | 1280 | 24 | | | \x0a000000
2 | 8128 | 1 | 28 | 709 | 710 | 0 | (0,3) | 16385 | 1280 | 24 | | | \x0a000000
3 | 8096 | 1 | 28 | 710 | 0 | 0 | (0,3) | 32769 | 10496 | 24 | | | \x0b000000
复制
可以看到表中有三行数据,第一行为已经删除的行。第二行也是已经删除的行(即再次执行的insert age=10)。第三行为更新过的行。也就是age=11.
所以本质上更新是删除以前的行,同时插入一行新数据。同时将旧行的t_ctid 修改为新行的地址(0,3)
可以看到,ctid(0,2) 的t_infomask 为(256+1024)t_xmax committed + t_xmin committed。所以上面的话再修改下。
暂且可以认为,如果一行的t_xmin 以及t_xmax 都已经提交,无论t_ctid是否等于他自己,那么这行是已删除状态(dead tuple),因为理论上只有delete 和update 会更新t_xmax。
再看ctid(0,3), t_xmax 为0. t_infomask 为10496(8192+2048+256)this is UPDATEd version of row+ t_xmax invalid/aborted + t_xmin committed
还是比较清楚的。
回滚操作。
ivandb=# begin;
BEGIN
ivandb=# select txid_current();
txid_current
--------------
711
(1 row)
ivandb=# update t1 set age=12;
UPDATE 1
ivandb=# rollback;
ROLLBACK
ivandb=# select * from t1;
age
-----
11
(1 row)
ivandb=# select * from heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
1 | 8160 | 1 | 28 | 706 | 708 | 0 | (0,1) | 8193 | 1280 | 24 | | | \x0a000000
2 | 8128 | 1 | 28 | 709 | 710 | 0 | (0,3) | 16385 | 1280 | 24 | | | \x0a000000
3 | 8096 | 1 | 28 | 710 | 711 | 0 | (0,4) | 49153 | 10496 | 24 | | | \x0b000000
4 | 8064 | 1 | 28 | 711 | 0 | 0 | (0,4) | 32769 | 10752 | 24 | | | \x0c000000
复制
可以看到表中新增了一行数据ctid(0,4),
此时这行的状态是10752,即(8192+2048+512)this is UPDATEd version of row + t_xmax invalid/aborted + t_xmin invalid/aborted.
即这行是无效的行(dead tuple)。对于ctid(0,3) ,虽然t_xmax 设置有值,但是 t_infomask 为10496(8192+2048+256)this is UPDATEd version of row+ t_xmax invalid/aborted + t_xmin committed. 故此行对后面的session 可见。
至此,引入我一个天真的遐想,如果这样的话,那在我们vacuum (vacuum 会清理dead tuple)前,我们是有机会对这个表进行闪回查询的。只需要设定规则。数据为t_data.
因为实验数据用的是int 类型,所以直接16进制转10进制即可。
对于varchar 或者text 可以通过以下sql 查询转换。
ivandb=# create table t2(name text);
CREATE TABLE
ivandb=# insert into t2 values('ivan 是中国人');
INSERT 0 1
ivandb=# select convert_from(t_data,'UTF-8') from heap_page_items(get_raw_page('t2',0));
convert_from
----------------
%ivan 是中国人
(1 row)
ivandb=# select substring(convert_from(t_data,'UTF-8'),2) from heap_page_items(get_raw_page('t2',0));
substring
---------------
ivan 是中国人
(1 row)
复制
note:不加substring ,前面有奇怪的字符,暂时不解。待有缘人告知。