drop table hot;
create table hot (id int , s char(2000));
create index hot_id on hot(id);
insert into hot values (1,'A');
auxdb=# SELECT * FROM heap_page_items(get_raw_page('hot',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
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
1 | 6160 | 1 | 2032 | 24467 | 0 | 0 | (0,1) | 2 | 2050 | 24 | |
(1 row)
lp: 行指针(Line Pointer)的索引,表示这是页面上的第几个条目。
lp_off: 行指针的偏移量,表示该行数据在页面中的物理位置。
lp_flags: 行指针的标志位,用于指示行的状态,如是否已删除等。
lp_len: 行数据的长度。
t_xmin: 插入该行的事务ID。
t_xmax: 删除或更新该行的事务ID(如果为0,表示该行未被删除或更新)。
t_field3: 在较新版本的PostgreSQL中,这个字段通常用于存储行版本信息,但在某些版本中可能未使用或保留。
t_ctid: 行的CTID(块号,元组索引),表示该行数据在表中的物理位置。这里的(0,1)表示这是该页面上的第一条记录。
t_infomask2: 第二个信息掩码,包含关于行的额外信息,如是否有空值、是否压缩等。
t_infomask: 第一个信息掩码,包含关于行的基本信息,如是否已删除、是否有隐藏列等。
t_hoff: 行头(header)的偏移量,表示行头数据在行中的位置。
t_bits: 指向行中NULL值位图的指针(如果行中有可变长度的字段,则可能使用此位图来标记哪些字段为NULL)。
t_oid: 对象的ID(对于没有OID的表,这个字段通常不适用)。
从结果中可以看出,这个页面上有且仅有一条记录,这条记录由事务ID为24467的事务插入,且目前没有被任何事务删除或更新(t_xmax为0)。这条记录的物理位置由lp_off(6160)和t_ctid(0,1)共同确定,表示它是该页面上的第一条记录,且从页面开始偏移6160字节处开始存储。update hot set s = 'B';
update hot set s = 'C';
update hot set s = 'D';auxdb=# SELECT * FROM heap_page_items(get_raw_page('hot',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
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
1 | 6160 | 1 | 2032 | 24372 | 24373 | 0 | (0,2) | 16386 | 1282 | 24 | |
2 | 4128 | 1 | 2032 | 24373 | 24374 | 0 | (0,3) | 49154 | 9474 | 24 | |
3 | 2096 | 1 | 2032 | 24374 | 24375 | 0 | (0,4) | 49154 | 8450 | 24 | |
4 | 64 | 1 | 2032 | 24375 | 0 | 0 | (0,4) | 32770 | 10242 | 24 | |
(4 rows)auxdb=# \dt+ hot
List of relations
Schema | Name | Type | Owner | Size | Storage | Description
--------+------+-------+--------+-------+----------------------------------+-------------
public | hot | table | sysomm | 16 kB | {orientation=row,compression=no} |
(1 row)update hot set s = 'E';auxdb=# \dt+ hot;
List of relations
Schema | Name | Type | Owner | Size | Storage | Description
--------+------+-------+--------+-------+----------------------------------+-------------
public | hot | table | sysomm | 48 kB | {orientation=row,compression=no} |
(1 row)
-- 更新第五行数据后,表size变大
auxdb=# SELECT * FROM heap_page_items(get_raw_page('hot',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
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
1 | 6160 | 1 | 2032 | 24483 | 24484 | 0 | (0,2) | 16386 | 1282 | 24 | |
2 | 4128 | 1 | 2032 | 24484 | 24485 | 0 | (0,3) | 49154 | 9474 | 24 | |
3 | 2096 | 1 | 2032 | 24485 | 24486 | 0 | (0,4) | 49154 | 9474 | 24 | |
4 | 64 | 1 | 2032 | 24486 | 24487 | 0 | (1,1) | 32770 | 8450 | 24 | |
(4 rows)
auxdb=# SELECT * FROM heap_page_items(get_raw_page('hot',1));
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
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
1 | 6160 | 1 | 2032 | 24487 | 0 | 0 | (1,1) | 2 | 10242 | 24 | |
(1 row)
-- 新数据出现在第二个块上(多出来一个块存放数据)
vacuum hot;
vacuum hot;
vacuum hot;
auxdb=# \dt+ hot;
List of relations
Schema | Name | Type | Owner | Size | Storage | Description
--------+------+-------+--------+-------+----------------------------------+-------------
public | hot | table | sysomm | 56 kB | {orientation=row,compression=no} |
(1 row)
-- vacuum 表后,表size 没有缩小(表只有一行数据)
auxdb=# SELECT * FROM heap_page_items(get_raw_page('hot',1));
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
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
1 | 6160 | 1 | 2032 | 24487 | 0 | 0 | (1,1) | 2 | 10498 | 24 | |
(1 row)
-- 数据还在第二个块上auxdb=# vacuum full hot;
VACUUM
auxdb=# \dt+ hot;
List of relations
Schema | Name | Type | Owner | Size | Storage | Description
--------+------+-------+--------+-------+----------------------------------+-------------
public | hot | table | sysomm | 16 kB | {orientation=row,compression=no} |
(1 row)
-- vacuum full 可以缩小表size
VACUUM
功能:VACUUM命令主要用于回收表中的空间,使得已被删除的行(dead tuples)所占用的空间可以被再次使用。它并不会将这部分空间释放回操作系统,而是将其标记为可重用。
VACUUM FULL
功能:VACUUM FULL命令则更为彻底。它不仅会回收空间,还会将表的内容重新写入一个没有垃圾数据的新文件中,并释放旧表占用的空间给操作系统。这实际上是对表进行了一次物理重建。drop table hot;
create table hot (id int , s char(2000));
create index hot_id on hot(id);
insert into hot values (1,'A');
insert into hot values (2,'A1');
update hot set s = 'B';
update hot set s = 'C';
update hot set s = 'D';
vacuum hot;
vacuum hot;
vacuum hot;auxdb=# SELECT * FROM heap_page_items(get_raw_page('hot',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
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
1 | 6160 | 1 | 2032 | 24493 | 24495 | 0 | (0,3) | 16386 | 1282 | 24 | |
2 | 4128 | 1 | 2032 | 24494 | 24495 | 0 | (0,4) | 16386 | 1282 | 24 | |
3 | 2096 | 1 | 2032 | 24495 | 24496 | 0 | (1,1) | 32770 | 9474 | 24 | |
4 | 64 | 1 | 2032 | 24495 | 24496 | 0 | (1,2) | 32770 | 9474 | 24 | |
(4 rows)
auxdb=# SELECT * FROM heap_page_items(get_raw_page('hot',1));
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
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
1 | 6160 | 1 | 2032 | 24496 | 24497 | 0 | (1,3) | 16386 | 9474 | 24 | |
2 | 4128 | 1 | 2032 | 24496 | 24497 | 0 | (1,4) | 16386 | 9474 | 24 | |
3 | 2096 | 1 | 2032 | 24497 | 0 | 0 | (1,3) | 32770 | 10498 | 24 | |
4 | 64 | 1 | 2032 | 24497 | 0 | 0 | (1,4) | 32770 | 10498 | 24 | |
(4 rows)
vacuum hot;
vacuum hot;
vacuum hot;vacuum full hot;
auxdb=# SELECT * FROM heap_page_items(get_raw_page('hot',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
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
1 | 6160 | 1 | 2032 | 24497 | 0 | 0 | (0,1) | 2 | 10498 | 24 | |
2 | 4128 | 1 | 2032 | 24496 | 24497 | 0 | (0,1) | 2 | 9474 | 24 | |
3 | 2096 | 1 | 2032 | 24495 | 24496 | 0 | (0,2) | 2 | 9474 | 24 | |
4 | 64 | 1 | 2032 | 24493 | 24495 | 0 | (0,3) | 2 | 1282 | 24 | |
(4 rows)
auxdb=# SELECT * FROM heap_page_items(get_raw_page('hot',1));
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
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
1 | 6160 | 1 | 2032 | 24497 | 0 | 0 | (1,1) | 2 | 10498 | 24 | |
2 | 4128 | 1 | 2032 | 24496 | 24497 | 0 | (1,1) | 2 | 9474 | 24 | |
3 | 2096 | 1 | 2032 | 24495 | 24496 | 0 | (1,2) | 2 | 9474 | 24 | |
4 | 64 | 1 | 2032 | 24494 | 24495 | 0 | (1,3) | 2 | 1282 | 24 | |
(4 rows)
vacuum hot;
--vacuum full 没有缩小表size,重新整理了2行数据,一个数据块一行数据,最新的数据放在了块的最前面
drop table hot;
create table hot (id int , s char(2000));
create index hot_id on hot(id);
insert into hot values (1,'A');
insert into hot values (2,'A1');
insert into hot values (3,'A2');
update hot set s = 'B';
update hot set s = 'C';
update hot set s = 'D';
auxdb=# SELECT * FROM heap_page_items(get_raw_page('hot',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
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
1 | 6160 | 1 | 2032 | 24502 | 24505 | 0 | (0,4) | 16386 | 1282 | 24 | |
2 | 4128 | 1 | 2032 | 24503 | 24505 | 0 | (1,1) | 2 | 1282 | 24 | |
3 | 2096 | 1 | 2032 | 24504 | 24505 | 0 | (1,2) | 2 | 1282 | 24 | |
4 | 64 | 1 | 2032 | 24505 | 24506 | 0 | (1,3) | 32770 | 9474 | 24 | |
(4 rows)
auxdb=# SELECT * FROM heap_page_items(get_raw_page('hot',1));
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
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
1 | 6160 | 1 | 2032 | 24505 | 24506 | 0 | (1,4) | 16386 | 9474 | 24 | |
2 | 4128 | 1 | 2032 | 24505 | 24506 | 0 | (2,1) | 2 | 9474 | 24 | |
3 | 2096 | 1 | 2032 | 24506 | 24507 | 0 | (2,2) | 2 | 8450 | 24 | |
4 | 64 | 1 | 2032 | 24506 | 24507 | 0 | (2,3) | 32770 | 8450 | 24 | |
(4 rows)
auxdb=# SELECT * FROM heap_page_items(get_raw_page('hot',2));
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
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
1 | 6160 | 1 | 2032 | 24506 | 24507 | 0 | (2,4) | 16386 | 8450 | 24 | |
2 | 4128 | 1 | 2032 | 24507 | 0 | 0 | (2,2) | 2 | 10242 | 24 | |
3 | 2096 | 1 | 2032 | 24507 | 0 | 0 | (2,3) | 2 | 10242 | 24 | |
4 | 64 | 1 | 2032 | 24507 | 0 | 0 | (2,4) | 32770 | 10242 | 24 | |
(4 rows)
vacuum hot;
vacuum hot;
vacuum hot;
auxdb=# vacuum full hot;
VACUUM
auxdb=# SELECT * FROM heap_page_items(get_raw_page('hot',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
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
1 | 6160 | 1 | 2032 | 24507 | 0 | 0 | (0,1) | 2 | 10498 | 24 | |
2 | 4128 | 1 | 2032 | 24506 | 24507 | 0 | (0,1) | 2 | 9474 | 24 | |
3 | 2096 | 1 | 2032 | 24505 | 24506 | 0 | (0,2) | 2 | 9474 | 24 | |
4 | 64 | 1 | 2032 | 2 | 24505 | 0 | (0,3) | 2 | 1282 | 24 | |
(4 rows)
auxdb=# SELECT * FROM heap_page_items(get_raw_page('hot',1));
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
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
1 | 6160 | 1 | 2032 | 24507 | 0 | 0 | (1,1) | 2 | 10498 | 24 | |
2 | 4128 | 1 | 2032 | 24506 | 24507 | 0 | (1,1) | 2 | 9474 | 24 | |
3 | 2096 | 1 | 2032 | 24505 | 24506 | 0 | (1,2) | 2 | 9474 | 24 | |
4 | 64 | 1 | 2032 | 2 | 24505 | 0 | (1,3) | 2 | 1282 | 24 | |
(4 rows)
auxdb=# SELECT * FROM heap_page_items(get_raw_page('hot',2));
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
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------
1 | 6160 | 1 | 2032 | 24507 | 0 | 0 | (2,1) | 2 | 10498 | 24 | |
2 | 4128 | 1 | 2032 | 24506 | 24507 | 0 | (2,1) | 2 | 9474 | 24 | |
3 | 2096 | 1 | 2032 | 24505 | 24506 | 0 | (2,2) | 2 | 9474 | 24 | |
4 | 64 | 1 | 2032 | 2 | 24505 | 0 | (2,3) | 2 | 1282 | 24 | |
(4 rows)
--同上
页修剪(Page Pruning)
定义与功能:
页修剪是指在读取或更新堆表页面时,自动删除在任何快照中都不再可见的元组(即xmax<snapshot.xmin)。
它主要发生在块内,通过删除无效或已过期的数据来释放空间,这些空间随后可以被用于更新操作。
定义与功能:
VACUUM是一种数据库维护操作,用于清理和优化数据库。
它通过标记和回收无效数据所占用的空间,以及更新相关的统计信息和索引,来提高数据库的查询性能和空间利用率。HOT技术
在传统的数据库系统中,更新操作通常会导致数据页膨胀和索引频繁更新,从而影响数据库的性能和空间利用效率。为了解决这个问题,PostgreSQL引入了HOT技术。
drop table hot;
create table hot (id int , s char(2000));
create index hot_id on hot(id);
insert into hot values (1,'A');
insert into hot values (2,'A1');
auxdb=# select * from bt_page_items('hot_id', 1);
itemoffset | ctid | itemlen | nulls | vars | data
------------+-------+---------+-------+------+-------------------------
1 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 00
2 | (0,4) | 16 | f | f | 02 00 00 00 00 00 00 00update hot set s = 'B';
-- update hot set s = 'C'; 跨页
update hot set id = 3 where id = 2; -- 索引列auxdb=# select * from bt_page_items('hot_id', 1);
itemoffset | ctid | itemlen | nulls | vars | data
------------+-------+---------+-------+------+-------------------------
1 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 00
2 | (0,2) | 16 | f | f | 01 00 00 00 00 00 00 00
3 | (0,3) | 16 | f | f | 02 00 00 00 00 00 00 00
(3 rows)
update hot set s = 'B' where id = 1;
HOT技术实现:当更新操作发生时,如果新行和旧行存储在相同的数据页中,HOT技术会通过CTID指针的方式来定位新元组。CTID指针是行指针的一部分,它标注了行的物理位置(offset,len)。在形成update chain时,CTID指向的是链表中下一个版本的位置。因此,当从索引访问到数据行时,会根据这个指针找到新行,而无需更新索引。
相同数据页内:HOT技术中的行之间的指针只能在同一个数据块内,不能跨数据块。因此,为了使用HOT技术,应该在数据块中留出较大的空闲空间,可以通过设置较小的fillfactor参数来实现。
索引列未更新:当更新的字段不是索引的键值时,HOT技术才能发挥作用。如果更新了索引列,则需要更新索引,此时HOT技术无法避免索引的更新。

