暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

postgres hot技术详解

原创 wonder 2023-07-25
735

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的版本信息。

pg_hot_01.jpg

hot更新

HOT是“Heap Only Tuple”(仅元组堆)的缩写,是为了提高update的效率,那么是如何提高的呢?

update一条数据后,如果旧行和新行是在同一个块内,那么旧行会有指针将自己的t_ctid指向新行的t_ctid,就好像旧行上有个软链接,链接到新行上面,旧行和新行的tuple数据形成了一个链,以tuple1更新为tuple4为例,访问新pg_hot_02.jpg行的路径为:

  • 访问旧行的指针linp1,指针指向旧行的tuple1位置
  • 旧行的tuple1的t_cid指向了新行tuple4的位置,而且tuple1内有标识标识(infomask2)这个链后没有结束,继续往下走
  • 新行tuple4内有标识标识自己是这个链的末端,返回tuple数据

但是因为旧tuple是dead tuple,会被vacuum回收掉,那么这个链就有问题了,所以会发生pruning剪枝操作,对行指针进行重定向,

旧元组的指针重新指向到新元组的指针,同样以tuple1更新为tuple4为例,访问新行的路径为:

  • 访问旧行的指针linp1,指针指向新行指针linp4的位置
  • 访问新行指针linp4,指向新行tuple4的位置,返回tuple数据
  • 合适的时候将旧行tuple1回收

pg_hot_03.jpg

那么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逻辑进行。

  1. 被更新的元组和之前的元组不在同一个页面内。
  2. 更新的列上存在索引。当索引的键更新时,会在索引页插入一条新的索引元组。

实验验证

创建测试表和索引,插入数据

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行为一致

最后修改时间:2023-07-27 13:53:12
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论