本文主要记录了 PostgreSQL 中外存管理部分代码的阅读笔记。首先针对 PostgreSQL 中表的磁盘页的组织形式进行分析,然后介绍外存操作的相关接口等。

表和元组的存储

PostgreSQL 中数据表是以块存储的形式来实现的,表中的所有数据通常会以元组(行)的形式存储到块中,这些块在被载入内存时中以内存页的形式存在,在内存页上进行相关操作后,会以同样的形式写入物理存储中。本质上,内存页和磁盘页是数据在不同存储介质中的表示形式。

PostgreSQL 中的四种堆文件(普通堆、临时堆、序列、TOAST表)均使用相似的磁盘页结构进行存储,具体介绍如下。

磁盘页结构

PostgreSQL 中,磁盘页的结构如下图示例:

PG_page

在一个表的磁盘页上,分别由 PageHeaderData 、Tuple、Special Space 三部分组成,其中:

  1. 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_lowerpd_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 */
      
  2. Tuple,位于 Special Space 前面,由页面高地址往低地址增长,存储元组的具体内容,将在下文具体介绍。

  3. Special Space,位于页面的尾部,存储与索引方法相关的数据,主要用于索引文件中,普通表中将其置空。

元组结构

元组结构的示意图如下,分别由元组头部信息和实际数据组成。元组头部信息由 HeapTupleHeaderData 结构表示,该结构不仅包含了元组中的数据内容的相关信息,也包含了一个 t_heap 的结构用于实现并发访问的 MVCC。

Tuple

该结构中的具体代码如下:

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采用多版本方式更新元组,标记删除旧版本,插入新版本),则记录新版本元组的物理位置。

Reference