第八章:数据的家-MySQL的数据目录 学习笔记
一、 数据目录与安装目录的区别
- 安装目录:存储 MySQL 的各类软件及可执行文件(如
bin目录下的mysql、mysqld等命令程序)。 - 数据目录:存储 MySQL 在运行过程中产生的真实数据(如数据库、表里的记录、日志文件等)。两者的概念和作用完全不同。
- 查看数据目录路径:在客户端连接服务器后,可通过执行命令
SHOW VARIABLES LIKE 'datadir';来查看当前系统的数据目录位置。
二、 数据目录的内部结构
1. 数据库在文件系统中的表示
- 每创建一个数据库,MySQL 就会在数据目录下创建一个与数据库名完全同名的子目录(文件夹)。
- 在这个对应的子目录下,会生成一个名为
db.opt的文件,专门用于保存该数据库的属性信息(如默认的字符集和比较规则)。 - 特例:系统自带的
information_schema数据库比较特殊,它在设计上被特殊对待,在数据目录下并没有对应的物理子目录。
2. 表在文件系统中的表示
表的数据分为“表结构”和“真实数据”两部分,它们在文件系统中的存储方式如下:
- 表结构文件:不论使用哪种存储引擎,MySQL 都会在表所在的数据库子目录下创建一个名为
表名.frm的二进制文件,用于记录表的结构(有哪些列、数据类型、约束等)。 - 表数据文件:
- InnoDB 存储引擎:基于“表空间(Tablespace)”来管理存储。
- 系统表空间:在 MySQL 5.6.6 版本之前,所有表的数据默认统统挤在系统表空间(通常是数据目录下的
ibdata1文件)中。 - 独立表空间:从 MySQL 5.6.6 开始默认开启了独立表空间。每创建一张表,都会在数据库子目录下生成一个
表名.ibd文件,该表的数据和 B+ 树索引都保存在这一个文件里。
- 系统表空间:在 MySQL 5.6.6 版本之前,所有表的数据默认统统挤在系统表空间(通常是数据目录下的
- MyISAM 存储引擎:采用数据和索引分离存储的机制,没有表空间的概念。
表名.MYD文件:专门存放表的用户记录数据(D 代表 Data)。表名.MYI文件:专门存放表的索引信息(I 代表 Index)。
- InnoDB 存储引擎:基于“表空间(Tablespace)”来管理存储。
3. 视图在文件系统中的表示
视图本质上是虚拟表(只是一个查询语句的别名),不需要存储真实数据。因此,创建视图时,文件系统中只会产生一个用于保存其结构的 视图名.frm 文件。
4. 其他额外文件
数据目录下除了用户数据,还包含了保障 MySQL 正常运行的其他文件:
- 服务器进程文件:如
.pid文件,记录当前运行的 MySQL 进程 ID。 - 服务器日志文件:包括错误日志、查询日志、二进制日志(binlog)、redo日志等。
- SSL/RSA 证书及密钥文件:用于保证客户端与服务器之间通信的安全。
三、 文件系统对数据库的制约
既然 MySQL 的数据归根结底是存放在操作系统的文件系统上的,那就不可避免地要受其物理制约:
- 命名长度限制:数据库名和表名受限于文件系统对单个目录或文件名称的最大长度限制。
- 特殊字符映射:为了防止表名或数据库名中的特殊字符引发文件系统报错,MySQL 会将除数字和拉丁字母外的特殊字符(如
?)映射为@+编码值的形式(例如test@003f.frm)。 - 文件大小限制:不管是 InnoDB 的
.ibd文件还是 MyISAM 的.MYD/.MYI文件,随着数据量增多文件会不断膨胀,但其最大容量受限于当前文件系统支持的最大单文件大小。
四、 MySQL 自带的四大系统数据库简介
- mysql:核心系统库。存储了 MySQL 的用户账户、权限配置、存储过程、事件、时区等至关重要的运行信息。
- information_schema:元数据宝库。保存了服务器维护的所有其他数据库的结构信息(如表、视图、列、索引、触发器等描述性信息),它内部的数据并非真实用户数据,而是系统的元数据。
- performance_schema:性能监控库。专门收集 MySQL 运行过程中的状态数据(如近期执行的语句、各阶段耗时、内存使用情况),用于性能调优。
- sys:便利工具库。它通过视图的形式,将
information_schema和performance_schema中的复杂信息整合起来,让 DBA 和程序员能更直观、方便地了解数据库的运行健康状态。
第九章:存放页面的大池子-InnoDB的表空间 学习笔记
一、 宏观结构:表空间、段、区、页的关系
表空间是一个抽象的概念,对应着文件系统上的一个或多个实际文件。如果把表空间想象成一个大池子,它的内部其实有着极其严密的层级划分:
- 页 (Page):InnoDB 管理存储空间的基本单位,默认大小为 16KB。
- 区 (Extent):为了避免在 B+ 树中相邻的页在物理磁盘上距离过远而导致随机 I/O,InnoDB 引入了“区”的概念。连续的 64 个页构成了一个区,一个区的大小是 1MB。
- 段 (Segment):这是一个逻辑概念,由若干个零散的页和若干个完整的区组成。为了不让 B+ 树的叶子节点(存放数据)和非叶子节点(存放目录)混在一个区里,InnoDB 规定每个索引都对应两个段(叶子节点段和非叶子节点段)。
- 碎片区机制:考虑到有的表数据极少,如果一上来就给它分配一个 1MB 的完整区太浪费空间了。因此,InnoDB 提出了“碎片区”的概念:这些区直属于表空间,不属于任何段,里面的页可以零散地分配给不同的段使用。当段内占用的零散页达到了 32 个,才会开始以“完整的区”为单位来分配空间。
二、 区的状态与链表管理
随着表空间不断增大,区的数量会成千上万。为了高效地管理这些区,InnoDB 引入了链表结构。
1. 区的四种状态
FREE:完全空闲的区。FREE_FRAG:直属于表空间的碎片区,且内部还有空闲页面。FULL_FRAG:直属于表空间的碎片区,且内部没有空闲页面了。FSEG:已经作为一个完整的区,被分配给某个具体的段了。
2. 核心管理链表
- 直属于表空间的 3 个链表(管理碎片区):
FREE链表:所有处于 FREE 状态的区。FREE_FRAG链表:所有处于 FREE_FRAG 状态的区。FULL_FRAG链表:所有处于 FULL_FRAG 状态的区。
- 附属于每个段的 3 个链表(管理 FSEG 状态的区):
FREE链表:段内所有页面都空闲的区。NOT_FULL链表:段内仍有空闲空间的区。FULL链表:段内已经没有空闲空间的区。
- 链表基节点 (List Base Node):为了能快速找到这些链表的头尾,InnoDB 为每个链表设计了一个基节点结构,其中只包含链表的长度、头节点指针和尾节点指针。
三、 幕后黑手:XDES Entry 与 INODE Entry
怎么记录每个区到底属于什么状态、属于哪个链表呢?
- XDES Entry (区描述信息):每个区都对应一个占 40 字节的结构,记录着这个区的状态和链表指针。
- 为了存放所有的 XDES Entry,表空间的区被划分为多个组(Group),每组包含 256 个区。每组的第一个页面就专门用来存放这 256 个区对应的 XDES Entry。
- INODE Entry (段描述信息):段是逻辑概念,为了记录段的属性(比如段 ID,段内的链表基节点在哪,零散页在哪等),InnoDB 为每个段设计了一个 INODE Entry 结构。
四、 表空间的关键页面类型
一个表空间里有着各种各样特定功能的页面,最核心的有三种:
FSP_HDR类型(页号为 0):这是整个表空间的第一个页面。它不仅存放了第一组(前 256 个区)的 XDES Entry,更重要的是,它包含了 File Space Header 部分,记录了整个表空间的整体属性以及那三个直属表空间的链表基节点。XDES类型:除了第一组,之后每个区组的第一个页面都是这种类型。它和 FSP_HDR 结构极其相似,但由于表空间整体属性在页 0 已经存过了,所以 XDES 页里只专门存放本组 256 个区的 XDES Entry。INODE类型:因为每个段都有一个 INODE Entry,这些 Entry 就统一集中存放在这种特殊类型的页面中。
五、 系统表空间的特殊之处
我们平时建表产生的叫独立表空间(.ibd文件),而 MySQL 还有一个全局的系统表空间(通常是 ibdata1 文件)。
系统表空间不仅存储表数据,还要统筹整个数据库的运行。它有以下非常显著的区别:
- 它的前三个页面与独立表空间一模一样(页号 0 是 FSP_HDR,页号 1 是 IBUF_BITMAP,页号 2 是 INODE)。
- 特有页面(页号 3 ~ 7):这些页面专门用于记录系统的底层属性。比如存放 Insert Buffer 信息的页、存放事务系统 (Transaction System) 信息的页,以及存放数据字典头部 (Data Dictionary Header) 信息的页。
- Doublewrite Buffer(双写缓冲区):系统表空间中极其重要的一部分。它的 extent 1 和 extent 2(也就是页号从 64 到 191 的这 128 个页面)被划分为双写缓冲区,用于在极端情况下(如断电)保证数据页刷盘的完整性。
- 数据字典 (Data Dictionary):记录表结构、列信息、索引信息的元数据。它们在底层其实是作为隐藏的内部系统表(如 SYS_TABLES, SYS_COLUMNS)存储在系统表空间里的。
跟上一篇博客对应,手撕具体页
InnoDB 表空间页解析:innodb_space -f t1.ibd -p 4 page-dump
一、实验命令
innodb_space -f t1.ibd -p 4 page-dump
含义:
| 参数 | 含义 |
|---|---|
-f t1.ibd | 解析 t1.ibd 这个独立表空间文件 |
-p 4 | 查看表空间中的第 4 号页 |
page-dump | 将该页的内部结构 dump 出来 |
InnoDB 默认页大小为 16KB,所以:
page 4 起始偏移 = 4 * 16KB = 65536 字节
二、原始输出
#<Innodb::Page::Index:0x0000776a49cbdda8>:
fil header:
#<struct Innodb::Page::FilHeader checksum=23771308, offset=4, prev=nil, next=nil, lsn=19458723, type=:INDEX, flush_lsn=0, space_id=2>
fil trailer:
#<struct Innodb::Page::FilTrailer checksum=23771308, lsn_low32=19458723>
page header:
#<struct Innodb::Page::Index::PageHeader
n_dir_slots=2,
heap_top=195,
n_heap_format=32773,
n_heap=5,
format=:compact,
garbage_offset=0,
garbage_size=0,
last_insert_offset=177,
direction=:right,
n_direction=2,
n_recs=3,
max_trx_id=0,
level=0,
index_id=153>
fseg header:
#<struct Innodb::Page::Index::FsegHeader
leaf=<Innodb::Inode space=<Innodb::Space file="./t1.ibd", page_size=16384, pages=7>, fseg=4>,
internal=<Innodb::Inode space=<Innodb::Space file="./t1.ibd", page_size=16384, pages=7>, fseg=3>>
sizes:
header 120
trailer 8
directory 4
free 16177
used 207
record 75
per record 25.00
page directory:
[99, 112]
system records:
#<struct Innodb::Page::Index::SystemRecord
offset=99,
header=#<struct Innodb::Page::Index::RecordHeader length=5, next=127, type=:infimum, heap_number=0, n_owned=1, info_flags=0, offset_size=nil, n_fields=nil, nulls=nil, lengths=nil, externs=nil>,
next=127,
data="infimum\x00",
length=8>
#<struct Innodb::Page::Index::SystemRecord
offset=112,
header=#<struct Innodb::Page::Index::RecordHeader length=5, next=112, type=:supremum, heap_number=1, n_owned=4, info_flags=0, offset_size=nil, n_fields=nil, nulls=nil, lengths=nil, externs=nil>,
next=112,
data="supremum",
length=8>
(records not dumped due to missing record describer or data dictionary)
三、整体结论
这个页可以概括为:
t1.ibd 的第 4 号页是一个 INDEX 页。
它属于 index_id = 153 的 B+ 树。
它的 level = 0,所以它是叶子页。
它有 3 条用户记录。
它同时保存了 leaf/internal 两个段的 fseg header,所以它大概率也是这棵 B+ 树的根页。
也就是说:
page 4 = root page + leaf page
这说明当前表数据量很小,整棵 B+ 树只有一个页。
最终结构可以理解为:
t1.ibd
└── page 4
└── INDEX 页
└── index_id = 153 的 B+ 树
├── internal segment: fseg = 3
└── leaf segment: fseg = 4
└── 当前页中有 3 条用户记录
四、FIL Header:页的通用头部
原始输出:
fil header:
#<struct Innodb::Page::FilHeader
checksum=23771308,
offset=4,
prev=nil,
next=nil,
lsn=19458723,
type=:INDEX,
flush_lsn=0,
space_id=2>
FIL Header 是每个 InnoDB 页面都有的通用页头。
1. checksum=23771308
checksum=23771308
这是页校验和,用来检查这个 16KB 页是否损坏。
后面的 FIL Trailer 中也有相同的 checksum:
fil trailer:
checksum=23771308
说明:
页头 checksum = 页尾 checksum
也就是:
23771308 == 23771308
这表示该页目前看起来是完整的,没有明显损坏。
2. offset=4
offset=4
表示这是表空间中的第 4 号页。
对应命令:
innodb_space -f t1.ibd -p 4 page-dump
所以:
offset=4 说明当前页就是 t1.ibd 的 page 4。
3. prev=nil, next=nil
prev=nil
next=nil
InnoDB B+ 树中,同一层的页面之间通常通过双向链表连接:
prev page <--> current page <--> next page
但是当前页:
prev=nil
next=nil
说明当前层没有前驱页,也没有后继页。
也就是说:
当前这一层只有 page 4 一个页。
再结合后面的:
level=0
可以推断:
当前 B+ 树的叶子层只有一个页。
4. lsn=19458723
lsn=19458723
LSN 是 Log Sequence Number。
它表示:
这个页最后一次被修改时,对应的 redo log 序号。
后面的 FIL Trailer 中也有:
lsn_low32=19458723
说明页头和页尾的 LSN 也能对上。
5. type=:INDEX
type=:INDEX
说明该页类型是 INDEX 页。
INDEX 页就是:
B+ 树索引页
对于 InnoDB 来说:
表数据本身存储在聚簇索引 B+ 树的叶子页中。
因此,如果这个页属于主键索引,那么它里面存储的就是整行数据。
6. flush_lsn=0
flush_lsn=0
这个字段通常只在系统表空间某些页面中有特殊意义。
在普通独立表空间的 INDEX 页里,这里为 0 很正常。
7. space_id=2
space_id=2
表示这个页属于:
space_id = 2 的表空间
也就是当前这个 t1.ibd 对应的表空间。
五、FIL Trailer:页尾校验信息
原始输出:
fil trailer:
#<struct Innodb::Page::FilTrailer checksum=23771308, lsn_low32=19458723>
FIL Trailer 是每个 InnoDB 页尾部的结构。
一个 16KB 页可以大致理解为:
+-----------------------------+
| FIL Header |
+-----------------------------+
| Page Body |
+-----------------------------+
| FIL Trailer |
+-----------------------------+
当前页头页尾信息如下:
header checksum = 23771308
trailer checksum = 23771308
header lsn = 19458723
trailer lsn_low32 = 19458723
这说明:
该页没有明显的 torn page 问题。
所谓 torn page,就是页面刷盘时只写了一部分,导致页内容不完整。
六、Page Header:INDEX 页专属头部
原始输出:
page header:
#<struct Innodb::Page::Index::PageHeader
n_dir_slots=2,
heap_top=195,
n_heap_format=32773,
n_heap=5,
format=:compact,
garbage_offset=0,
garbage_size=0,
last_insert_offset=177,
direction=:right,
n_direction=2,
n_recs=3,
max_trx_id=0,
level=0,
index_id=153>
这是 INDEX 页自己的头部信息。
1. n_dir_slots=2
n_dir_slots=2
表示 Page Directory 中有 2 个槽。
后面也能看到:
page directory:
[99, 112]
Page Directory 的作用是:
加速页内记录查找。
B+ 树查找过程可以简单理解为:
先定位到某个 page
再在 page 内部查找 record
页内不是简单从第一条记录扫到最后一条记录,而是借助 Page Directory 做近似二分查找。
当前页记录很少,所以目录槽只有 2 个。
2. heap_top=195
heap_top=195
表示当前页中的 record heap 已经使用到 offset 195 附近。
也就是说:
offset 195 之前放了系统记录和用户记录。
offset 195 之后大部分都是空闲空间。
这和后面的空间统计能对应上:
used = 207
free = 16177
说明当前页非常空。
3. n_heap_format=32773
n_heap_format=32773
这个字段可以拆开理解。
十进制 32773 转成十六进制是:
32773 = 0x8005
其中:
0x8000 表示 compact 行格式
0x0005 表示 heap 中一共有 5 条记录
所以工具进一步解析出了:
n_heap=5
format=:compact
这 5 条记录包括:
2 条系统记录:infimum、supremum
3 条用户记录
4. n_heap=5
n_heap=5
表示该页 record heap 中共有 5 条记录。
分别是:
1. infimum 系统最小记录
2. supremum 系统最大记录
3. 用户记录 1
4. 用户记录 2
5. 用户记录 3
5. format=:compact
format=:compact
说明当前页中的记录使用 Compact 行格式。
Compact 是 InnoDB 的一种行记录格式。
6. garbage_offset=0, garbage_size=0
garbage_offset=0
garbage_size=0
表示当前页没有垃圾记录。
也就是说:
这个页里没有被删除后留下的记录空间。
如果对表执行过大量 DELETE,可能会出现 garbage record。
InnoDB 删除记录时,不一定马上物理清理空间,而是先标记删除,后续由 purge 或页面整理机制处理。
当前这两个值都是 0,说明这个页比较干净。
7. last_insert_offset=177
last_insert_offset=177
表示最近一次插入的记录位于页内 offset 177 附近。
结合 n_recs=3 可以理解为:
最后一条用户记录大概在 offset 177 位置。
8. direction=:right
direction=:right
表示最近的插入方向是向右。
这通常出现在主键递增插入场景中。
例如:
INSERT INTO t1 VALUES (1, ...);
INSERT INTO t1 VALUES (2, ...);
INSERT INTO t1 VALUES (3, ...);
如果主键递增,新记录一般会插入到页内右侧,因此 InnoDB 记录为:
direction = right
9. n_direction=2
n_direction=2
表示连续朝同一个方向插入了 2 次左右。
因为当前表数据很少,所以这个值也很小。
10. n_recs=3
n_recs=3
这是非常关键的字段。
它表示:
当前页中有 3 条用户记录。
注意:
n_recs 不包括 infimum 和 supremum。
所以当前页中:
n_recs = 3
n_heap = 5
正好对应:
3 条用户记录 + 2 条系统记录 = 5 条 heap record
11. max_trx_id=0
max_trx_id=0
这个字段主要和二级索引页、MVCC、purge 等机制有关。
当前值为 0,在这个实验里暂时不用重点关注。
12. level=0
level=0
这是判断当前页是不是叶子页的关键字段。
在 InnoDB B+ 树中:
level = 0 表示叶子页
level > 0 表示非叶子页,也叫 internal page
所以当前 page 4 是:
叶子页
如果 B+ 树更大,结构可能是:
level 2: root page
level 1: internal page internal page
level 0: leaf page leaf page leaf page leaf page
而当前只有:
level 0: page 4
说明表数据很少,B+ 树还没有长高。
13. index_id=153
index_id=153
表示该页属于:
index_id = 153 的索引
每个 InnoDB 索引都有自己的 index id。
如果结合数据字典信息,可以进一步查出:
index_id = 153 对应 PRIMARY 还是某个二级索引。
如果当前表只有一个主键索引,那么它大概率就是 PRIMARY 聚簇索引。
七、FSEG Header:索引对应的两个段
原始输出:
fseg header:
#<struct Innodb::Page::Index::FsegHeader
leaf=<Innodb::Inode space=<Innodb::Space file="./t1.ibd", page_size=16384, pages=7>, fseg=4>,
internal=<Innodb::Inode space=<Innodb::Space file="./t1.ibd", page_size=16384, pages=7>, fseg=3>>
这里出现了两个 segment:
leaf fseg=4
internal fseg=3
这正好对应 InnoDB 的理论:
每个索引对应两个段:
1. 叶子节点段 leaf segment
2. 非叶子节点段 internal segment
也就是说:
当前索引的叶子页由 fseg=4 管理。
当前索引的非叶子页由 fseg=3 管理。
因为当前页同时保存了 leaf/internal 两个段的 FSEG Header,所以它很可能是这棵索引的根页。
再结合:
level=0
可以推断:
page 4 既是根页,也是叶子页。
也就是:
这棵 B+ 树目前高度为 1。
结构图:
page 4
INDEX page
level = 0
root + leaf page
contains 3 user records
如果以后数据变多,B+ 树发生页分裂,可能会变成:
root page
level = 1
/ \
leaf page leaf page
level = 0 level = 0
八、Sizes:页空间使用情况
原始输出:
sizes:
header 120
trailer 8
directory 4
free 16177
used 207
record 75
per record 25.00
一个 InnoDB 页默认是:
16KB = 16384 bytes
当前页空间使用情况:
| 项目 | 大小 | 含义 |
|---|---|---|
header | 120 bytes | 页头部 |
trailer | 8 bytes | 页尾部 |
directory | 4 bytes | 页目录 |
used | 207 bytes | 已使用空间 |
free | 16177 bytes | 空闲空间 |
record | 75 bytes | 用户记录总大小 |
per record | 25 bytes | 平均每条用户记录大小 |
当前页:
used = 207 bytes
free = 16177 bytes
说明这个页几乎还是空的。
因为:
record = 75
n_recs = 3
所以:
75 / 3 = 25 bytes
即每条用户记录平均大约 25 字节。
九、Page Directory:页目录
原始输出:
page directory:
[99, 112]
Page Directory 中有两个槽:
99
112
这两个 offset 对应下面的系统记录:
infimum offset = 99
supremum offset = 112
所以当前页目录非常简单。
Page Directory 的作用是加速页内查找。
页内整体结构可以粗略画成:
16KB INDEX page
+--------------------------------------------------+
| FIL Header |
+--------------------------------------------------+
| Page Header |
+--------------------------------------------------+
| FSEG Header |
+--------------------------------------------------+
| infimum |
+--------------------------------------------------+
| supremum |
+--------------------------------------------------+
| user record 1 |
+--------------------------------------------------+
| user record 2 |
+--------------------------------------------------+
| user record 3 |
+--------------------------------------------------+
| free space |
+--------------------------------------------------+
| page directory |
+--------------------------------------------------+
| FIL Trailer |
+--------------------------------------------------+
注意:
记录区域从页前部向后增长。
Page Directory 从页尾部附近向前增长。
中间是 free space。
十、System Records:两个系统伪记录
InnoDB 每个 INDEX 页里都有两个系统记录:
infimum
supremum
它们不是用户数据,而是页内记录链表的边界哨兵。
可以理解为:
infimum = 页内最小伪记录
supremum = 页内最大伪记录
页内记录链表大致是:
infimum -> user record 1 -> user record 2 -> user record 3 -> supremum
十一、Infimum 记录解析
原始输出:
#<struct Innodb::Page::Index::SystemRecord
offset=99,
header=#<struct Innodb::Page::Index::RecordHeader length=5, next=127, type=:infimum, heap_number=0, n_owned=1, info_flags=0, offset_size=nil, n_fields=nil, nulls=nil, lengths=nil, externs=nil>,
next=127,
data="infimum\x00",
length=8>
逐项解析:
1. offset=99
offset=99
表示 infimum 记录位于页内 offset 99。
2. type=:infimum
type=:infimum
表示这是页内最小伪记录。
3. heap_number=0
heap_number=0
表示它是 record heap 中的第 0 条记录。
4. next=127
next=127
表示 infimum 的下一条记录位于 offset 127。
这条记录应该是第一条用户记录。
因此当前页内记录链表开头是:
infimum(offset=99) -> user record 1(offset=127)
5. data="infimum\x00"
data="infimum\x00"
这是 infimum 系统记录的内容。
它不是用户表里的数据。
十二、Supremum 记录解析
原始输出:
#<struct Innodb::Page::Index::SystemRecord
offset=112,
header=#<struct Innodb::Page::Index::RecordHeader length=5, next=112, type=:supremum, heap_number=1, n_owned=4, info_flags=0, offset_size=nil, n_fields=nil, nulls=nil, lengths=nil, externs=nil>,
next=112,
data="supremum",
length=8>
逐项解析:
1. offset=112
offset=112
表示 supremum 记录位于页内 offset 112。
2. type=:supremum
type=:supremum
表示这是页内最大伪记录。
3. heap_number=1
heap_number=1
表示它是 record heap 中的第 1 条记录。
4. n_owned=4
n_owned=4
这个字段和 Page Directory 有关。
可以粗略理解为:
当前目录槽管理了若干条记录。
当前页有:
3 条用户记录 + supremum
所以这里是:
n_owned=4
5. data="supremum"
data="supremum"
这是 supremum 系统记录的内容。
它也不是用户表里的数据。
十三、为什么用户记录没有被 dump 出来?
原始输出最后一行:
(records not dumped due to missing record describer or data dictionary)
意思是:
innodb_space 知道这个页里有用户记录,
但是它不知道这些记录的列结构,
所以无法把记录解析成人能读懂的字段。
比如它可能看到一串二进制:
0x80 0x00 0x00 0x01 ...
但它不知道表结构是:
id INT PRIMARY KEY,
name VARCHAR(20)
还是:
a BIGINT,
b CHAR(10),
c VARCHAR(100)
所以它无法判断这些 bytes 分别属于哪些列。
因此只能提示:
records not dumped
要解析用户记录,需要提供 record describer 或数据字典信息。
十四、当前 Page 4 的整体结构图
t1.ibd
space_id = 2
page_size = 16KB
pages = 7
page 4
type = INDEX
index_id = 153
level = 0
n_recs = 3
n_heap = 5
+--------------------------------------------------+
| FIL Header |
| checksum = 23771308 |
| page offset = 4 |
| prev = nil |
| next = nil |
| lsn = 19458723 |
| type = INDEX |
| space_id = 2 |
+--------------------------------------------------+
| INDEX Page Header |
| n_dir_slots = 2 |
| heap_top = 195 |
| n_heap = 5 |
| format = compact |
| n_recs = 3 |
| level = 0 |
| index_id = 153 |
+--------------------------------------------------+
| FSEG Header |
| internal segment = fseg 3 |
| leaf segment = fseg 4 |
+--------------------------------------------------+
| infimum system record |
| offset = 99 |
| next = 127 |
+--------------------------------------------------+
| supremum system record |
| offset = 112 |
+--------------------------------------------------+
| user record 1 |
| offset probably = 127 |
+--------------------------------------------------+
| user record 2 |
+--------------------------------------------------+
| user record 3 |
| last_insert_offset = 177 |
+--------------------------------------------------+
| free space |
| about 16177 bytes |
+--------------------------------------------------+
| page directory |
| slots = [99, 112] |
+--------------------------------------------------+
| FIL Trailer |
| checksum = 23771308 |
| lsn_low32 = 19458723 |
+--------------------------------------------------+
十五、B+ 树视角下的解释
根据这些字段:
type = INDEX
level = 0
n_recs = 3
prev = nil
next = nil
fseg header 有 leaf/internal 两个段
可以推断出:
当前 page 4 是一棵很小的 B+ 树。
这棵 B+ 树只有一个页。
这个页既是根页,也是叶子页。
图示:
page 4
INDEX B+Tree Page
level = 0
root + leaf page
+----------------------+
| infimum |
| user record 1 |
| user record 2 |
| user record 3 |
| supremum |
+----------------------+
如果以后数据继续增多,B+ 树可能发生页分裂,变成:
root page
level = 1
/ \
leaf page leaf page
level = 0 level = 0
再继续增大,可能变成:
root page
level = 2
/ \
internal page internal page
level = 1 level = 1
/ \ / \
leaf leaf leaf leaf
level=0 level=0 level=0 level=0
十六、和“表空间、段、区、页”的理论对应
这次 dump 可以和之前学习的理论这样对应:
| 理论概念 | 当前输出中的体现 |
|---|---|
| 表空间 Tablespace | t1.ibd |
| 页 Page | page 4 |
| 页大小 | page_size=16384,即 16KB |
| INDEX 页 | type=:INDEX |
| B+ 树叶子页 | level=0 |
| 用户记录数 | n_recs=3 |
| 页内总记录数 | n_heap=5 |
| 系统记录 | infimum、supremum |
| 页目录 | page directory: [99, 112] |
| 叶子段 | leaf fseg=4 |
| 非叶子段 | internal fseg=3 |
| 索引 ID | index_id=153 |
| 表空间 ID | space_id=2 |
十七、推理链
type=:INDEX
↓
这是 B+ 树索引页
level=0
↓
这是根节点
n_recs=3
↓
有 3 条用户记录
n_heap=5
↓
3 条用户记录 + infimum + supremum
prev=nil, next=nil
↓
这一层只有一个页
fseg header 同时出现 leaf/internal segment
↓
这是这棵索引的根页
所以:
page 4 是一棵很小的 B+ 树的 root leaf page
十八、最终总结
t1.ibd 的 page 4 是索引页。
它不是 FSP_HDR 页,不是 INODE 页,也不是 XDES 页,而是:
INDEX 页,也就是 B+ 树页。
它的关键信息如下:
space_id = 2
page_no = 4
type = INDEX
index_id = 153
level = 0
n_recs = 3
n_heap = 5
format = compact
prev = nil
next = nil
leaf segment = fseg 4
internal segment = fseg 3
因此可以得出结论:
page 4 是 index_id=153 这棵 B+ 树的根页。
这个页里现在有 3 条用户记录(索引页)。
因为 prev/next 都是 nil,所以当前叶子层只有这一个页。
由于页里有 fseg header,可以看到该索引的两个段:
internal segment = fseg 3
leaf segment = fseg 4