自己分析一下ibd文件还是蛮有意思的,能够学到不少东西,建议跟着走一遍,慢慢领会作者设计的意图
人学东西总是先感性的认识,慢慢到理性 —— 过程中大脑需要理解和消化
mysql版本5.7.26
先贴一张数据页的结构图-方便对整体有个印象
利用工具查看数据库文件ibd的页分布情况 (工具名py_innodb_page_info)
当你熟悉page的格式后,自己也能写一个这样的工具
- page offset 00000003, page type
, page level <0001> - page offset 00000004, page type
, page level <0000> - page offset 00000005, page type
, page level <0000> - page offset 00000006, page type
, page level <0000>
以上四个都是数据页
我们分析 page offset 00000006, page type, page level <0000>
page level <0000> 表示的是叶子节点
使用十六进制工具Synalyze It! 打开/usr/local/mysql/data/nishui/test.ibd 文件(文件权限问题此处忽略,二进制工具任意)
找到 00000006 页在文件中的位置
因为看的是00000006数据页, 用6 * 16 * 1024 = 98304B (innodb引擎默认一页16KB(可通过innodb_page_size改变页大小),然而1KB= 1024B) 转换成十六进制为 0x18000
show global status like 'Innodb_page_size' 可查看当前页大小
找到该位置截图如下:
这个页便是 00000006 数据页开始的位置了, 可以开始分析详细数据了
一、先分析File Header(38字节-描述页信息)
- 72 08 C8 7F -> 数据页的checksum值
- 00 00 00 06 -> 页号(偏移量)
- 00 00 00 05 -> 前一页是第5页
- FF FF FF FF -> 由于没有下一页,因此为该值
- 00 00 00 00 00 38 23 77 -> 页的LSN
- 45 BF -> 页的类型 0x45BF代表数据页,刚好这页是数据页
- 00 00 00 00 00 00 00 00 -> 独立表空间,该值为0
- 00 00 00 5B -> 表空间的SPACE ID
二、分析Page Header(56字节-记录页的状态信息)
SHOW TABLE STATUS LIKE 'test'查看表行记录格式
- 00 07 -> 代表Page Directory 有7个槽
-
17 27 -> 代表空闲空间开始位置的偏移量,即 0x18000 + 0x1727 = 0x19727,观察这个位置,这是最后一行的结束,后面都是空闲的
- 80 1D -> 当前为 Compact 格式,第15位表示行记录格式,再加上两条伪记录, 因此0x801D - 0x8002 = 0x001B,代表该页中实际的记录有27条记录
- 00 00 -> 指向页中空闲位置(偏移量)
- 00 00 -> PAGE_GARBAGE 表示没有删除的数据
-
16 4A -> PAGE_LAST_INSERT 最后插入记录的位置偏移 0x18000 + 0x164A = 0x1964A 直接指向最后一行数据存储的地址,也就是id为199,这条确实是最后一条插入的
- 00 02 -> PAGE_DIRECTION 最后插入的方向,向右边插入
- 00 1A -> PAGE_N_DIRECTION 一个方向连续插入记录的数量 连续插入26个
- 00 1B -> PAGE_N_RECS 当前数据页中含有27条记录
- 00 00 00 00 00 00 00 00 修改当前页的最大事务ID
- 00 00 -> 代表页为叶子节点
- 00 00 00 00 00 00 00 43 -> 索引ID,表示当前页属于哪个索引
- 00 00 00 42 00 00 00 02 00 F2 -> B+树数据页非叶子节点所在段的segment header。注意该值仅在树的root页中定义
- 00 00 00 42 00 00 00 02 00 32 ->B+树数据页所在段的segment header。
小结一下
1.innodb在整个页可以使用的空间当成heap,当需要插入记录的时候,首先会检查PAGE_FREE指向的空闲空间,若申请的空间小于等于该空间容量时,那么使用该空闲空间,否者从PAGE_HEAP_TOP指向的空闲空间进行分配
heap中存储的记录非物理连续的,只是逻辑上连续的,可用下图表示
2.PAGE_LAST_INSERT、PAGE_DIRECTION、PAGE_N_DIRECTION主要使用来做页分裂操作的
三、伪记录分析Infimum + Supremum Record (26字节-两个虚拟行记录)
innodb存储引擎有两个伪记录,用来界定行记录的边界
数据从 0x1805E 到 0x18077
- 01 00 02 00 1E -> recorder header (5字节)
- 69 6E 66 69 6D 75 6D 00 -> 只有一个列的伪记录,记录内容就是Infimum(多了一个 0x00 字节) (8字节)
- 08 00 0B 00 00 -> recorder header (5字节)
- 73 75 70 72 65 6D 75 6D -> 只有一个列的伪记录,记录内容就是Supremum (8字节)
分析下伪记录中的recorder header中的next_record
recorder header最后两个字节 0x001E,表示下一个记录位置的偏移量,即当前行记录“内容”的位置0x18063 + 0x001E,即0x18081,这个位置就是存放第一条实际用户记录
四、分析User Record
当前 Row Format 为Compact格式 可通过命令show table status like 'table_name' 进行查看
CREATE TABLE `test` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`t1` varchar(10) DEFAULT NULL,
`t2` varchar(15) DEFAULT NULL,
`t3` int(11) DEFAULT NULL,
`t4` varchar(1500) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=200 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
创建数据的脚本
CREATE DEFINER=`root`@`localhost` PROCEDURE `insert_test`( )
BEGIN
#Routine body goes here...
declare i int;
declare tmp int;
set i=1;
set tmp = 1;
while i<200 do
if tmp=1 then
insert into test (t1, t4) values('a', REPEAT('a', i));
set tmp = 0;
else
insert into test (t1, t3, t4) values('a', i, REPEAT('a', i));
set tmp = 1;
end if;
set i=i+1;
end while;
END
由上面伪记录Infimum的Record Header可知下一条记录的开始地址是0x18081,顺便把前面的extra info也分析下,截图如下
- AD 80 01 -> 变长字段长度列表, 逆序,第一列是1字节,第四列是2字节,所以第一列包含1个字符,第四列包含173个 字符80 AD存的是补码,换算成原码为0x00AD,转换成10进制就是173
-
02 -> 二进制(00000010)表示第二个字段为null
- 00 00 10 00 CC -> Record Header 固定5字节长度
- 00 00 00 AD -> 由于是自动创建的int自增id ,固是4个字节,当前行记录id为173, 由于该id是无符号的,所以最高位不是符号位
- 00 00 00 00 69 E7 -> TransactionId
- D7 00 00 01 5C 01 10 -> Roll Pointer
- 61 -> 第一列字段数据 a
- 80 00 00 AD -> 第二列,存的是补码,因此原码为0xAD,故值为173
- 第三列为null,不占用空间
- 61 .... 61 -> 第四列字段数据 a ... a 173个 省略
分析User Record中Record Header中的内容
0x 00 00 10 00 CC 转换成十进制如下 00000000 00000000 00010000 00000000 11001100
下面都是二进制的,其他的都是十六进制的
- 0 -> 预留位1
- 0 -> 预留位2
- 0 -> delete_mask 标记该记录是否删除,0表示没有删除 说明删除的数据很可能还在页中,并且占用着空间
- 0 -> min_rec_mask 标记该记录是否为B+树的非叶子节点中的最小记录
- 0000 -> n_owned 表示当前槽管理的记录数
- 00000000 00010 -> heap_no 表示当前记录在记录堆的位置信息,这个值表示当前记录在heap中的位置为2
- 000 -> record_type 表示当前记录的类型,0表示普通记录
-
00000000 11001100 -> next_record 表示下一条记录的相对位置,转换16进制为0xCC,0x18081 + 0xCC = 0x1814D,下一条记录的值地址为0x1814D,截图如下
简单用图可表示如下(忽略实际内容):
五、分析Page Directory
这一页的末尾是0x1BFFF,并且加上Page Header中PAGE_N_DIR_SLOTS,能够知道Page Directory中包含了7个slot 截图如下
位置是从 0x1BFEA - 0x1BFF7,一共14个字节,因此展开如下:
- 00 70 -> supremum记录所在行偏移量地址
- 10 2C -> id为192的行偏移量地址
- 0C C2 -> id为188的行偏移量地址
- 09 68 -> id为184的行偏移量地址
- 06 1E -> id为180的行偏移量地址
- 02 E4 -> id为176的行偏移量地址
- 00 63 -> infimum记录所在行偏移量地址
六、分析File Tailer
固定占用8个字节,并且是在页尾部,可以直接得出位置为0x1BFF8 开始的
- 72 08 C8 7F -> Old-style Checksum
- 00 38 23 77 -> Low 32 bit of LSN
为了保证页能够完整地写入磁盘(如可能发生的写入过程中磁盘损坏、机器宕机等原因),InnoDB存储引擎的页中设置了File Trailer部分。File Trailer只有一个FIL_PAGE_END_LSN部分,占用8个字节。前4个字节代表该页的checksum值,最后4个字节和File Header中的FIL_PAGE_LSN相同。通过这两个值来和File Header中的FIL_PAGE_SPACE_OR_CHKSUM和FIL_PAGE_LSN值进行比较,看是否一致(checksum的比较需要通过InnoDB的checksum函数来进行比较,不是简单的等值比较),以此来保证页的完整性(not corrupted)。
数据页格式
File Header
名称 | 大小(字节) | 说明 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4 | 当mysql为4.0.14之前的版本时,该值为0。在之后的mysql版本中,该值代表页的checksum值(一种新的checksum值) |
Fil_PAGE_OFFSET | 4 | 表空间中页的偏移值,如果独立表空间a.ibd的大小为1GB,如果页的大小为16kb,那么总共有65536个页.FIL_PAGE_OFFSET表示该页在所有页中的位置。若此表空间的ID为10,那么搜索页(10, 1)就表示在表a中的第二页 |
FIL_PAGE_PREV | 4 | 当前页的上一页,B+树的特性就决定了叶子节点必须是双向列表 |
FIL_PAGE_NEXT | 4 | 当前页的下一页 |
FIL_PAGE_LSN | 8 | 该值代表页最后被修改的日志序列位置LSN |
FIL_PAGE_TYPE | 2 | INNODB 存储页的类型, |
FIL_PAGE_FILE_FLUSH_LSN | 8 | 该值仅在系统表空间的一个页中定义,代表文件至少更新到了LSN值。对于独立表空间,该值为0 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4 | 从mysql4.1开始,该值代表属于哪个表空间 |
Innodb存储引用中页的类型
名称 | 十六进制 | 解释 |
---|---|---|
FIL_PAGE_INDEX | 0x45BF | B+树叶节点 |
FIL_PAGE_UNDO_LOG | 0x0002 | undo log页 |
FIL_PAGE_INODE | 0x0003 | 索引节点 |
FIL_PAGE_IBUF_FREE_LIST | 0x0004 | insert buffer空闲列表 |
FIL_PAGE_TYPE_ALLOCATED | 0x0000 | 该页为最新分配页 |
FIL_PAGE_IBUF_BITMAP | 0x0005 | insert buffer 位图 |
FIL_PAGE_TYPE_SYS | 0x0006 | 系统页 |
FIL_PAGE_TYPE_TRX_SYS | 0x0007 | 事务系统数据 |
FIL_PAGE_TYPE_FSP_HDR | 0x0008 | File space Header |
FIL_PAGE_TYPE_XDES | 0x0009 | 扩展描述页 |
FIL_PAGE_TYPE_BLOB | 0x000A | BLOB页 |
Page Header
用来数据页的状态信息,14部分组成,共56字节
名称 | 大小(字节) | 说明 |
---|---|---|
PAGE_N_DIR_SLOTS | 2 | 在Page Directory (页目录〉中 的Slot (槽〉 数,“4.4.S Page Directory” 小节中会介绍 |
PAGE HEAP TOP | 2 | 堆中第一个记录的指针, 记录在页中是根据堆 的形式存放的 堆中空闲空间的位置(偏移量) |
PAGE N HEAP | 2 | 堆中的记录数. 一共占用2 字节, 但是第15 位表示行记录格式 (包括最小和最大记录以及标记为删除的记录) |
PAGE FREE | 2 | 指向可重用空间的首指针 指向页中空闲空间的位置(偏移量)(就是标记为删除的记录地址) |
PAGE GARBAGE | 2 | 己删除记录的字节 数, 即行记录结构中也阳也在为1的记录大小的总数 |
PAGE LAST INSERT | 2 | 最后捕入记录的位置(偏移量) |
PAGE DIRECTION | 2 | 最后插入的方向. 可能的取值为2 。 1.PAGE LEFT (0x01) 2.PAGE RIGHT (Ox02) 3.PAGE SAME REC (Ox03) 4. PAGE SAME PAGE (Ox04) |
PAGE N DIRECTION | 2 | 一个方向连续插入记录的数量 |
PAGE N RECS | 2 | 该页中记录的数量 |
AGE MAX TRX ID | 8 | 修改当前页 的最大事务ID, 注意该值仅在Secondary Index中定义 |
PAGE LEVEL | 2 | 当前页 在索引树中的位置, OxOO代表叶节点, l!P时节 J点总是在第0层 |
PAGE INDEX ID | 8 | 索引ID, 表示当前页属于哪个索引 |
PAGE BTR SEG LEAF | 10 | B+树数据页非页节点所在段的segment header。 注意该值仅在 B+ 树的 Root 页中定义 |
PAGE BTR SEG TOP | 10 | B+树数据页所在段的 segment header. 注意该值仅在 B+树的 Root 页中定义 |
COMPACT行记录格式
名称 | 大小(bit) | 描述 |
---|---|---|
预留位1 | 1 | 没有使用 |
预留位2 | 1 | 没有使用 |
delete_mask | 1 | 标记该记录是否被删除 |
min_rec_mask | 1 | 标记该记录是否为B+树的非叶子节点中的最小记录 |
n_owned | 4 | 表示当前槽管理的记录数 |
heap_no | 13 | 表示当前记录在记录堆的位置信息 |
record_type | 3 | 表示当前记录的类型,0表示普通记录,1表示B+树非叶节点记录,2表示最小记录,3表示最大记录 |
next_record | 16 | 表示下一条记录的相对位置 |
Total | 40(Byte) | nothing |
参考借鉴:
InnoDB数据页结构
MYSQL内核:INNODB存储引擎 卷1-4
MySQL技术内幕InnoDB存储引擎第2版