postgres hot技术详解
传统的update
postgres采用的是MVCC多版本并发机制,当一个表的一行数据被更新时,其实是插入了一行新的数据,然后更新旧行的一些标识位,将旧行标识为一个dead tuple,这样一行数据就有多个版本了。
新的tuple在page中有了新的物理位置t_cid(blockno,offset),这个肯定是必然的开销;如果这个表上有索引,索引叶子结点存储的是tuple的tcid,那么索引也需要发生更新,新增一条indextuple,新的indextuple的tcid指向新的tuple,旧的indextuple标志为dead。那么带来了一个问题,假如表上索引非常多,那么该表每个索引都要发生更新,update的性能会非常低;一个update需要维护1个tupe + n个index tuple的版本信息。

hot更新
HOT是“Heap Only Tuple”(仅元组堆)的缩写,是为了提高update的效率,那么是如何提高的呢?
update一条数据后,如果旧行和新行是在同一个块内,那么旧行会有指针将自己的t_ctid指向新行的t_ctid,就好像旧行上有个软链接,链接到新行上面,旧行和新行的tuple数据形成了一个链,以tuple1更新为tuple4为例,访问新
行的路径为:
- 访问旧行的指针linp1,指针指向旧行的tuple1位置
- 旧行的tuple1的t_cid指向了新行tuple4的位置,而且tuple1内有标识标识(infomask2)这个链后没有结束,继续往下走
- 新行tuple4内有标识标识自己是这个链的末端,返回tuple数据
但是因为旧tuple是dead tuple,会被vacuum回收掉,那么这个链就有问题了,所以会发生pruning剪枝操作,对行指针进行重定向,
旧元组的指针重新指向到新元组的指针,同样以tuple1更新为tuple4为例,访问新行的路径为:
- 访问旧行的指针linp1,指针指向新行指针linp4的位置
- 访问新行指针linp4,指向新行tuple4的位置,返回tuple数据
- 合适的时候将旧行tuple1回收

