在数据文件(堆表、索引、自由空间映射和可见性映射)内部,它被划分为固定长度的page(或block),默认为 8192 字节(8 KB)。每个文件中的页面从 0 开始顺序编号,这些编号称为块编号。如果文件已满,PostgreSQL 会在文件末尾添加一个新的空页,以增加文件大小。
page包含三个重要数据
typedef struct PageHeaderData
{
/* XXX LSN是*任何*块的成员,不仅限于页面组织的块 */
PageXLogRecPtr pd_lsn; /* LSN:指向此页面上次更改的xlog记录的下一个字节 */
uint16 pd_checksum; /* 校验和 */
uint16 pd_flags; /* 标志位,见下文 */
LocationIndex pd_lower; /* 空闲空间开始的偏移量 */
LocationIndex pd_upper; /* 空闲空间结束的偏移量 */
LocationIndex pd_special; /* 特殊空间开始的偏移量 */
uint16 pd_pagesize_version;
TransactionId pd_prune_xid; /* 最旧可修剪的XID,如果没有则为零 */
ItemIdData pd_linp[FLEXIBLE_ARRAY_MEMBER]; /* 行指针数组 */
} PageHeaderData;
行指针末尾和最新元组开头之间的空位称为Free space。
为了识别表内的元组,内部使用了元组标识符(TID)。TID 由一对值组成:包含元组的页面块编号和指向元组的行指针偏移编号。
假设表格由一页组成,其中只包含一个heap tuple。该页的 pd_lower 指向第一个行指针,行指针和 pd_upper 都指向第一个堆元组。
插入第二个元组时,它被放在第一个元组之后。第二个行指针被附加到第一个行指针上,并指向第二个元组。pd_lower 变为指向第二个行指针,pd_upper 变为指向第二个堆元组。
该页面中的其他头数据(如 pd_lsn、pg_checksum、pg_flag)也会更新为适当的值;
例如,在图 1.6(b)中,获得的索引元组的 TID 值为’(block = 7, Offset = 2)',这意味着目标堆元组是表中第 7 页的第 2 个元组,因此 PostgreSQL 可以读取所需的堆元组,而无需在页面中进行不必要的扫描。
PostgreSQL的page会出现point和tuple空隙,这是因为page中的行数据是动态变化的,可能会发生插入、删除、更新等操作,导致page中的空闲空间分散在不同的位置,形成空隙。这些空隙会影响page的利用率和性能,因为它们会占用额外的空间和IO,而且会增加扫描和查找的时间。
PostgreSQL有一些方法来解决point和tuple空隙的问题,主要有以下几种:
堆元组由三部分组成:HeapTupleHeaderData 结构、NULL 位图和用户数据
typedef struct HeapTupleFields
{
TransactionId t_xmin; /* 插入事务的ID */
TransactionId t_xmax; /* 删除或锁定事务的ID */
union
{
CommandId t_cid; /* 插入或删除命令的ID,可能同时存在 */
TransactionId t_xvac; /* 旧式VACUUM FULL事务的ID */
} t_field3;
} HeapTupleFields;
typedef struct DatumTupleFields
{
int32 datum_len_; /* varlena头部(不要直接操作!) */
int32 datum_typmod; /* -1,或记录类型的标识符 */
Oid datum_typeid; /* 复合类型的OID,或RECORDOID */
/*
* datum_typeid不能是复合类型上的域,只能是普通复合类型,即使datum被视为是复合类型上的域的值。
* 这与通常的原则一致,即CoerceToDomain不会改变基础类型值的物理表示。
*
* 注意:选择字段顺序是考虑到Oid可能在将来扩展到64位。
*/
} DatumTupleFields;
struct HeapTupleHeaderData
{
union
{
HeapTupleFields t_heap;
DatumTupleFields t_datum;
} t_choice;
ItemPointerData t_ctid; /* 当前元组的TID或更新的元组(或一个假设性插入标记) */
/* 下面的字段必须与MinimalTupleData匹配! */
#define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK2 2
uint16 t_infomask2; /* 属性数量 + 各种标志 */
#define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK 3
uint16 t_infomask; /* 各种标志位,见下文 */
#define FIELDNO_HEAPTUPLEHEADERDATA_HOFF 4
uint8 t_hoff; /* 头部大小,包括位图和填充 */
/* ^ - 23字节 - ^ */
#define FIELDNO_HEAPTUPLEHEADERDATA_BITS 5
bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* NULL位图 */
/* 结构的末尾有更多的数据 */
};
会简要描述用于插入和更新元组的自由空间映射(FSM)。
元组的表示
将新元组直接插入目标表的页面中
假设一个 txid 为 99 的事务在页面中插入了一个元组,在这种情况下,插入元组的标头字段设置如下
Tuple_1:
目标元组会被逻辑删除。执行 DELETE 命令的 txid 值被设置为元组的 t_xmax
假设 txid 111 删除了元组 Tuple_1。在这种情况下,Tuple_1 的标头字段设置如下:
Tuple_1:
PostgreSQL 在逻辑上删除最新的元组并插入新的元组
假设用 txid 99 插入的记录被 txid 100 更新了两次。
执行第一条 UPDATE 命令时,通过将 txid 100 设置为 t_xmax,逻辑上删除了 Tuple_1,然后插入了 Tuple_2。然后,Tuple_1 的 t_ctid 被改写为指向 Tuple_2。Tuple_1 和 Tuple_2 的标头字段如下:
Tuple_1:
执行第二条 UPDATE 命令时,与第一条 UPDATE 命令一样,逻辑上删除 Tuple_2,插入 Tuple_3。Tuple_2 和 Tuple_3 的标头字段如下:
Tuple_2:
Postgresql中插入和更新元组的自由空间映射是一种用于提高数据存储效率和性能的机制,它可以避免对每个页面进行全表扫描,从而快速找到有足够空间的页面来存放新的或更新的元组。