Innodb Page 的物理存储结构与行格式
大家好,今天我们来深入探讨 InnoDB 存储引擎中最为核心的概念之一:Page。Page 是 InnoDB 存储引擎管理数据的最小单元,理解 Page 的物理结构和行格式对于优化数据库性能至关重要。
一、Page 的物理存储结构
InnoDB 使用固定大小的 Page 来存储数据,默认大小为 16KB。可以通过参数 innodb_page_size
修改,但通常不建议修改,因为修改后会对性能产生影响。
一个 Page 主要由以下几个部分组成:
组成部分 | 大小 (Bytes) | 描述 |
---|---|---|
File Header | 38 | 记录 Page 的一些通用的信息,例如 Page 的类型、Page 的校验和、所属的表空间 ID 等。 |
Page Header | 56 | 记录 Page 自身的一些信息,例如 Page 中记录的数量、Page 中第一个记录的地址、Page 中最后一个记录的地址、Page 中空闲空间的起始地址等。 |
User Records | 可变 | 实际存储行记录的部分。不同的行格式 (Row Format) 会影响这部分的存储方式。 |
Free Space | 可变 | Page 中未被使用的空间。新记录会首先分配到 Free Space 中。 |
System Records | 可变 | 系统记录,例如 Infimum 记录和 Supremum 记录。这两个记录是 InnoDB 自动创建的,分别表示 Page 中最小和最大的记录。它们不存储实际的用户数据,主要用于简化查询操作。 |
File Trailer | 8 | 用于校验 Page 的完整性。包含 Page 的校验和以及 Page 的 LSN (Log Sequence Number) 的低 4 个字节。 |
1. File Header
File Header 包含了 Page 的通用信息,其结构如下:
名称 | 大小 (Bytes) | 描述 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4 | Page 所在的表空间 ID。如果设置了 innodb_checksums ,则这 4 个字节用于存储 Page 的校验和。 |
FIL_PAGE_OFFSET | 4 | Page 在表空间中的偏移量,也就是 Page 的编号。 |
FIL_PAGE_PREV | 4 | 上一个 Page 的偏移量。如果 Page 是链表中的第一个 Page,则该值为 NULL。 |
FIL_PAGE_NEXT | 4 | 下一个 Page 的偏移量。如果 Page 是链表中的最后一个 Page,则该值为 NULL。 |
FIL_PAGE_LSN | 8 | Page 最后被修改的日志序列号 (LSN)。LSN 用于恢复操作,确保数据的一致性。 |
FIL_PAGE_TYPE | 2 | Page 的类型。常见的类型包括:INDEX (索引页)、UNDO LOG PAGE (Undo 日志页)、SYSTEM PAGE (系统页) 等。 |
FIL_PAGE_FILE_FLUSH_LSN | 8 | Page 被刷新到磁盘时的 LSN。 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4 | 用于归档的日志编号或表空间 ID。 |
2. Page Header
Page Header 记录了 Page 自身的状态信息,其结构如下:
名称 | 大小 (Bytes) | 描述 |
---|---|---|
PAGE_N_DIR_SLOTS | 2 | Page 中目录槽 (Directory Slot) 的数量。目录槽用于快速定位记录,类似于哈希表的桶。 |
PAGE_HEAP_TOP | 2 | Page 中堆的顶部地址,也就是 Free Space 的起始地址。 |
PAGE_N_HEAP | 2 | Page 中堆的数量。 |
PAGE_FREE | 2 | 指向 Free Space 链表中第一个 Page 的指针。 |
PAGE_GARBAGE | 2 | 已删除记录占用的字节数。 |
PAGE_LAST_INSERT | 2 | 最后一个插入的记录的地址。 |
PAGE_DIRECTION | 2 | 记录插入的方向。用于优化插入操作。 |
PAGE_N_DIRECTION | 2 | 连续往同一个方向插入的记录的数量。 |
PAGE_N_RECS | 2 | Page 中记录的数量(包括 Infimum 和 Supremum 记录)。 |
PAGE_MAX_TRX_ID | 8 | 修改当前页的最大事务ID |
PAGE_LSN | 8 | 当前页的LSN |
PAGE_SPACE_ID | 4 | 当前页的表空间ID |
PAGE_CHECKSUM | 4 | 当前页的校验和 |
3. User Records
User Records 存储了实际的行记录。具体的存储格式取决于所使用的行格式 (Row Format)。我们将在下一节详细讨论行格式。
4. Free Space
Free Space 是 Page 中未被使用的空间。当需要插入新的记录时,InnoDB 会首先从 Free Space 中分配空间。
5. System Records (Infimum 和 Supremum Records)
Infimum 和 Supremum 记录是 InnoDB 自动创建的两个虚拟记录,分别表示 Page 中最小和最大的记录。它们不存储实际的用户数据,主要用于简化查询操作。
6. File Trailer
File Trailer 用于校验 Page 的完整性。包含 Page 的校验和以及 Page 的 LSN (Log Sequence Number) 的低 4 个字节。校验和用于检测数据是否损坏,LSN 用于恢复操作。
二、行格式 (Row Format)
InnoDB 提供了多种行格式,不同的行格式会影响 User Records 的存储方式。常见的行格式包括:
- REDUNDANT (已废弃)
- COMPACT
- DYNAMIC
- COMPRESSED
MySQL 5.7 及之前的版本默认使用 COMPACT
行格式,MySQL 8.0 默认使用 DYNAMIC
行格式。
我们可以通过以下 SQL 语句查看表的行格式:
SHOW TABLE STATUS LIKE 'your_table_name'G
其中 Row_format
字段显示了表的行格式。
接下来,我们分别介绍 COMPACT
和 DYNAMIC
行格式。
1. COMPACT 行格式
COMPACT
行格式的设计目标是尽可能地减少存储空间。其记录结构如下:
部分 | 描述 |
---|---|
记录头信息 (Record Header) | 5 字节。记录头信息包含了记录的一些状态信息,例如:记录是否被删除、记录的类型、指向下一条记录的指针等。 |
列长度信息 (Variable Length Column Length) | 如果记录中包含变长列(例如 VARCHAR、TEXT、BLOB 等),则这部分存储了变长列的长度。 |
NULL值标志位 (NULL Flags) | 如果记录中包含允许为 NULL 的列,则这部分存储了 NULL 值标志位。每个允许为 NULL 的列对应一个标志位,如果该列的值为 NULL,则对应的标志位被设置为 1。 |
实际列数据 (Actual Column Data) | 实际存储列数据的部分。 |
记录头信息 (Record Header)
记录头信息包含了记录的一些状态信息,其结构如下:
名称 | 大小 (Bits) | 描述 |
---|---|---|
delete_mask | 1 | 标记记录是否被删除。 |
min_rec_mask | 1 | 标记记录是否为 B+ 树的最小记录。 |
n_owned | 4 | 表示当前记录拥有的记录数。仅在目录槽 (Directory Slot) 对应的记录中有效。 |
heap_no | 13 | 表示当前记录在堆中的编号。 |
record_type | 3 | 表示记录的类型。0 表示普通记录,1 表示 B+ 树非叶子节点记录,2 表示 Infimum 记录,3 表示 Supremum 记录。 |
next_record | 16 | 指向下一条记录的指针。通过 next_record 可以将 Page 中的记录连接成一个单向链表。 |
列长度信息 (Variable Length Column Length)
如果记录中包含变长列(例如 VARCHAR、TEXT、BLOB 等),则这部分存储了变长列的长度。
- 如果变长列的实际长度小于 255 字节,则用 1 个字节存储长度。
- 如果变长列的实际长度大于 255 字节,则用 2 个字节存储长度。
NULL值标志位 (NULL Flags)
如果记录中包含允许为 NULL 的列,则这部分存储了 NULL 值标志位。每个允许为 NULL 的列对应一个标志位,如果该列的值为 NULL,则对应的标志位被设置为 1。
实际列数据 (Actual Column Data)
实际存储列数据的部分。
2. DYNAMIC 行格式
DYNAMIC
行格式是对 COMPACT
行格式的改进。它主要解决了 COMPACT
行格式在存储大 TEXT 或 BLOB 列时效率较低的问题。
在 DYNAMIC
行格式中,如果 TEXT 或 BLOB 列的长度超过一定阈值(默认为 40 字节),则只会将列的一部分内容(通常是 20 字节)存储在 User Records 中,而将剩余的内容存储在溢出页 (Overflow Page) 中。User Records 中存储的部分内容包含了指向溢出页的指针。
DYNAMIC
行格式的记录结构与 COMPACT
行格式类似,主要的区别在于对 TEXT 和 BLOB 列的处理方式。
3. 行溢出(Row Overflow)
当一行数据过大,单个Page无法容纳时,就会发生行溢出。DYNAMIC
和COMPRESSED
行格式就是为了更好地处理行溢出而设计的。
三、Page 目录
为了加快在 Page 中查找记录的速度,InnoDB 使用了 Page 目录 (Page Directory)。Page 目录类似于哈希表的桶,它将 Page 中的记录分成若干个组,每个组对应一个目录槽 (Directory Slot)。目录槽存储了组中最后一条记录的地址。
当我们需要在 Page 中查找一条记录时,首先通过二分查找找到对应的目录槽,然后从目录槽对应的记录开始,沿着链表遍历,直到找到目标记录。
四、代码示例
虽然无法直接访问 InnoDB 的物理存储结构,但可以通过一些工具来分析 Page 的内容。例如,可以使用 hexdump
命令查看 Page 的十六进制表示,然后根据 Page 的结构来解析数据。
以下是一个使用 Python 解析 COMPACT
行格式的示例代码:
import struct
def parse_compact_row(row_data):
"""
解析 COMPACT 行格式的数据。
"""
# 1. 解析记录头信息 (Record Header)
header = struct.unpack('<BBBBBB', row_data[:6]) # 这里假设只取前6个字节,实际是5个字节,第六个字节可能是其他信息
delete_mask = header[0] & 0x01
min_rec_mask = header[0] & 0x02
n_owned = (header[0] >> 2) & 0x0F
heap_no = (header[1] << 8 | header[2]) & 0x1FFF
record_type = (header[3] >> 13) & 0x07
next_record = header[4] << 8 | header[5]
# 2. 解析列长度信息 (Variable Length Column Length) 和 NULL 值标志位 (NULL Flags)
# 这部分的代码需要根据表的结构来动态解析
# 这里只是一个示例,假设表包含一个 VARCHAR 列和一个 INT 列,VARCHAR 列允许为 NULL
offset = 6 # 记录头信息的长度
# 解析 VARCHAR 列的长度
varchar_length = struct.unpack('<H', row_data[offset:offset+2])[0]
offset += 2
# 解析 NULL 值标志位
null_flags = struct.unpack('<B', row_data[offset:offset+1])[0]
offset += 1
# 3. 解析实际列数据 (Actual Column Data)
varchar_data = row_data[offset:offset+varchar_length].decode('utf-8')
offset += varchar_length
int_data = struct.unpack('<i', row_data[offset:offset+4])[0]
offset += 4
print(f"Delete Mask: {delete_mask}")
print(f"Min Rec Mask: {min_rec_mask}")
print(f"N Owned: {n_owned}")
print(f"Heap No: {heap_no}")
print(f"Record Type: {record_type}")
print(f"Next Record: {next_record}")
print(f"VARCHAR Data: {varchar_data}")
print(f"INT Data: {int_data}")
# 示例数据 (需要替换为实际的 COMPACT 行格式数据)
row_data = b'x00x00x00x00x00x00x05x00x00x05hellox01x00x00x00'
parse_compact_row(row_data)
注意:
- 以上代码只是一个示例,实际的解析过程会更加复杂,需要根据表的结构来动态解析。
- 获取实际的
COMPACT
行格式数据需要使用一些专业的工具,例如hexdump
。 - 在生产环境中,不建议直接解析 InnoDB 的物理存储结构,因为这可能会导致数据损坏。
五、总结,InnoDB Page 的重要性
InnoDB Page 作为 InnoDB 存储引擎中管理数据的最小单元,其物理结构和行格式对于数据库的性能至关重要。理解 Page 的结构可以帮助我们更好地优化数据库设计和查询。行格式直接影响存储效率,选择合适的行格式可以减少磁盘空间占用,提高 IO 效率。