那么pruning操作发生在什么时候呢? 有些文章中写道进行select,update,delete,insert的时候就会发生pruning操作,后面我们用实验进行验证。
hot的优点
上面详细描述了hot的工作方式,那么带来了什么好处呢? 可以发现访问新tuple时,是从访问旧行的指针开始的,虽然更新tuple导致实际的tuple在page内的物理位置发生了变化,但是访问该tuple的指针没有发生变化。
文章开头提到了因为tuple更新,指针位置发生变化,导致索引页需要同步更新,造成了update效率低下;那么使用了hot之后,tuple更新了,但是指针位置没有发生变化,进行索引扫描时,通过indextuple中的t_cid依然能访问到原来旧指针的物理位置,然后访问到新行的tuple数据,索引不需要额外进行更新维护了!结合pruning操作,清理了dead tuple,一定程度上降低了vacuum的负载。
什么时候进行pruning动作呢?
1、vacuum的时候,扫描page会进行pruning
2、hot update后page会通过PageSetPrunable(page, xid)函数标记为Prunable,下一次扫描到该page的时候可能会进行pruning动作,所以select, update,delete,insert都可能触发pruning动作,但需要满足一个条件:页面空间不足。页面还有剩余空间的情况下,只有进行vacuum操作才能进行pruning。
hot带来两项性能优化:
1、不必修改索引。由于元组的指针没有发生变化,因此仍可以使用原始索引条目。索引扫描遵循hot链以找到适当的元组。
2、在正常操作(包括select)期间,可以完全删除更新行的旧版本,而不需要定期进行vacuum操作。
hot的限制
hot也不是任何情况都适用的,以下场景下hot将不适用,update按照传统的update逻辑进行。
- 被更新的元组和之前的元组不在同一个页面内。
- 更新的列上存在索引。当索引的键更新时,会在索引页插入一条新的索引元组。
实验验证
创建测试表和索引,插入数据
create table t1(c1 int,c2 int, c3 int);
create index idx_t1_c1 on t1(c1);
create index idx_t1_c2 on t1(c2);
insert into t1 values(1,1,1),(2,2,2),(3,3,3);
select * from t1;
c1 | c2 | c3
----+----+----
1 | 1 | 1
2 | 2 | 2
3 | 3 | 3
使用pageinspect查看heap的items
postgres=# 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 | 8152 | 1 | 36 | 835 | 0 | 0 | (0,1) | 3 | 2048 | 24 | | | \x010000000100000001000000
2 | 8112 | 1 | 36 | 835 | 0 | 0 | (0,2) | 3 | 2048 | 24 | | | \x020000000200000002000000
3 | 8072 | 1 | 36 | 835 | 0 | 0 | (0,3) | 3 | 2048 | 24 | | | \x030000000300000003000000
再记录下pageheader信息
postgres=# select * from page_header(get_raw_page('t1',0));
lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid
------------+----------+-------+-------+-------+---------+----------+---------+-----------
0/840024C0 | 0 | 0 | 36 | 8072 | 8192 | 8192 | 4 | 0
因为会用到infomask的计算,这里也展示下infomask的数据结构和的各种标志位信息
uint16 t_infomask; /* various flag bits, see below */
| Flag | 10进制 | Mask | Describe |
|---|---|---|---|
| 0x0001 | 1 | HEAP_HASNULL | 有 NULL 值的属性 |
| 0x0002 | 2 | HEAP_HASVARWIDTH | 有变宽的属性(varchar 等) |
| 0x0004 | 4 | HEAP_HASEXTERNAL | 有存储在外部的属性 (TOAST) |
| 0x0008 | 8 | HEAP_HASOID_OLD | 有一个 OID 字段 |
| 0x0010 | 16 | HEAP_XMAX_KEYSHR_LOCK | XMAX (执行删除的事务) 是一个 key-shared 锁 |
| 0x0020 | 32 | HEAP_COMBOCID | t_cid 是一个复合 cid (既包含 CMIN 也包含 CMAX,在同一个事务中创建并删除) |
| 0x0040 | 64 | HEAP_XMAX_EXCL_LOCK | XMAX (执行删除的事务) 是一个 exclusive 锁 |
| 0x0080 | 128 | HEAP_XMAX_LOCK_ONLY | 如果 XMAX 域有效,那么仅仅是一个锁 |
| 0x0100 | 256 | HEAP_XMIN_COMMITTED | XMIN (插入操作) 对应的事务已经提交,即当前 tuple 已经创建成功 |
| 0x0200 | 512 | HEAP_XMIN_INVALID | XMIN (插入操作) 对应的事务无效或者已经被终止了 |
| 0x0400 | 1024 | HEAP_XMAX_COMMITTED | XMAX (删除操作) 对应的事务已经提交,即当前 tuple 已经被删除了 |
| 0x0800 | 2048 | HEAP_XMAX_INVALID | XMAX (删除操作) 对应的事务无效或者已经被终止了 |
| 0x1000 | 4096 | HEAP_XMAX_IS_MULTI | XMAX (删除操作) 对应的事务是一个多段事务 ID |
| 0x2000 | 8192 | HEAP_UPDATED | 这是数据行被更新后的版本 |
| 0x4000 | 16384 | HEAP_MOVED_OFF | 被 9.0 之前的 VACUUM FULL 移动到另外的地方,为了兼容二进制程序升级而保留 |
| 0x8000 | 32768 | HEAP_MOVED_IN | 与 HEAP_MOVED_OFF 相对,表明是从别处移动过来的,也是为了兼容性而保留 |
因为会用到infomask2的计算,这里也展示下infomask2的数据结构和的各种标志位信息
uint16 t_infomask2; /* number of attributes + various flags */
number of attributes指的是tuple中columns的数量,上面的items中都是3
| Flag | 10进制 | Mask | Describe |
|---|---|---|---|
| 0x07FF | 2047 | HEAP_NATTS_MASK | 记录了属性(字段)的数量 |
| 0x2000 | 8192 | HEAP_KEYS_UPDATED | tuple 被更新且列被修改了,或者 tuple 被删除了 |
| 0x4000 | 16384 | HEAP_HOT_UPDATED | tuple 被使用 HOT 方式更新了 |
| 0x8000 | 32768 | HEAP_ONLY_TUPLE | 这是 HOT tuple |
| 0xE000 | 57344 | HEAP2_XACT_MASK | 与可见性相关的位 |
hot更新
c3列不包含索引,更新c3列的时候将使用hot
update t1 set c3=10 where c3=1;
查看c1,c2列的索引items信息,依然是3条,说明hot uodate,index没有受到影响
postgres=# select * from bt_page_items('idx_t1_c1', 1);
itemoffset | ctid | itemlen | nulls | vars | data | dead | htid | tids
------------+-------+---------+-------+------+-------------------------+------+-------+------
1 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 00 | f | (0,1) |
2 | (0,2) | 16 | f | f | 02 00 00 00 00 00 00 00 | f | (0,2) |
3 | (0,3) | 16 | f | f | 03 00 00 00 00 00 00 00 | f | (0,3) |
(3 rows)
postgres=# select * from bt_page_items('idx_t1_c2', 1);
itemoffset | ctid | itemlen | nulls | vars | data | dead | htid | tids
------------+-------+---------+-------+------+-------------------------+------+-------+------
1 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 00 | f | (0,1) |
2 | (0,2) | 16 | f | f | 02 00 00 00 00 00 00 00 | f | (0,2) |
3 | (0,3) | 16 | f | f | 03 00 00 00 00 00 00 00 | f | (0,3) |
(3 rows)
pruning测试记录
再来通过数据确定下pruning在说明情况下发生,使用pageinspect模块查看heap items
postgres=# 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 | 8152 | 1 | 36 | 835 | 836 | 0 | (0,4) | 16387 | 256 | 24 | | | \x010000000100000001000000
2 | 8112 | 1 | 36 | 835 | 0 | 0 | (0,2) | 3 | 2304 | 24 | | | \x020000000200000002000000
3 | 8072 | 1 | 36 | 835 | 0 | 0 | (0,3) | 3 | 2304 | 24 | | | \x030000000300000003000000
4 | 8032 | 1 | 36 | 836 | 0 | 0 | (0,4) | 32771 | 10240 | 24 | | | \x01000000010000000a000000
(4 rows)
// page hader
postgres=# select * from page_header(get_raw_page('t1',0));
lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid
------------+----------+-------+-------+-------+---------+----------+---------+-----------
0/850020D8 | 0 | 0 | 40 | 8032 | 8192 | 8192 | 4 | 836
指针lp1的t_xmax=836, lp4的t_xmin=836,表示事务id86更新了这一行数据,它们都指向了同一物理位置t_ctid( 0,4 )
- infomask2变化
lp1对应tuple的infomask2信息变化:
从3变为16387 = 16384(HEAP_HOT_UPDATED ) + 3 (attributes),说明该tuple进行了hot更新
lp4对应tuple的infomask2信息变化:
从3变为32771 = 32768 (HEAP_ONLY_TUPLE)+ 3(attributes),说明是hot tuple的最终数据
-
t_ctid变化
lp1的t_ctid从(0,1)变为(0,4),说明tuple1指向了tuple4的所在位置
-
header变化
排除lower,upper,prune_xid从0变成了836,表示了需要pruning的事物id为836
指linp1依然指向了tuple1,tuple1指向tuple4,说明还没有发生pruning
这里select一次后再查看items看看
select * from t1;
c1 | c2 | c3
----+----+----
2 | 2 | 2
3 | 3 | 3
1 | 1 | 10
(3 rows)
postgres=# 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 | 8152 | 1 | 36 | 835 | 836 | 0 | (0,4) | 16387 | 1280 | 24 | | | \x010000000100000001000000
2 | 8112 | 1 | 36 | 835 | 0 | 0 | (0,2) | 3 | 2304 | 24 | | | \x020000000200000002000000
3 | 8072 | 1 | 36 | 835 | 0 | 0 | (0,3) | 3 | 2304 | 24 | | | \x030000000300000003000000
4 | 8032 | 1 | 36 | 836 | 0 | 0 | (0,4) | 32771 | 10496 | 24 | | | \x01000000010000000a000000
(4 rows)
postgres=# select * from page_header(get_raw_page('t1',0));
lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid
------------+----------+-------+-------+-------+---------+----------+---------+-----------
0/87000128 | 0 | 0 | 40 | 8032 | 8192 | 8192 | 4 | 836
(1 row)
- infomask变化
lp1对应tuple的infomask信息变化:
从256 HEAP_XMIN_COMMITTED 变为1280(1024 HEAP_XMAX_COMMITTED + 256HEAP_XMIN_COMMITTED
HEAP_XMAX_COMMITTED解释为对应的事务已经提交,即当前 tuple 已经被删除了,标记为了dead tuple
lp4对应tuple的infomask信息变化:
从10240(8192 + 2048)被更新后的版本+ 对应的事务无效或者已经被终止变为了10496
10496 = (8192 + 2048 + 256),多了个256表示HEAP_XMIN_COMMITTED,该tuple为的插入操作commit
- header信息没有发生变化
因为还有剩余空间,所以没有发生pruning
vacuum测试pruning变化
vacuum之前看page header,(8192-8032)/ 4 =40,说明一个实际一个tuple占用了40字节(page内是36字节,4个字节是空白)
postgres=# select * from page_header(get_raw_page('t1',0));
lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid
------------+----------+-------+-------+-------+---------+----------+---------+-----------
0/87000128 | 0 | 0 | 40 | 8032 | 8192 | 8192 | 4 | 836
(1 row)
进行vacuum后的信息
postgres=# select * from page_header(get_raw_page('t1',0));
lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid
------------+----------+-------+-------+-------+---------+----------+---------+-----------
0/89002198 | 0 | 4 | 40 | 8072 | 8192 | 8192 | 4 | 0
postgres=# 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 | 4 | 2 | 0 | | | | | | | | | |
2 | 8152 | 1 | 36 | 835 | 0 | 0 | (0,2) | 3 | 2304 | 24 | | | \x020000000200000002000000
3 | 8112 | 1 | 36 | 835 | 0 | 0 | (0,3) | 3 | 2304 | 24 | | | \x030000000300000003000000
4 | 8072 | 1 | 36 | 836 | 0 | 0 | (0,4) | 32771 | 10496 | 24 | | | \x01000000010000000a000000
依然是4个指针(40-24)/ 4 = 4,upper从8032到8072,8072-8032=40,释放了40字节空间,刚好是一个tuple占用的空间
lp1的lp_flags是从0变为2,其他字段都为空了,看下lp_flag的定义
/*
* lp_flags has these possible states. An UNUSED line pointer is available
* for immediate re-use, the other states are not.
*/
#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) */
#define LP_DEAD 3 /* dead, may or may not have storage */
2表示是一个hot重定向,lp_off=4表示重定向到lp4
得出结论:vacuum的时候会发生pruning
填满page测试pruning
通过上面pruning测试记录,页面有空闲空间的时候,进行页面扫描操作(select)不会触发pruning,现在把一个page填充慢试试
一个page 8192字节,减去头部24字节,可用为8168字节
每插入一个tuple,lower的最右边多一个指针4字节,upper最作左边多tupledate40字节,合集44字节
填满一个page需要8168/44=185.6,取185
流程为先清理表,造一个hot update,再填满到185条记录
truncate table t1;
insert into t1 values(1,1,1),(2,2,2),(3,3,3);
update t1 set c3=10 where c3=1;
insert into t1 select n,n,n from generate_series(4,185) n;
查看header信息
postgres=# select * from page_header(get_raw_page('t1',0));
lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid
------------+----------+-------+-------+-------+---------+----------+---------+-----------
0/8F008DA8 | 0 | 0 | 764 | 792 | 8192 | 8192 | 4 | 842
free = 792-764=28
items信息
postgres=# select * from heap_page_items(get_raw_page('t1',0)) where lp <5;
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 | 8152 | 1 | 36 | 841 | 842 | 0 | (0,4) | 16387 | 1280 | 24 | | | \x010000000100000001000000
2 | 8112 | 1 | 36 | 841 | 0 | 0 | (0,2) | 3 | 2304 | 24 | | | \x020000000200000002000000
3 | 8072 | 1 | 36 | 841 | 0 | 0 | (0,3) | 3 | 2304 | 24 | | | \x030000000300000003000000
4 | 8032 | 1 | 36 | 842 | 0 | 0 | (0,4) | 32771 | 10496 | 24 | | | \x01000000010000000a000000
(4 rows)
lp1还没有被pruning
执行一个select: select * from t1, 然后再查看items
postgres=# select * from heap_page_items(get_raw_page('t1',0)) where lp <5;
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 | 4 | 2 | 0 | | | | | | | | | |
2 | 8152 | 1 | 36 | 841 | 0 | 0 | (0,2) | 3 | 2304 | 24 | | | \x020000000200000002000000
3 | 8112 | 1 | 36 | 841 | 0 | 0 | (0,3) | 3 | 2304 | 24 | | | \x030000000300000003000000
4 | 8072 | 1 | 36 | 842 | 0 | 0 | (0,4) | 32771 | 10496 | 24 | | | \x01000000010000000a000000
(4 rows)
lp1发生pruning了,清空了数据,lp_flags变为2,说明没有足够剩余空间,扫描页面的时候发生pruning
非hot更新
为了方便查看,将测试数据重建
create table t1(c1 int,c2 int, c3 int);
create index idx_t1_c1 on t1(c1);
create index idx_t1_c2 on t1(c2);
insert into t1 values(1,1,1),(2,2,2),(3,3,3);
select * from t1;
c1 | c2 | c3
----+----+----
1 | 1 | 1
2 | 2 | 2
3 | 3 | 3
更新索引列
update t1 set c1=11 where c1=1
查看c1 c2列的索引items
postgres=# select * from bt_page_items('idx_t1_c1', 1);
itemoffset | ctid | itemlen | nulls | vars | data | dead | htid | tids
------------+-------+---------+-------+------+-------------------------+------+-------+------
1 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 00 | f | (0,1) |
2 | (0,2) | 16 | f | f | 02 00 00 00 00 00 00 00 | f | (0,2) |
3 | (0,3) | 16 | f | f | 03 00 00 00 00 00 00 00 | f | (0,3) |
4 | (0,4) | 16 | f | f | 0b 00 00 00 00 00 00 00 | f | (0,4) |
(4 rows)
postgres=# select * from bt_page_items('idx_t1_c2', 1);
itemoffset | ctid | itemlen | nulls | vars | data | dead | htid | tids
------------+-------+---------+-------+------+-------------------------+------+-------+------
1 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 00 | f | (0,1) |
2 | (0,4) | 16 | f | f | 01 00 00 00 00 00 00 00 | f | (0,4) |
3 | (0,2) | 16 | f | f | 02 00 00 00 00 00 00 00 | f | (0,2) |
4 | (0,3) | 16 | f | f | 03 00 00 00 00 00 00 00 | f | (0,3) |
(4 rows)
更新了c1列,所有索引都会产生新的多版本index tuples
进行vacuum前后,heap items变化
postgres=# 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 | 8152 | 1 | 36 | 832 | 833 | 0 | (0,4) | 3 | 256 | 24 | | | \x010000000100000001000000
2 | 8112 | 1 | 36 | 832 | 0 | 0 | (0,2) | 3 | 2048 | 24 | | | \x020000000200000002000000
3 | 8072 | 1 | 36 | 832 | 0 | 0 | (0,3) | 3 | 2048 | 24 | | | \x030000000300000003000000
4 | 8032 | 1 | 36 | 833 | 0 | 0 | (0,4) | 3 | 10240 | 24 | | | \x0b0000000100000001000000
(4 rows)
postgres=# vacuum t1;
VACUUM
postgres=# 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 | 0 | 0 | 0 | | | | | | | | | |
2 | 8152 | 1 | 36 | 832 | 0 | 0 | (0,2) | 3 | 2304 | 24 | | | \x020000000200000002000000
3 | 8112 | 1 | 36 | 832 | 0 | 0 | (0,3) | 3 | 2304 | 24 | | | \x030000000300000003000000
4 | 8072 | 1 | 36 | 833 | 0 | 0 | (0,4) | 3 | 10496 | 24 | | | \x0b0000000100000001000000
(4 rows)
直接将lp1的tuple删除了,与传统update行为一致




