这一次终于把PostgreSQL Page结构搞懂了

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值