前言
对于从事PostgreSQL相关的,不管你是开发还是运维,肯定都或多或少听到过标志位这个东东,云里雾里,大概了解这个标志位可以加速事务的获取、标识某些状态信息等等,今天让我们一起弥补这一块的知识空缺。
何为infomask
首先我们需要简单了解一下PostgreSQL的内部结构,数据块的结构如下 ?,默认8KB,最大32KB,在此不过多介绍,度娘一堆。

下面是Tuple的具体结构,Tuple头部是由23byte固定大小的前缀和可选的NullBitMap构成,当元组中存在空值时,会出现空值位图,每个字段占一位,远远大于Oracle的3字节,也比Mysql略大。所以在PostgreSQL里面,存储的额外开销要略大于Oracle,另外值得注意的是可能还会有字节对齐,在此表过不提。

我们关心的是infomask,more tuple flags,可以看到,是关于元组Tuple的一些标志位,用于标识元组的一些属性和状态。在源码里面,infomask有如下这么多状态位,可以看到,就是通过比特位来标识某些属性,比如是否具有空属性、是否具有变长的属性

这里分享一个实用函数,可以直接获取infomask和infomask2里面的标志位信息
create type infomask_bit_desc as (mask varbit, symbol text);
create or replace function infomask(msk int, which int) returns text
language plpgsql as $$
declare
r infomask_bit_desc;
str text = '';
append_bar bool = false;
begin
for r in select * from infomask_bits(which) loop
if (msk::bit(16) & r.mask)::int <> 0 then
if append_bar then
str = str || '|';
end if;
append_bar = true;
str = str || r.symbol;
end if;
end loop;
return str;
end;
$$ ;
create or replace function infomask_bits(which int)
returns setof infomask_bit_desc
language plpgsql as $$
begin
if which = 1 then
return query values
(x'8000'::varbit, 'MOVED_IN'),
(x'4000', 'MOVED_OFF'),
(x'2000', 'UPDATED'),
(x'1000', 'XMAX_IS_MULTI'),
(x'0800', 'XMAX_INVALID'),
(x'0400', 'XMAX_COMMITTED'),
(x'0200', 'XMIN_INVALID'),
(x'0100', 'XMIN_COMMITTED'),
(x'0080', 'XMAX_LOCK_ONLY'),
(x'0040', 'EXCL_LOCK'),
(x'0020', 'COMBOCID'),
(x'0010', 'XMAX_KEYSHR_LOCK'),
(x'0008', 'HASOID'),
(x'0004', 'HASEXTERNAL'),
(x'0002', 'HASVARWIDTH'),
(x'0001', 'HASNULL');
elsif which = 2 then
return query values
(x'2000'::varbit, 'UPDATE_KEY_REVOKED'),
(x'4000', 'HOT_UPDATED'),
(x'8000', 'HEAP_ONLY_TUPLE');
end if;
end;
$$;
下面让我们一个个来分析这些标志位!Are you ready?
HEAP_HASNULL & HASVARWIDTH
这个标志位很明显,就是用于判断是否具有空列,见如下样例,可以看到第一行因为是 text 变长列,所以置了HASVARWIDTH的标志位,第二行和第三行插入了null,所以置了HASNULL的标志位。
#define HEAP_HASNULL 0x0001 /* has null attribute(s) */
#define HEAP_HASVARWIDTH 0x0002 /* has variable-width attribute(s) */
postgres=# create table nullt1(id int,info text);
CREATE TABLE
postgres=# insert into nullt1 values(1,'xiongcc');
INSERT 0 1
postgres=# insert into nullt1 values(1,null);
INSERT 0 1
postgres=# insert into nullt1 values(null,null);
INSERT 0 1
postgres=# select lp, t_xmin, t_xmax, t_ctid,
infomask(t_infomask, 1) as infomask,
infomask(t_infomask2, 2) as infomask2
from heap_page_items(get_raw_page('nullt1', 0));
lp | t_xmin | t_xmax | t_ctid | infomask | infomask2
----+---------+--------+--------+--------------------------+-----------
1 | 3643895 | 0 | (0,1) | XMAX_INVALID|HASVARWIDTH |
2 | 3643896 | 0 | (0,2) | XMAX_INVALID|HASNULL |
3 | 3643897 | 0 | (0,3) | XMAX_INVALID|HASNULL |
(3 rows
HEAP_HASEXTERNAL
用于标识元组是否包含外部存储的字段
#define HEAP_HASEXTERNAL 0x0004 /* has external stored attribute(s) */
postgres=# create table blog(id int, title text, content text);
CREATE TABLE
postgres=# \d+ blog
Table "public.blog"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
---------+---------+-----------+----------+---------+----------+--------------+-------------
id | integer | | | | plain | |
title | text | | | | extended | |
content | text | | | | extended | |
Access method: heap
可以看到int类型默认策略为plain,而text为extended ,目前PostgreSQL支持的存储方式共有4种:
PLAIN :避免压缩和行外存储。
EXTENDED :先压缩,后行外存储。
EXTERNAL :允许行外存储,但不许压缩。
MAIN :允许压缩,尽量不使用行外存储。
那么,何时压缩数据?何时行外存储呢?
Tuple压缩:当Tuple大小超过大概2KB时,大概1/4个Block大小时,PostgreSQL会尝试基于LZ压缩算法进行压缩,另外在v14里面直接对表支持了LZ4压缩算法
postgres=# CREATE TABLE tab_compression (
a text COMPRESSION pglz,
b text COMPRESSION lz4);
CREATE TABLE
postgres=# \d+ tab_compression
Table "public.tab_compression"
Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description
--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
a | text | | | | extended | pglz | |
b | text | | | | extended | lz4 | |
Access method: heap
行外存储 (TOAST):Toast属性的本意是The Oversized-Attribute Storage Technique,主要用来应对物理数据行超过数据块大小的场景,对于某个超长的属性单独存储,因为PostgreSQL不允许Tuple跨页存储,因此页的大小就是行大小的硬上限,当某行数据超过PostgreSQL页大小 (8K) 后,会将这个页放到系统命名空间pg_toast下的一个单独的表中,而在原表中存储一个TOAST pointer
typedef struct
{
uint8 va_header; /* Always 0x80 or 0x01 */
uint8 va_tag; /* Type of datum */
char va_data[FLEXIBLE_ARRAY_MEMBER]; /* Type-specific data */
} varattrib_1b_e;
typedef struct varatt_external
{
int32 va_rawsize; /* Original data size (includes header) */
int32 va_extsize; /* External saved size (doesn't) */
Oid va_valueid; /* Unique ID of value within TOAST table */
Oid va_toastrelid; /* RelID of TOAST table containing it */
} varatt_external;
看一下blog表的Toast存储在哪里
postgres=# select relname,relfilenode,reltoastrelid from pg_class where relname='blog';
relname | relfilenode | reltoastrelid
---------+-------------+---------------
blog | 49444 | 49447
(1 row)
postgres=# \d pg_toast.pg_toast_49444
TOAST table "pg_toast.pg_toast_49444"
Column | Type
------------+---------
chunk_id | oid
chunk_seq | integer
chunk_data | bytea
Owning table: "public.blog"
Indexes:
"pg_toast_49444_index" PRIMARY KEY, btree (chunk_id, chunk_seq)
chunk_id :用来表示特定TOAST值的OID,可以理解为具有同样 chunk_id 值的所有行组成原表 (也就是此处的blog表) 的TOAST字段的一行数据
chunk_seq :用来表示该行数据在整个数据中的位置
chunk_data :实际存储的数据。
postgres=# insert into blog values(1, 'title', '0123456789');
INSERT 0 1
postgres=# select * from blog;
id | title | content
----+-------+------------
1 | title | 0123456789
(1 row)
postgres=# select * from pg_toast.pg_toast_49444;
chunk_id | chunk_seq | chunk_data
----------+-----------+------------
(0 rows)
postgres=# update blog set content=content||content where id=1;
UPDATE 1
postgres=# select id,title,length(content) from blog;
id | title | length
----+-------+--------
1 | title | 20
(1 row)
postgres=# select * from pg_toast.pg_toast_49444;
chunk_id | chunk_seq | chunk_data
----------+-----------+------------
(0 rows)
postgres=# select id,title,length(content) from blog;
id | title | length
----+-------+--------
1 | title | 40
(1 row)
---反复执行,不断扩大
postgres=# select id,title,length(content) from blog;
id | title | length
----+-------+----------
1 | title | 41943040
(1 row)
---可以看到,当content 的长度为41943040时,pg_toast里面有了数据,并且长度都略小于2K,说明在 extended策略下,先启用了压缩,然后才使用行外存储。
postgres=# select chunk_id,chunk_seq,length(chunk_data) from pg_toast.pg_toast_49444;
chunk_id | chunk_seq | length
----------+-----------+--------
49458 | 0 | 1996
49458 | 1 | 1996
49458 | 2 | 1996
49458 | 3 | 1996
49458 | 4 | 1996
49458 | 5 | 1996
49458 | 6 | 1996
49458 | 7 | 1996
49458 | 8 | 1996
49458 | 9 | 1996
49458 | 10 | 1996
49458 | 11 | 1996
49458 | 12 | 1996
49458 | 13 | 1996
49458 | 14 | 1996
49458 | 15 | 1996
49458 | 16 | 1996
49458 | 17 | 1996
49458 | 18 | 1996
49458 | 19 | 1996
...
我们再来看看标志位,这次就很清楚的看到了HASEXTERNAL。
postgres=# select lp, t_xmin, t_xmax, t_ctid,
infomask(t_infomask, 1) as infomask,
infomask(t_infomask2, 2) as infomask2
from heap_page_items(get_raw_page('blog', 0)) limit 2;
lp | t_xmin | t_xmax | t_ctid | infomask | infomask2
----+---------+---------+--------+---------------------------------------------------------------+-----------------------------
1 | | | | |
2 | 3643921 | 3643922 | (0,3) | UPDATED|XMAX_COMMITTED|XMIN_COMMITTED|HASEXTERNAL|HASVARWIDTH | HOT_UPDATED|HEAP_ONLY_TUPLE
(2 rows)
HEAP_HASOID_OLD
这个就很好理解了,是否具有oid,PostgreSQL的系统表中大多包含一个叫做OID的隐藏字段,这个OID也是这些系统表的主键。所谓OID,中文全称就是"对象标识符",oid的分配来自一个实例的全局变量,每分配一个新的对象,对这个全局变量加一。当分配的oid超过4字节整形最大值的时候会重新从0开始分配,但这并不会导致类似于事务ID回卷那样严重的影响,值得注意的是,从v12开始default_with_oids参数就没了,The parameter default_with_oids is gone, it had been disabled by default since after PostgreSQL 8.0,并且the default_with_oids parameter cannot be changed to 'on',可能也是为了性能吧,毕竟每次都去检验重试一遍还是十分耗时的。
#define HEAP_HASOID_OLD0x0008/* has an object-id field */
postgres=# show default_with_oids ;
default_with_oids
-------------------
off
(1 row)
postgres=# select version();
version
---------------------------------------------------------------------------------------------------------
PostgreSQL 11.9 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-44), 64-bit
(1 row)
postgres=# create table tb3(id int) with oids;
CREATE TABLE
postgres=# insert into tb3 values(1);
INSERT 16440 1
postgres=# select lp, t_xmin, t_xmax, t_ctid,
postgres-# infomask(t_infomask, 1) as infomask,
postgres-# infomask(t_infomask2, 2) as infomask2
postgres-# from heap_page_items(get_raw_page('tb3', 0));
lp | t_xmin | t_xmax | t_ctid | infomask | infomask2
----+--------+--------+--------+---------------------+-----------
1 | 574 | 0 | (0,1) | XMAX_INVALID|HASOID |
(1 row)
HEAP_XMAX_KEYSHR_LOCK & HEAP_XMAX_SHR_LOCK
这仨好基友就是行锁的体现了,在PostgreSQL里面,行锁是存储在磁盘上的,表锁我们可以很方便的通过pg_locks查看,行锁就只能pgrowlocks和pageinspect了。
#define HEAP_XMAX_KEYSHR_LOCK0x0010/* xmax is a key-shared locker */
#define HEAP_XMAX_EXCL_LOCK0x0040/* xmax is exclusive locker */
#define HEAP_XMAX_LOCK_ONLY0x0080/* xmax, if valid, is only a locker */
postgres=# create table mytest2(id int);
CREATE TABLE
postgres=# insert into mytest2 values(1);
INSERT 0 1
postgres=# insert into mytest2 values(2);
INSERT 0 1
postgres=# begin;
BEGIN
postgres=*# select * from mytest2 where id = 1 for key share;
id
----
1
(1 row)
postgres=*# select * from mytest2 where id = 2 for update;
id
----
2
(1 row)
再开一个会话查看
postgres=# select lp, t_xmin, t_xmax, t_ctid,
infomask(t_infomask, 1) as infomask,
infomask(t_infomask2, 2) as infomask2
from heap_page_items(get_raw_page('mytest1', 0));
ERROR: relation "mytest1" does not exist
postgres=# select lp, t_xmin, t_xmax, t_ctid,
infomask(t_infomask, 1) as infomask,
infomask(t_infomask2, 2) as infomask2
from heap_page_items(get_raw_page('mytest2', 0));
lp | t_xmin | t_xmax | t_ctid | infomask | infomask2
----+---------+---------+--------+------------------------------------------------+--------------------
1 | 3643931 | 3643934 | (0,1) | XMIN_COMMITTED|XMAX_LOCK_ONLY|XMAX_KEYSHR_LOCK |
2 | 3643932 | 3643934 | (0,2) | XMIN_COMMITTED|XMAX_LOCK_ONLY|EXCL_LOCK | UPDATE_KEY_REVOKED
(2 rows)
HEAP_COMBOCID
元组的t_cid是组合command id,这个标记,常用来标记本事务内的老记录,在介绍combo cid前,需要先了解下cmin和cmax。
cmin和cmax位于数据页中每个tuple的头部,用于判断tupel在一个事务内可见性的判断,cmin是产生该条tuple的command id,cmax是删除该tuple的command id。在一个事务内,command id是从0开始递增。一般来说,在PostgreSQL中,判断一条tuple的可见性的时候,通常使用事务当前的snapshot(xmin, xmax, xiplist),其中xmin是当前活跃的最小事务id,比这个xmin还是小的事务id要么commit要么rollack。xmax是本事务取snapshot时,还没有分配的最小事务id,也就是说,大于等于xmax的事务id所做的修改,对于当前事务来说都不可见。xip_list是当前活跃事务id列表,当前事务看不到该list中的事务所做修改。但是在同一个事务内的时候,并读取时,就会有点问题了:
postgres=# create table test1 (c1 int, c2 int);
CREATE TABLE
postgres=# begin;
BEGIN
postgres=*# insert into test1 values (1,2);
INSERT 0 1
postgres=*# select * from test1;
c1 | c2
----+----
1 | 2
(1 row)
postgres=*# commit;
COMMIT
如果仅仅使用xmin,xmax就无法判断tuple的可见性,因为插入的事务跟查询的事务在同一个事务中。所以,此时使用query的command id去比较tuple上的cmin和cmax。上面的例子中,query的cid为1,tuple(1,2)上cmin为0,cmax为invalid。因此,cid>cmin,同时cmax为invalid,所以query对该tuple可见。
因此,为了对一个tuple进行事务内部可见性的判读,需要在每个tuple的头部存储两个uint32类型的字段,cmin和cmax。但是,一个tuple在一个事务内被插入然后马上被更新或删除的场景一般比较少,所以一般MVCC判断的时候,比较少走到使用cmin和cmax的逻辑。
因而,为了减少cmin和cmax对heap page空间的占用,在PG8.3后,将cmin和cmax合并成一个字段,使用1个unit32类型,即combo cid。那么combo cid是如何实现,仅依靠一个字段的存储,在需要cmin的时候提供cmin,而在需要cmax的时候提供cmax呢?对于这个问题,相信通过了解combo cid的逻辑,就可引刃而解。
combo cid逻辑的介绍,这里通过下面几个问题的回答来介绍:
何时会产生combo cid?
在一个事务,当新插入一个tuple的时候,实际是不需要使用combo cid的,因为此时只有cmin有效。而只有在一个tuple被update或者delete时,cmax才会产生,此时就需要使用cmax。所以,在刚插入一个tuple的时候,cmin/cmax这个字段就是指示的cmin,只有,在update或者delete的时候,这个域才会是combo cid。
postgres=# begin;
BEGIN
postgres=*# insert into test1 values (1,2);
INSERT 0 1
postgres=*# update test1 set c2 = 99 ;
UPDATE 2
postgres=*# select lp, t_xmin, t_xmax, t_ctid,
infomask(t_infomask, 1) as infomask,
infomask(t_infomask2, 2) as infomask2
from heap_page_items(get_raw_page('test1', 0));
lp | t_xmin | t_xmax | t_ctid | infomask | infomask2
----+---------+---------+--------+----------------------+-----------------
1 | 3643936 | 3643937 | (0,3) | XMIN_COMMITTED | HOT_UPDATED
2 | 3643937 | 3643937 | (0,4) | COMBOCID | HOT_UPDATED
3 | 3643937 | 0 | (0,3) | UPDATED|XMAX_INVALID | HEAP_ONLY_TUPLE
4 | 3643937 | 0 | (0,4) | UPDATED|XMAX_INVALID | HEAP_ONLY_TUPLE
(4 rows)
HEAP_XMIN_COMMITTED
#define HEAP_XMIN_COMMITTED0x0100 /* t_xmin committed */
#define HEAP_XMIN_INVALID0x0200 /* t_xmin invalid/aborted */
#define HEAP_XMIN_FROZEN(HEAP_XMIN_COMMITTED|HEAP_XMIN_INVALID)
#define HEAP_XMAX_COMMITTED0x0400 /* t_xmax committed */
#define HEAP_XMAX_INVALID0x0800 /* t_xmax invalid/aborted */
这几个好基友就是用来加速事务获取和冻结相关的,冻结可以参照我之前的文章,在此就不再演示了。当查询一条数据的时候,需要去判断行的可见性,需要去查询相应事务的提交状态,我们只能从CLOG中或者PGXACT内存结构中(未结束的或未清除的事务信息内存)得知该tuple对应的事务提交状态,显然如果每条tuple都要查询pg_clog的话,性能一定会很差,当然还要根据隔离级别、事务快照来综合判断行的可见性,在此不再赘述,在PostgreSQL中提供了TransactionIdIsInProgress、TransactionIdDidCommit和TransactionIdDidAbort用于获取事务的状态,这些函数被设计为尽可能减少对CLOG的频繁访问 (假如把freeze相关参数设置为20亿的话,那么clog最多可能达到500多MB,每一个事务占2bit) 。尽管如此,如果在检查每条元组时都执行这些函数,也可能会成为瓶颈。所以,为了解决这个问题,PostgreSQL在t_infomask中使用了相关标志位,如下:
#define HEAP_XMIN_COMMITTED0x0100 /* t_xmin committed */
#define HEAP_XMIN_INVALID0x0200 /* t_xmin invalid/aborted */
#define HEAP_XMAX_COMMITTED0x0400 /* t_xmax committed */
#define HEAP_XMAX_INVALID 0x0800 /* t_xmax invalid/aborted */
在读取或写入元组时,PostgreSQL会择机将提示为设置到t_infomask中,比如PostgreSQL检查了元组的t_xmin对应事务的状态,结果为commited,那么就会在元组的t_infomask中置位一个HEAP_XMIN_COMMITTED,表示这条元组已经提交了,如果设置了标志位,那么就不再需要去调用TransactionIdDidCommit和TransactionIdDidAbort去获取事务的状态,可以高效地检查每个元组xmin和xmax对应的事务状态。所以,和Oracle一样,一些select操作也会产生写IO,原因就是设置标志位。
postgres=# create table test2(id int);
CREATE TABLE
postgres=# insert into test2 values(1);
INSERT 0 1
postgres=# select t_xmin,t_xmax,t_infomask,t_infomask2 from heap_page_items(get_raw_page('test2', 0));
t_xmin | t_xmax | t_infomask | t_infomask2
---------+--------+------------+-------------
3643942 | 0 | 2048 | 1
(1 row)
postgres=# select lp, t_xmin, t_xmax, t_ctid,
infomask(t_infomask, 1) as infomask,
infomask(t_infomask2, 2) as infomask2
from heap_page_items(get_raw_page('test2', 0));
lp | t_xmin | t_xmax | t_ctid | infomask | infomask2
----+---------+--------+--------+--------------+-----------
1 | 3643942 | 0 | (0,1) | XMAX_INVALID |
(1 row)
---注意,此处仅有XMAX_INVALID标志位
postgres=# select * from test2;
id
----
1
(1 row)
postgres=# select lp, t_xmin, t_xmax, t_ctid,
infomask(t_infomask, 1) as infomask,
infomask(t_infomask2, 2) as infomask2
from heap_page_items(get_raw_page('test2', 0));
lp | t_xmin | t_xmax | t_ctid | infomask | infomask2
----+---------+--------+--------+-----------------------------+-----------
1 | 3643942 | 0 | (0,1) | XMAX_INVALID|XMIN_COMMITTED |
(1 row)
---添加了XMIN_COMMITTED的标志位
但是需要注意的是,并不是在事务结束时设置t_infomask的标志位,而是等到后面的DML或者DQL,VACUUM等SQL扫描到对应的TUPLE时,触发置位的操作,是不是大有前人拉屎后人擦屁股的赶脚??
HEAP_XMAX_IS_MULTI
也就是multixact,因为对于FOR SHARE和FOR KEY SHARE这一类的行级锁,一行上面可能会被多个事务加锁,Tuple上动态维护这些事务代价很高,为此引入了multixact机制,将多个事务记录到MultiXactId,再将MultiXactId记录到tuple的xmax中。

