本文主要记录了 PostgreSQL 中外存管理部分代码的阅读笔记。首先针对 PostgreSQL 中表的磁盘页的组织形式进行分析,然后介绍外存操作的相关接口等。
表和元组的存储
PostgreSQL 中数据表是以块存储的形式来实现的,表中的所有数据通常会以元组(行)的形式存储到块中,这些块在被载入内存时中以内存页的形式存在,在内存页上进行相关操作后,会以同样的形式写入物理存储中。本质上,内存页和磁盘页是数据在不同存储介质中的表示形式。
PostgreSQL 中的四种堆文件(普通堆、临时堆、序列、TOAST表)均使用相似的磁盘页结构进行存储,具体介绍如下。
磁盘页结构
PostgreSQL 中,磁盘页的结构如下图示例:
在一个表的磁盘页上,分别由 PageHeaderData 、Tuple、Special Space 三部分组成,其中:
-
PageHeaderData ,位于页面的首部,存储该页的相关元信息。该结构的代码表示如下,其中各项的内容如下:
// From src/include/storage/bufpage.h typedef struct PageHeaderData { /* XXX LSN is member of *any* block, not only page-organized ones */ PageXLogRecPtr pd_lsn; /* LSN: next byte after last byte of xlog * record for last change to this page */ uint16 pd_checksum; /* checksum */ uint16 pd_flags; /* flag bits, see below */ LocationIndex pd_lower; /* offset to start of free space */ LocationIndex pd_upper; /* offset to end of free space */ LocationIndex pd_special; /* offset to start of special space */ uint16 pd_pagesize_version; TransactionId pd_prune_xid; /* oldest prunable XID, or zero if none */ ItemIdData pd_linp[FLEXIBLE_ARRAY_MEMBER]; /* line pointer array */ } PageHeaderData;
-
pd_lsn
:上次更改该页面的 xlog ,用于保证 buffer manager 中 WAL 的写入(仅当 xlog 写入后才会将该页面落盘) -
pd_checksum
:该页面的校验和 -
pd_flags
:特殊标志,在头部记录某些特殊数据用于某些特定的操作,例如,其中的PD_HAS_FREE_LINES
的标志表示是否当前存在空闲的 line pointer,在插入数据时可以快速定位到数据欲写入的位置;PD_PAGE_FULL
标志表示页面是否满了。 -
pd_lower
和pd_upper
:页面中的空闲空间的起始位置和结束位置指针 -
pd_special
:Special Space 开始时的页面指针 -
pd_linp[FLEXIBLE_ARRAY_MEMBER]
:ItemIdData
类型的数据,用于指示特定的元组在页面偏移量、状态标记和长度,具体代码如下。// From src/include/storage/itemid.h typedef struct ItemIdData { unsigned lp_off:15, /* offset to tuple (from start of page) */ lp_flags:2, /* state of line pointer, see below */ lp_len:15; /* byte length of tuple */ } ItemIdData; typedef ItemIdData *ItemId; /* * lp_flags has these possible states. An UNUSED line pointer is available * for immediate re-use, the other states are not. */ #define LP_UNUSED 0 /* unused (should always have lp_len=0) */ #define LP_NORMAL 1 /* used (should always have lp_len>0) */ #define LP_REDIRECT 2 /* HOT redirect (should have lp_len=0) */ #define LP_DEAD 3 /* dead, may or may not have storage */
-
-
Tuple,位于 Special Space 前面,由页面高地址往低地址增长,存储元组的具体内容,将在下文具体介绍。
-
Special Space,位于页面的尾部,存储与索引方法相关的数据,主要用于索引文件中,普通表中将其置空。
元组结构
元组结构的示意图如下,分别由元组头部信息和实际数据组成。元组头部信息由 HeapTupleHeaderData
结构表示,该结构不仅包含了元组中的数据内容的相关信息,也包含了一个 t_heap
的结构用于实现并发访问的 MVCC。
该结构中的具体代码如下:
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_choice
是个联合体,其中包含了两个成员:t_heap
:用于记录对元组执行插入或删除操作的事务 ID 和命令 ID,用于实现 MVCC。t_datum
:在新元组在内存中被创建时,此时不需要关心该元组的事务可见性,该字段用于在内存中记录元组的长度等信息。当新元组被插入到内存页上时,会将其结构所占用的内存转换为t_heap
并填入相关信息。
t_ctid
是个用于记录当前元组或者新元组的物理位置(块内偏移量和元组长度),如果元组被更新(PostgreSQL采用多版本方式更新元组,标记删除旧版本,插入新版本),则记录新版本元组的物理位置。