我们知道在数据库行数据更新时,索引也需要进行维护,如果是高并发的情况下,索引维护的代价很大,可能造成索引分裂。Pg为了避免这个问题,采用了HOT(堆内元组技术)解决这个问题,下面我们就这个技术详细探讨一下。
我们先看看postgresql中page的结构:

pd_lsn:本页面最后一次变更所写入的xlog记录对应的lsn。
pd_checksum:页面校验和。
pd_lower:指向行指针的末尾(空闲空间开始位置)。
pd_upper:指向最新堆元组的起始位置(空闲空间结束位置)。
pd_special:用在索引页中,在索引页中它指向特殊空间的起始位置,在堆表页面中它指向页尾。
pd_pagesize_version:页面大小以及页面布局的版本号。
pd_prune_xid:本页面可以修剪的最老元组的xid。
drop table hot;
create table hot (id int , s char(2000));
create index hot_id on hot(id);
insert into hot values (1,'A');
update hot set s = 'B';
update hot set s = 'C';
update hot set s = 'D';
auxdb=# SELECT * FROM heap_page_items(get_raw_page('hot',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
----+--------+----------+--------+---------+---------+----------+--------+-------------+------------+--------+--------+-------
1 | 6160 | 1 | 2032 | 1049419 | 1049420 | 0 | (0,2) | 16386 | 1282 | 24 | |
2 | 4128 | 1 | 2032 | 1049420 | 1049421 | 0 | (0,3) | 49154 | 9474 | 24 | |
3 | 2096 | 1 | 2032 | 1049421 | 1049422 | 0 | (0,4) | 49154 | 8450 | 24 | |
4 | 64 | 1 | 2032 | 1049422 | 0 | 0 | (0,4) | 32770 | 10242 | 24 | |
(4 rows)
auxdb=# select pg_relation_filepath('hot');
pg_relation_filepath
----------------------
base/16384/24659
(1 row)
[sysomm@db1 ~]$ pagehack -f /mogdata/cluster_26000/base/16384/24659
page information of block 0/1
pd_lsn: 0/63C44D88
pd_checksum: 0xFBBA, verify success
pd_flags:
pd_lower: 56, non-empty
pd_upper: 64, old
pd_special: 8192, size 0
Page size & version: 8192, 6
pd_xid_base: 1049414, pd_multi_base: 0
pd_prune_xid: 1049420
Heap tuple information on this page
Tuple #1 is normal: length 2032, offset 6160
t_xmin/t_xmax/t_cid: 1049419/1049420/0
ctid:(block 0/0, offset 2)
t_infomask: HEAP_HASVARWIDTH HEAP_XMIN_COMMITTED HEAP_XMAX_COMMITTED HEAP_HAS_NO_UID
t_infomask2: HEAP_HOT_UPDATED Attrs Num: 2
t_hoff: 24
t_bits:
NNNNNNNN
Tuple #2 is normal: length 2032, offset 4128
t_xmin/t_xmax/t_cid: 1049420/1049421/0
ctid:(block 0/0, offset 3)
t_infomask: HEAP_HASVARWIDTH HEAP_XMIN_COMMITTED HEAP_XMAX_COMMITTED HEAP_UPDATED HEAP_HAS_NO_UID
t_infomask2: HEAP_HOT_UPDATED HEAP_ONLY_TUPLE Attrs Num: 2
t_hoff: 24
t_bits:
NNNNNNNN
Tuple #3 is normal: length 2032, offset 2096
t_xmin/t_xmax/t_cid: 1049421/1049422/0
ctid:(block 0/0, offset 4)
t_infomask: HEAP_HASVARWIDTH HEAP_XMIN_COMMITTED HEAP_UPDATED HEAP_HAS_NO_UID
t_infomask2: HEAP_HOT_UPDATED HEAP_ONLY_TUPLE Attrs Num: 2
t_hoff: 24
t_bits:
NNNNNNNN
Tuple #4 is normal: length 2032, offset 64
t_xmin/t_xmax/t_cid: 1049422/0/0
ctid:(block 0/0, offset 4)
t_infomask: HEAP_HASVARWIDTH HEAP_XMAX_INVALID HEAP_UPDATED HEAP_HAS_NO_UID
t_infomask2: HEAP_ONLY_TUPLE Attrs Num: 2
t_hoff: 24
t_bits:
NNNNNNNN
Summary (4 total): 0 unused, 4 normal, 0 dead
Normal Heap Page, special space is 0
Relation information : pageCount 1.
RP information : rpCount 4, rpMax 4, rpAvg 4.000000.
TD information : tdCount 0, tdMax 0, tdAvg 0.000000.
Freespace information : freeTotal 8, freeMax 8, freeAvg 8.000000.
页面基本信息部分
页面标识与完整性相关
“page information of block 0/1” 明确当前查看的是块编号为 0/1 的页面情况。“pd_lsn: 0/63C44D88” 展示了该页面的日志序列号,用于追踪页面相关事务操作的先后顺序,在数据库进行恢复等操作时会依据此信息。“pd_checksum: 0xFBBA, verify success” 表明页面的校验和值为 0xFBBA 且校验成功,意味着页面的数据完整性得以保证,未出现数据损坏等异常状况。
页面范围及标志等情况
“pd_flags:” 此处为空,即没有显示出特定的页面相关标志位信息,其具体含义取决于对应的数据库系统自身的定义与设定。
“pd_lower: 56, non-empty” 指出页面内已使用空间下限相关指标为 56 ,并且页面非空,说明已经有数据存储在该页面中了。“pd_upper: 64, old” 表示页面内数据存储范围的上限为 64 ,标记为 “old”,其确切含义通常和数据更新状态或者版本相关,要结合具体使用的数据库来深入理解。
“pd_special: 8192, size 0” 说明页面的特殊区域起始位置设定为 8192 ,但当前大小为 0 字节,特殊区域往往有着特殊用途,例如存储页面相关的管理数据等,不同数据库对其功能定义有所不同。
页面尺寸、版本及事务相关标识
“Page size & version: 8192, 6” 表明页面大小是 8192 字节,版本号为 6 ,这对于数据库在存储管理、数据格式兼容性等方面有着重要意义。
“pd_xid_base: 1049414, pd_multi_base: 0” 中,pd_xid_base 大概率与事务标识符(XID)的基准有关,在数据库处理事务依赖、并发控制等场景会用到该基准值;pd_multi_base 为 0 ,其具体用途取决于对应数据库的多事务处理相关设定,可能涉及多事务并发操作的基础标识等方面。
“pd_prune_xid: 1049420” 可能用于判断何时可以清理页面上与某些事务相关的历史数据,当达到这个事务标识符对应的条件时,数据库会进行相应的数据回收等空间管理操作。
页面内元组信息部分
各个元组详细情况
元组 1:
“Tuple #1 is normal: length 2032, offset 6160” 表示第一个元组状态正常,长度为 2032 字节,在页面内的偏移量是 6160 字节位置开始存储。“t_xmin/t_xmax/t_cid: 1049419/1049420/0” 给出了其涉及的事务相关信息,t_xmin 是创建该元组的事务标识符,t_xmax 是使该元组失效(如删除等操作对应的事务)的事务标识符,这里表明由事务 1049419 创建,在事务 1049420 有相关变动;“ctid:(block 0/0, offset 2)” 是元组的物理存储位置标识,在块 0/0 中偏移量为 2 的位置;后面的 “t_infomask” 和 “t_infomask2” 等标志位信息用于描述元组的更多特征,比如是否有可变宽度字段、事务是否已提交、是否有热点更新以及属性数量等情况。
元组 2:
类似元组 1 的分析逻辑,“Tuple #2 is normal: length 2032, offset 4128” 说明其长度和在页面内的偏移量情况,“t_xmin/t_xmax/t_cid: 1049420/1049421/0” 体现了创建与失效对应的事务标识符变化,在标志位等方面也有着相应的特征体现,与元组 1 有细微差异,如多了 “HEAP_UPDATED” 标志,意味着经历过更新操作。
元组 3:
“Tuple #3 is normal: length 2032, offset 2096” 等信息展示了其基本情况,“t_xmin/t_xmax/t_cid: 1049421/1049422/0” 体现其事务相关信息,同样其标志位等也反映了该元组的特点,例如有 “HEAP_UPDATED” 标志,表明经过了更新操作。
元组 4:
“Tuple #4 is normal: length 2032, offset 64” 给出基本属性,“t_xmin/t_xmax/t_cid: 1049422/0/0” 中,t_xmax 为 0 比较特殊,结合 “t_infomask: HEAP_HASVARWIDTH HEAP_XMAX_INVALID HEAP_UPDATED HEAP_HAS_NO_UID ” 来看,可能意味着该元组目前处于一种特殊状态,比如对应的失效事务尚未确定或者有其他特殊情况导致 XMAX 标记为无效,其标志位同样展示了元组的其他相关特性。
元组总结信息
“Summary (4 total): 0 unused, 4 normal, 0 dead” 对页面内这 4 个元组整体情况做了总结,即没有未使用的元组,4 个都是正常状态的元组,也不存在已失效(“dead”,比如被删除但还未彻底清理等情况对应的)元组。
页面类型相关说明
“Normal Heap Page, special space is 0” 再次强调该页面属于普通的堆页面类型,并且特殊空间大小为 0 ,与前面页面基本信息中关于特殊区域的描述相呼应,进一步明确其状态。
关系、RP、TD 及空闲空间信息部分
关系信息(Relation information)
“pageCount 1.” 表明与该页面相关的关系(这里的 “关系” 可能是指在数据库中关联到的表或者其他数据对象相关概念)所涉及的页面数量为 1 个,用于从整体关系层面描述数据的分布情况。
RP 信息(RP information)
“rpCount 4, rpMax 4, rpAvg 4.000000.” 中的 RP 具体含义需结合对应数据库定义来确定,大概率是某种资源相关的统计指标,这里表示其数量为 4 ,最大值为 4 ,平均值也是 4 ,这些统计数据有助于了解对应资源在该页面相关场景下的分布与使用情况。
TD 信息(TD information)
“tdCount 0, tdMax 0, tdAvg 0.000000.” 同样,TD 的具体含义取决于数据库内部设定,从数值来看当前相关的数量、最大值和平均值都为 0 ,反映了该指标对应的相关情况,比如可能是某种数据结构或者资源的计数情况等。
空闲空间信息(Freespace information)
“freeTotal 8, freeMax 8, freeAvg 8.000000.” 清晰地给出了空闲空间方面的统计数据,空闲空间的总量、最大值和平均值都为 8 ,单位可能与数据库的存储单位相关(比如字节等),这对于数据库后续进行空间分配、数据插入等操作时评估可用空间有着重要参考价值。从上面的结构我们可以看到,pd_lower和pd_upper分别指向空闲空间的起始和终止位置,而图中的1和2是行指针,分别指向真实的元组位置。
了解了page的结构后我们再来看看元组的结构:

t_mix:插入此元组的事务txid。
t_max:删除或更新此元组的事务txid,如果为删除或更新则为0。
t_cid:command id,在当前事务中,已经执行过多少条sql,例如执行第一条sql时cid=0,第二条cid=2。
t_ctid:保存着执行自身或者新元组的元组标识符(tid),在更新元组后tid指向新版本的元组,否则指向自己,这个我们后面会细细讨论。
介绍完上面的基本概念后我们再来看看postgresql如何通过b树索引找到对应的数据行的。

我们知道索引元组中是kv的结构,key代表的是查询条件的值,value即TID,TID中记录了两部分信息,block=2代表页面号,数据位于第几个块(页面),offset=2代表第二个元组,这样就通过索引直接定位了某一条记录,而不需要对页面进行扫描。
auxdb=# select * from bt_page_items('hot_id', 1);
itemoffset | ctid | itemlen | nulls | vars | data
------------+-------+---------+-------+------+-------------------------
1 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 00
(1 row)下面我们进入正题,我们再来看看元组是如何更新的,我们知道元组的更新其实是新插入一条记录如下图所示,如果没有hot技术的话,每更新一个行,就会插入一个元组,同时会在索引页中新增一一条元组,该元组中的tid指向新的元组,而索引的维护开销也是非常大的,可以想象,这样的话在频繁更新的系统中不仅数据会膨胀而且索引也会膨胀,同时维护索引的开销太大。