postgres=# begin;
BEGIN
postgres=*# select txid_current();
txid_current
--------------
3643949
(1 row)
postgres=*# select * from test3 where id = 1 for key share;
id
----
1
(1 row)
查看行锁
postgres=# select * from test3 as t,pgrowlocks('test3') as lc where t.ctid = lc.locked_row;
id | locked_row | locker | multi | xids | modes | pids
----+------------+---------+-------+-----------+-------------------+---------
1 | (0,1) | 3643949 | f | {3643949} | {"For Key Share"} | {18215}
(1 row)
再开一个事务,加上行锁
postgres=# begin;
BEGIN
postgres=*# select txid_current();
txid_current
--------------
3643951
(1 row)
postgres=*# select * from test3 where id = 1 for share;
id
----
1
(1 row)
再次查看行锁,可以看到multi字段为true了
postgres=# select * from test3 as t,pgrowlocks('test3') as lc where t.ctid = lc.locked_row;
id | locked_row | locker | multi | xids | modes | pids
----+------------+--------+-------+-------------------+---------------------+---------------
1 | (0,1) | 1 | t | {3643949,3643951} | {"Key Share",Share} | {18215,18267}
(1 row)
postgres=# select lp, t_xmin, t_xmax, t_ctid,
infomask(t_infomask, 1) as infomask,
infomask(t_infomask2, 2) as infomask2
from heap_page_items(get_raw_page('test3', 0));
lp | t_xmin | t_xmax | t_ctid | infomask | infomask2
----+---------+--------+--------+------------------------------------------------------------------------+-----------
1 | 3643944 | 1 | (0,1) | XMAX_IS_MULTI|XMIN_COMMITTED|XMAX_LOCK_ONLY|EXCL_LOCK|XMAX_KEYSHR_LOCK |
2 | 3643945 | 0 | (0,2) | XMAX_INVALID|XMIN_COMMITTED |
3 | 3643946 | 0 | (0,3) | XMAX_INVALID|XMIN_COMMITTED |
(3 rows)
可以通过pg_get_multixact_members获取multixact
postgres=# select * from pg_get_multixact_members('1');
xid | mode
---------+-------
3643949 | keysh
3643951 | sh
(2 rows)
其他
其他几个infomask标志位就不再赘述了,下面这俩标志位保留用于pg_upgrate使用
#define HEAP_MOVED_OFF0x4000/* moved to another place by pre-9.0
* VACUUM FULL; kept for binary
* upgrade support */
#define HEAP_MOVED_IN0x8000/* moved from another place by pre-9.0
* VACUUM FULL; kept for binary
* upgrade support */
#define HEAP_MOVED (HEAP_MOVED_OFF | HEAP_MOVED_IN)
update产生的元组
#define HEAP_UPDATED0x2000/* this is UPDATEd version of row */
这个和可见性有关了,不过愚笨的我暂未复现什么条件会触发这个标志位的设置,再琢磨琢磨。
#define HEAP_XACT_MASK0xFFF0/* visibility-related bits */
小结
另外我们需要注意的是,标志位还和wal_log_hints有关
假设未开启checksum,开启了wal_log_hints,第一次使页面变脏的操作是修改hint bints,会记录整页到wal中
假设开启了checksum,不管wal_log_hints如何,checkpoint后第一次修改的页面都会记录整页到wal中。即使是hint bits
假设未开启wal_log_hints,第一次使页面变脏的操作是修改hint bints,不会记录full page image
可见,在启用 checksum 的情况下,checkpoint 后页面的第一次修改如果是更新 Hint Bits, 会写 Full Page Image 至 WAL 日志,这会导致 WAL 日志占用更多的存储空间。
同时,还有一种可能性,因为SetHintBits是针对单条tuple的,所以当有并行的会话在对一个数据页的多个tuple进行SetHintBits操作时,可能导致这个PAGE在多次checkpoint时被写多次到WAL。或者在2个checkpoint之间,多次被bgwriter刷到OS dirty page,可能造成多次OS IO。
以上种种演示了hint bits的作用,可以看到好处很多,并且可以用这个去面试小白:select是否会产生写IO?(坏笑ing...)
参考
https://cloud.tencent.com/developer/article/1004455
https://github.com/digoal/blog/blob/master/201610/20161002_03.md
https://www.commandprompt.com/blog/decoding_infomasks/
https://webcms3.cse.unsw.edu.au/COMP9315/20T1/resources/40359
https://zhuanlan.zhihu.com/p/67725967
进群唠嗑~