目录
一、环境信息
名称 | 值 |
CPU | 12th Gen Intel(R) Core(TM) i7-12700H |
操作系统 | CentOS Linux release 7.9.2009 (Core) |
内存 | 3G |
逻辑核数 | 2 |
PG版本 | 9.6.24 |
二、参考内容
参考内容 |
《PostreSQL指南:内幕探索》 |
《PostgreSQL: Documentation: 9.6: PostgreSQL 9.6.24 Documentation》 |
三、数据文件
数据文件例如:堆表、索引、空闲空间映射、可见性映射等,内部会被划分成多个固定长度的数据页,数据页大小通常大小为8KB,但也有其他情况如:16KB,32KB。每个文件中的页从0开始顺序编号,编号可以称为页号或区块号。
四、堆文件页面
1、图示
每一个PAGE可以转换为一个PageHeaderData结构体。
2、PageHeaderData结构体
typedef struct PageHeaderData
{
/* XXX LSN is member of *any* block, not only page-organized ones */
PageXLogRecPtr pd_lsn;
uint16 pd_checksum;
uint16 pd_flags;
LocationIndex pd_lower;
LocationIndex pd_upper;
LocationIndex pd_special;
uint16 pd_pagesize_version;
TransactionId pd_prune_xid;
ItemIdData pd_linp[FLEXIBLE_ARRAY_MEMBER];
} PageHeaderData, *PageHeader;
参数 | 说明 |
pd_lsn | 记录最后一次修改该页面的 WAL(Write-Ahead Log)记录的 LSN。即当前的WAL位置。 |
pd_checksum | 即校验和,为每个数据页计算校验和。检测到校验和失败将导致读取数据时出错,并将中止当前正在运行的事务。因此,这为直接在数据库服务器级别检测I/O或硬件问题带来了额外的控制。 |
pd_flags | 用于记录页面的状态标志。 |
pd_lower | 空闲空间的起始偏移量。 |
pd_upper | 空闲空间的结束偏移量。 |
pd_special | 特殊空间开始位置。 在索引页中会用到该字段,在堆表页中它指向页尾。(在索引页中它指向特殊空间的起始位置,特殊空间是仅由索引使用的特殊数据区域,包含特定的数据,具体内容依索引的类型而定,如B树、GiST、GiN等。) |
pd_pagesize_version | 用于管理页面的版本和大小信息,确保数据存储的兼容性和完整性。 页面版本编号与页面尺寸被打包成了单个uint16字段,这是有历史原因的:在PostgreSQL7.3之前,并没有页面版本编号这个概念,这样做能让我们假装7.3之前版本的页面版本编号为0。我们约定页面的尺寸必须为256的倍数,留下低8位用于页面版本编号。 |
pd_prune_xid | 最旧的可修剪(删除)的XID,是一个提示字段,用于帮助确认剪枝是否有用。目前索引页没用。 |
pd_linp | 行指针,是一个柔性数组。 |
C语言柔性数组相关介绍可以参考之前的博客:《C语言学习-24-柔性数组》;
3、pd_flags相关宏
#define PD_HAS_FREE_LINES 0x0001
#define PD_PAGE_FULL 0x0002
#define PD_ALL_VISIBLE 0x0004
#define PD_VALID_FLAG_BITS 0x0007
宏 | 描述 |
PD_HAS_FREE_LINES | 页面中存在空闲行指针(line pointers)。 |
PD_PAGE_FULL | 页面已被标记为“满”(空间不足,需优先跳过扫描)。 |
PD_ALL_VISIBLE | 页面所有元组对事务均可见(用于可见性映射优化)。 |
PD_VALID_FLAG_BITS | 保留位(表示当前版本支持的有效标志位总数)。 |
4、PageXLogRecPtr结构体
typedef struct
{
uint32 xlogid; /* high bits */
uint32 xrecoff; /* low bits */
} PageXLogRecPtr;
字段名 | 说明 |
---|---|
xlogid |
日志文件段编号的高位部分。 |
xrecoff |
日志文件段内的偏移量(低位部分)。 |
5、ItemIdData结构体
typedef struct ItemIdData
{
unsigned lp_off:15,
lp_flags:2,
lp_len:15;
} ItemIdData, *ItemId;
此结构体使用的是C语言位域,其特点和工作原理如下:
名称 | 描述 |
内存布局 | 这三个成员总共占用 15 + 2 + 15 = 32 位,即4字节,它们被打包在一个无符号整数(通常是32位)中,具体布局顺序(哪个成员在高位,哪个在低位)取决于编译器和平台。 |
空间优化 | 位域允许在单个机器字中存储多个小范围的值,相比使用三个独立的整数字段(可能占用12字节),这种方法只需4字节。 |
访问方式 | 可以像普通结构体成员一样访问位域成员,编译器会自动处理位的提取和设置。 |
参数 | 说明 |
lp_off | 元组在文件块中的偏移量(从页头开始计算)。 |
lp_flags | 元组的状态。 |
lp_len | 元组的字节长度。 |
6、lp_flags相关宏
#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 */
宏 | 描述 |
LP_UNUSED | 未使用(lp_len必须始终为0) 。 |
LP_NORMAL | 正常使用 (lp_len 必须始终>0)。 |
LP_REDIRECT | HOT 重定向 (lp_len必须为0)。 |
LP_DEAD | 死元组,有没有对应的存储尚未可知。 |
7、HeapTupleHeaderData结构体
typedef struct HeapTupleHeaderData
{
union
{
HeapTupleFields t_heap;
DatumTupleFields t_datum;
}t_choice;
ItemPointerData t_ctid;
uint16 t_infomask2;
uint16 t_infomask;
uint8 t_hoff;
bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs */
/* MORE DATA FOLLOWS AT END OF STRUCT */
}HeapTupleHeaderData, *HeapTupleHeader;
参数 | 说明 |
t_heap | 用于记录对元组执行插入或删除操作的事务ID和命令ID,这些信息主要用于并发控制时检查元组对事务的可见性。 |
t_datum | 记录元组的长度等信息。 |
t_ctid | 元组指针,用于记录当前元组或者新元组的物理位置(块内偏移量和元组长度),若元组被更新,则记录的是新版本元组的物理位置。 |
t_infomask2 | 使用其低11位表示当前元组的属性个数,其他位则用于HOT技术以及元组可见性的标志位。 |
t_infomask | 用于标识元组当前的状态,比如元组是否具有OID、是否有空属性等,其每一位对应不同的状态,共16种状态。 |
t_hoff | 元组头的大小。 |
t_bits | 表示元组那些字段为空。 |
8、HeapTupleFields结构体
专门用于管理事务可见性和元组生命周期,是实现 MVCC (多版本并发控制) 的基石。
typedef struct HeapTupleFields
{
TransactionId t_xmin;
TransactionId t_xmax;
union
{
CommandId t_cid;
TransactionId t_xvac;
} t_field3;
} HeapTupleFields;
字段名 | 说明 |
---|---|
t_xmin |
插入事务ID。 • 记录成功插入此元组的事务的唯一标识符。 • 对于一个事务来说,只有当 t_xmin 对应的事务已经提交,并且其提交时间点早于当前事务的快照时间,这个元组才对当前事务可见。 |
t_xmax |
删除/锁定事务ID。 • 默认值為 0,表示该元组尚未被删除或锁定。 • 如果非零,则表示: - 删除: 某个事务删除了此元组。该事务提交后,此元组应被视作无效(但可能由于MVCC而对某些事务仍可见)。 - 更新: 更新操作在PG中等于删除+插入。 t_xmax 记录了删除旧版本的事务ID。- 行锁: 一个事务锁定了此行(例如 SELECT ... FOR UPDATE )。在该事务结束(提交或回滚)前,t_xmax 会一直持有该事务ID。 |
t_field3 |
一个联合体(共用体),其含义取决于元组的当前状态(主要由 t_xmax 的值决定):• 当 t_xmax == 0 (元组有效):- t_cid (CommandId ): 命令ID。表示在插入事务(t_xmin )中,是第几个SQL命令插入了该元组(从0开始计数)。用于在同一事务内确定操作的先后顺序。• 当 t_xmax != 0 (元组被无效化):- t_xvac (TransactionId ): 清理事务ID。通常由 VACUUM FULL 操作设置。表示此元组是由一个 VACUUM FULL 事务移动或冻结的旧版本元组。这是一个相对少见的特殊用途字段。 |
9、DatumTupleFields结构体
typedef struct DatumTupleFields
{
int32 datum_len_;
int32 datum_typmod;
Oid datum_typeid;
} DatumTupleFields;
参数 | 描述 |
---|---|
datum_len_ |
Datum 数据长度 • 这是 varlena 类型数据的头部信息,存储数据的长度 • 在 PostgreSQL 中,varlena 是可变长度数据的内部表示格式 |
datum_typmod |
类型修饰符 • 用于提供数据类型的附加信息 • 值为 -1 表示没有类型修饰符 • 对于记录类型,这是一个标识符 |
datum_typeid |
数据类型对象标识符 • 存储数据类型的 OID(对象标识符) • 对于复合类型,存储复合类型的 OID • 如果是记录类型,则存储特殊的 RECORDOID • OID 是 PostgreSQL 内部用于唯一标识系统表中对象的数字 |
场景 | DatumTupleFields 是否有效? |
datum_typeid 的含义 |
datum_len_ 的含义 |
---|---|---|---|
普通堆元组(Heap Tuple) | 部分有效 | 通常无效/无意义。内存被 HeapTupleFields.t_field3 占用。 |
有效。被借用,存储所有变长字段的数据总长度。 |
TOAST 指针 | 完全有效 | 有效。值为 TIDOID ,表明这是一个指向TOAST数据的指针。 |
有效。存储这个TOAST指针结构本身的长度。 |
匿名记录类型 | 完全有效 | 有效。值为 RECORDOID ,表明这是一个匿名记录。 |
有效。存储这个记录数据的长度。 |
10、ItemPointerData结构体
typedef struct ItemPointerData
{
BlockIdData ip_blkid;
OffsetNumber ip_posid;
}ItemPointerData;
字段名 | 说明 |
---|---|
ip_blkid |
块标识符 • 指定元组所在的磁盘块(页面) • BlockIdData 内部包含 bi_hi 和 bi_lo 字段,共同表示 32 位块编号 |
ip_posid |
第几行TUPLE,元组在行指针数组中的索引号/序号,1, 2, 3, ... (从1开始的整数序号)。 |
11、BlockIdData结构体
typedef struct BlockIdData
{
uint16 bi_hi; /*块编号的高 16 位。*/
uint16 bi_lo; /*块编号的低 16 位。*/
} BlockIdData;
字段名 | 说明 |
---|---|
bi_hi |
块编号的高 16 位。 |
bi_lo |
块编号的低 16 位。 |
BlockIdGetBlockNumber(blockId): 从 BlockIdData 结构提取出完整的 32 位块号。
/*
* PointerIsValid
* True iff pointer is valid.
*/
#define PointerIsValid(pointer) ((const void*)(pointer) != NULL)
/*
* BlockIdIsValid
* True iff the block identifier is valid.
*/
#define BlockIdIsValid(blockId) \
((bool) PointerIsValid(blockId))
/*
* BlockIdGetBlockNumber
* Retrieve the block number from a block identifier.
*/
#define BlockIdGetBlockNumber(blockId) \
( \
AssertMacro(BlockIdIsValid(blockId)), \
(BlockNumber) (((blockId)->bi_hi << 16) | ((uint16) (blockId)->bi_lo)) \
)
12、t_infomask2相关宏
#define HEAP_NATTS_MASK 0x07FF
#define HEAP_KEYS_UPDATED 0x2000
#define HEAP_HOT_UPDATED 0x4000
#define HEAP_ONLY_TUPLE 0x8000
#define HEAP2_XACT_MASK 0xE000
宏 | 说明 |
---|---|
HEAP_NATTS_MASK |
属性数量掩码。 • 用于从 t_infomask2 字段中提取该元组所包含的属性(列)的数量。• 它覆盖了 t_infomask2 的低 11 位(2^11 - 1 = 2047),这意味着一个元组最多可拥有 2047 个列(实际限制可能更小,受页面大小等因素制约)。 |
HEAP_KEYS_UPDATED |
键被更新标志。 • 如果设置此标志位,表示此元组是由一个 UPDATE 操作产生的,并且该 UPDATE 修改了至少一个索引键的列。• 这对于索引维护至关重要。当此标志被设置时,数据库知道它必须删除旧的索引条目并插入新的条目,即使该 UPDATE 可能是一个HOT(Heap Only Tuple)更新。• 它帮助优化索引维护流程。 |
HEAP_HOT_UPDATED |
HOT更新目标标志。 • 此标志设置在旧版本的元组上。 • 如果设置,表示该元组已被后续的 HOT更新,并且在这个旧元组之后同一个数据页内存在一个更新的版本。 • 它本质上是一个指针,告诉 vacuum 和索引扫描等操作:“要找到这个元组的最新版本,不必去查索引,可以直接在同一页面上寻找更晚的版本。” |
HEAP_ONLY_TUPLE |
仅堆元组标志。 • 如果设置,表示此元组是一个 HOT 元组。 • 这意味着该元组是某个更新操作产生的新版本,并且由于更新没有修改任何索引键,因此这个新版本没有额外的索引条目指向它。索引仍然指向原始的行版本(即行的开头)。 • 设置此标志的元组只能通过基于旧版本元组的“HOT链”来找到,而不能通过索引直接定位。 |
HEAP2_XACT_MASK |
事务信息掩码(已废弃)。 • 这是一个历史遗留的掩码,在现代版本的 PostgreSQL 中已不再使用。 • 它最初的设计目的是为了从 t_infomask2 中提取一些事务相关信息,但其功能早已被转移到 t_infomask 字段中。• 现在,这个掩码所覆盖的位(高 3 位)被用于上述的三个独立标志: KEYS_UPDATED , HOT_UPDATED , 和 ONLY_TUPLE 。• 重要提示:在你的代码中,不应该使用这个掩码,而应该直接使用各个独立的标志位宏。 |
13、t_infomask相关宏
#define HEAP_HASNULL 0x0001
#define HEAP_HASVARWIDTH 0x0002
#define HEAP_HASEXTERNAL 0x0004
#define HEAP_HASOID 0x0008
#define HEAP_XMAX_KEYSHR_LOCK 0x0010
#define HEAP_COMBOCID 0x0020
#define HEAP_XMAX_EXCL_LOCK 0x0040
#define HEAP_XMAX_LOCK_ONLY 0x0080
/* xmax is a shared locker */
#define HEAP_XMAX_SHR_LOCK (HEAP_XMAX_EXCL_LOCK | HEAP_XMAX_KEYSHR_LOCK)
#define HEAP_LOCK_MASK (HEAP_XMAX_SHR_LOCK | HEAP_XMAX_EXCL_LOCK | HEAP_XMAX_KEYSHR_LOCK)
#define HEAP_XMIN_COMMITTED 0x0100
#define HEAP_XMIN_INVALID 0x0200
#define HEAP_XMIN_FROZEN (HEAP_XMIN_COMMITTED|HEAP_XMIN_INVALID)
#define HEAP_XMAX_COMMITTED 0x0400
#define HEAP_XMAX_INVALID 0x0800
#define HEAP_XMAX_IS_MULTI 0x1000
#define HEAP_UPDATED 0x2000
#define HEAP_MOVED_OFF 0x4000
#define HEAP_MOVED_IN 0x8000
#define HEAP_MOVED (HEAP_MOVED_OFF | HEAP_MOVED_IN)
#define HEAP_XACT_MASK 0xFFF0
宏名称 | 详细解释 |
---|---|
HEAP_HASNULL |
表示元组中包含至少一个 NULL 值。如果设置,元组头部后会有一个空值位图指示哪些列为 NULL。 |
HEAP_HASVARWIDTH |
表示元组中包含至少一个变长字段(如 TEXT、VARCHAR、JSONB 等)。如果设置,元组头部会借用 DatumTupleFields 结构。 |
HEAP_HASEXTERNAL |
表示元组中包含至少一个外部存储(TOAST)的字段。这些字段的值存储在独立的 TOAST 表中。 |
HEAP_HASOID |
表示元组包含系统生成的 OID 字段。在现代 PostgreSQL 中,普通表通常不再自动分配 OID。 |
HEAP_XMAX_KEYSHR_LOCK |
表示元组被键共享锁(Key Share Lock)锁定。允许读取但防止冲突的 UPDATE/DELETE 操作。 |
HEAP_COMBOCID |
表示使用组合命令 ID(Combined Command ID),用于优化子事务中的命令标识。 |
HEAP_XMAX_EXCL_LOCK |
表示元组被排他锁(Exclusive Lock)锁定。阻止其他事务以任何方式访问该元组。 |
HEAP_XMAX_LOCK_ONLY |
表示 t_xmax 仅用于记录锁持有者,而不是删除/更新操作。事务结束后锁会被释放。 |
HEAP_XMIN_COMMITTED |
表示插入事务(t_xmin)已提交。这是一个缓存优化,避免频繁查询事务日志。 |
HEAP_XMIN_INVALID |
表示插入事务(t_xmin)无效。通常意味着元组本身无效。 |
HEAP_XMAX_COMMITTED |
表示删除/锁定事务(t_xmax)已提交。这是一个缓存优化。 |
HEAP_XMAX_INVALID |
表示删除/锁定事务(t_xmax)无效。意味着 t_xmax 字段应该被忽略。 |
HEAP_XMAX_IS_MULTI |
表示 t_xmax 是多事务 ID(MultiXactId),不是普通事务 ID。用于多个事务对同一元组持有锁的情况。 |
HEAP_UPDATED |
表示元组是 UPDATE 操作的结果(新版本)。用于区分插入的元组和更新产生的元组。 |
HEAP_MOVED_OFF |
表示元组已被 pre-9.0 VACUUM FULL 移出。保留用于二进制升级支持。 |
HEAP_MOVED_IN |
表示元组已被 pre-9.0 VACUUM FULL 移入。保留用于二进制升级支持。 |
组合标志宏
宏名称 | 详细解释 |
---|---|
HEAP_XMAX_SHR_LOCK |
表示元组被共享锁锁定。这是 HEAP_XMAX_EXCL_LOCK 和 HEAP_XMAX_KEYSHR_LOCK 的组合。 |
HEAP_LOCK_MASK |
用于检查任何类型的锁(共享锁、排他锁或键共享锁)的位掩码。 |
HEAP_XMIN_FROZEN |
表示元组已被"冻结",对所有事务可见。这是防止事务 ID 回绕的关键机制。 |
HEAP_MOVED |
表示元组已被移动(无论是移出还是移入)。保留用于二进制升级支持。 |
HEAP_XACT_MASK |
用于提取所有与事务可见性相关的位的掩码(排除前4个与元组结构相关的位)。 |