于是postgresql使用HOT(堆内元组技术)解决这个问题,总体思想是在更新时通过修改指针指向定位新元组,而不需要插入相应的索引元组。我们来看看hot更新的流程:
在元组结构的t_informask2字段中有两个标记位,heap_hot_update和heap_only_tuple,在更新tuple1时,postgresql会将tuple1(老元组)的标记位置为heap_hot_update,同时将tuple2(新元组)的标记位置为heap_only_tuple。


t_infomask2:HOT链更新状态和当tuple的属性个数。
因为会用到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 | 与可见性相关的位 |
因为会用到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 相对,表明是从别处移动过来的,也是为了兼容性而保留 |
1.首先找到目标数据的索引元组
2.然后通过索引元组中的位置,访问行指针数组,找到行指针1
3.读取tuple1
4.发现tuple1的标记位是heap_hot_update,于是通过tuple1的t_ctid字段读取tuple2(上面也提到过,当元组被更新过后,元组的t_ctid字段指向新的元组)
上面的过程其实访问了tuple1和tuple2两个数据块,这时我们可能会考虑到一个问题,如果tuple1因为vacuum清理掉了,就无法通过tuple的ctid字段定位到tuple2了,为了解决这个问题,postgresql会在合适的时候进行行指针的重定向(redirect),这个过程称为修剪。

此时访问新元组的流程如下:
1.首先找到目标数据的索引元组
2.然后通过索引元组中的位置,访问行指针数组,找到行指针1
3.通过行指针的重定向,找到行指针2
4.通过行指针2定位tuple2
在postgresql进行修剪时,会挑选合适的时机来清理死元组,这个过程称为碎片整理,如下图所示:

碎片整理并不会清理索引元组,所以碎片整理比普通vacuum清理的开销要小的多。HOT特性降低了表和索引的空间消耗,同时减少了vacuum需要处理的元组数量,对于性能有很好的提升。
当然HOT技术也不是万能的,它也有不适用的场景,比如下面两个场景:
1.当更新的元组和老元组不在同一个page中时,指向该元组的索引元组也会被添加到索引页面中。
2.当索引的key值更新时,会在索引页面中插入一条新的索引元组。
















