PostgreSQL中的page指的是数据文件内部被划分的一个个固定长度的页,和Oracle中的数据块类似。page默认大小是8k,可以在编译数据库时通过–with-blocksize参数指定。
文件中的page从0开始一个个进行编号,当一个8k的page写满时就会在该page尾部追加一个新的page。这也是为什么pg中单表只能时32T,因为pg默认采用32位寻址,也就是说单张表的数据文件最多有2^32=4294967296个page。
言归正传,接下来我们来一起揭开page的庐山真面目。
1、page整体结构
下图是page的整体结构图:
我们通过上图,可以把page大致划分为以下5个部分:
其中比较重要的有3部分:
- head info:文件头信息,大小位24B,记录了页面的一些元信息;
- line pointers:行指针,每个行指针大小4B,保存指向行数据的指针;
- heap tuples:堆元组的数据,就是page中记录存储的行数据的地方。
2、pageheader文件头结构
首先我们先来看看pageheader的结构,其定义在src/include/storage/bufpage.h
每部分的介绍大致如下:
其中pd_lsn表示的是最后修改过这个page的lsn(PostgreSQL LSN详解)。
pd_checksum表示的是校验和,默认是关闭的,可以在initdb时加上-k参数开启(PostgreSQL checksum与Data Corruption
)。
pd_flags为标志位,其取值可以有:
#define PD_HAS_FREE_LINES 0x0001 /* are there any unused line pointers? */
#define PD_PAGE_FULL 0x0002 /* not enough free space for new tuple? */
#define PD_ALL_VISIBLE 0x0004 /* all tuples on page are visible to * everyone */
#define PD_VALID_FLAG_BITS 0x0007 /* OR of all valid pd_flags bits */
pd_lower和pd_upper分别表示空闲空间起始位置和结束位置,这个后面我们会再详细介绍。
pd_special存放和索引方法相关的数据,普通的数据page不使用该参数,默认为pagesize大小。
pd_pagesize_version是版本的标志符,pg8.3之后都是为4。
pd_prune_xid表示这个page上最早删除或者修改tuple的事务id,在vacuum操作的时候会用到。
3、line pointers结构
行指针的结构如下:src/include/storage/itemid.h
lp_off:元组偏移量,例如我们普通表的page中special的位置是8192,如果插入一个长度为40B的元组,那么此时的偏移量就是8192-40 = 8152,这个我们后面会再介绍如何计算。
lp_flags:标志位,取值有下面几种:
#define LP_UNUSED 0 /* unused ( lp_len始终=0) */
#define LP_NORMAL 1 /* used (lp_len始终>0) */
#define LP_REDIRECT 2 /* HOT 重定向(lp_len必须=0) */
#define LP_DEAD 3 /* 死元组,不确定是否有存储 */
lp_len:元组的长度,单位为字节,后面我们会介绍如何计算。
4、HeapTupleHeaderData结构
接下来就是存放数据的heaptuple了,这里我们主要介绍下heaptuple的头部结构:
src/include/access/htup_details.h
typedef struct HeapTupleFields
{
TransactionId t_xmin; /* inserting xact ID */
TransactionId t_xmax; /* deleting or locking xact ID */
union
{
CommandId t_cid; /* inserting or deleting command ID, or both */
TransactionId t_xvac; /* old-style VACUUM FULL xact ID */
} t_field3;
} HeapTupleFields;
typedef struct DatumTupleFields
{
int32 datum_len_; /* varlena header (do not touch directly!) */
int32 datum_typmod; /* -1, or identifier of a record type */
Oid datum_typeid; /* composite type OID, or RECORDOID */
/*
* datum_typeid cannot be a domain over composite, only plain composite,
* even if the datum is meant as a value of a domain-over-composite type.
* This is in line with the general principle that CoerceToDomain does not
* change the physical representation of the base type value.
*
* Note: field ordering is chosen with thought that Oid might someday
* widen to 64 bits.
*/
} DatumTupleFields;
struct HeapTupleHeaderData
{
union
{
HeapTupleFields t_heap;
DatumTupleFields t_datum;
} t_choice;
ItemPointerData t_ctid; /* current TID of this or newer tuple (or a
* speculative insertion token) */
/* Fields below here must match MinimalTupleData! */
#define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK2 2
uint16 t_infomask2; /* number of attributes + various flags */
#define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK 3
uint16 t_infomask; /* various flag bits, see below */
#define FIELDNO_HEAPTUPLEHEADERDATA_HOFF 4
uint8 t_hoff; /* sizeof header incl. bitmap, padding */
/* ^ - 23 bytes - ^ */
#define FIELDNO_HEAPTUPLEHEADERDATA_BITS 5
bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs */
/* MORE DATA FOLLOWS AT END OF STRUCT */
};
其含义大致如下:
关于其中字段的具体介绍我这里就不再赘述了,不太明白的可以去看看我之前的文章和官方文档。
这里说明一下t_hoff,实际的用户数据(行的列)就是从t_hoff指示的偏移位置开始, 它必须总是该平台的 MAXALIGN 距离的倍数。可能这一段你看完不太明白是啥意思,这里我们暂且按下不表,后面会举例说明。
除此之外,还有一个用来存储null值列的t_bits。因为NULL值在数据库中属于一个特别的数据类型,其与空有着区别,因此在保存该NULL值的时候,为了能够节省存储空间我们并不是保存一个特殊的值(因为无论我们使用何种方式,即使最小的使用1 bit来表示,当数据量巨大时,也会造成存储空间的增长),所以在pg中通过t_bits来存放null值的列。
5、通过pageinspect探究page结构
下面我们使用pageinspect来进行探究,同时也会解答前面我们留下的几个问题。
首先我们看一下null值和t_bits
创建测试表,插入数据,可以看到没有NULL值时t_bits为空。
bill@bill=>create table t1(c1 int default null,c2 text default null,c3 text default null);
CREATE TABLE
bill@bill=>insert into t1 values(1,'bill','bill');
INSERT 0 1
bill@bill=>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 | 38 | 2878 | 0 | 0 | (0,1) | 3 | 2050 | 24 | | | \x010000000b62696c6c0b62696
c6c
(1 row)
插入NULL值,t_bits变成了10000000
bill@bill=>insert into t1(c1) values(1);
INSERT 0 1
bill@bill=>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 | 38 | 2878 | 0 | 0 | (0,1) | 3 | 2050 | 24 | | | \x010000000b62696c6c0b626
96c6c
2 | 8120 | 1 | 28 | 2879 | 0 | 0 | (0,2) | 3 | 2049 | 24 | 10000000 | | \x01000000
(2 rows)
接着我们再验证下前面说的pd_lower和pd_upper,并且说明下如何计算元组的长度。
bill@bill=>create table t1(id int,info text);
CREATE TABLE
bill@bill=>insert into t1 values(1,'bill');
INSERT 0 1
bill@bill=>insert into t1 values(2,'bill');
INSERT 0 1
查看page头:
bill@bill=>select * from page_header(get_raw_page('t1',0));
lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid
------------+----------+-------+-------+-------+---------+----------+---------+-----------
1/1364D298 | 0 | 0 | 32 | 8112 | 8192 | 8192 | 4 | 0
(1 row)
可以看到lower变成了32,为什么呢?
我们前面说过page头是24个字节,接着就是行指针,每个行指针4个字节。而lower指向的就是行指针尾部的地方,所以是24+4+4=32。
接着查看heapitem结构:
bill@bill=>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 | 33 | 2882 | 0 | 0 | (0,1) | 2 | 2050 | 24 | | | \x010000000b62696c6c
2 | 8112 | 1 | 33 | 2883 | 0 | 0 | (0,2) | 2 | 2050 | 24 | | | \x020000000b62696c6c
(2 rows)
接下我们来看lp_off,这个值和pd_upper指向的地方是一样的。前面我们说过lp_off表示的是行的偏移,普通表的page中就等于special空间的位置(即8192)减去行的长度,那么为什么这里lp_len是33呢?8152+33也不等于8192呀,这是怎么回事?
我们下面看下pg中是如何计算行的长度的:
/*
* Check for nulls
*/
for (i = 0; i < numberOfAttributes; i++)
{
if (isnull[i])
{
hasnull = true;
break;
}
}
/*
* Determine total space needed
*/
len = SizeofMinimalTupleHeader;
if (hasnull)
len += BITMAPLEN(numberOfAttributes);
hoff = len = MAXALIGN(len); /* align user data safely */
data_len = heap_compute_data_size(tupleDescriptor, values, isnull);
len += data_len;
首先会判断t_bits,检查是否包含NULL值,计算相应的数据长度。
接下来就是我们前面问题的关键地方了:
hoff = len = MAXALIGN(len);
#define MAXALIGN(LEN) TYPEALIGN(MAXIMUM_ALIGNOF, (LEN))
#define MAXIMUM_ALIGNOF 8
什么意思呢?这里因为字节对齐的原因,所以长度要保持为MAXALIGN的倍数。
这里简单说明下什么是字节对齐,我们看下下面这个例子:
struct STUDENT
{
char a;
char b;
int c;
}data;
char 占 1 字节,int 占 4 字节,那么上面这个结构体多少个字节呢?1+1+4=6吗?
事实上并不是这样,其内部会进行字节对齐,大致如下所示,所以是8个字节。
所以我们上面计算长度时会进行字节对齐,结果要保证是8的整数倍,那么我们再回头看前面的例子:
bill@bill=>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 | 33 | 2882 | 0 | 0 | (0,1) | 2 | 2050 | 24 | | | \x010000000b62696c6c
2 | 8112 | 1 | 33 | 2883 | 0 | 0 | (0,2) | 2 | 2050 | 24 | | | \x020000000b62696c6c
(2 rows)
这里看到的lp_len是33,由于字节对齐的原因,其实际长度是8*5=40字节,40+8152刚好等于8192。
参考链接:
http://c.biancheng.net/view/243.html
https://www.interdb.jp/pg/pgsql01.html#_1.3.
http://www.postgres.cn/docs/13/storage-page-layout.html