1. 前言
一提到innodb大多数都知道它是个B+Tree存储结构,但是对于想深入了解innodb存储引擎的小伙伴来说远远不够,我也是其中之一。于是带着疑问和思考上路来进行学习和分析,既然是数据库存储引擎,就需要有一个比较可视化的工具来帮助我们理解和拆解复杂的存储文件信息等,最终我找到了innodb_ruby
一个基于ruby语言开发且开源的innodb存储引擎解析工具,下面我们通过它来一层层揭开神秘面纱探究清楚innodb底层的实现原理。我们知道mysql中有诸如参数配置、日志、表结构、存储引擎等很多文件,这里主要围绕innodb存储引擎相关的文件进行展开和剖析。
2. MySQL基础知识
2.1 表结构定义
首先,我们需要了解MySQL的存储引擎是插件式的,数据的存储是根据表来进行组织的,每个表都会有与之对应的文件。但不论表采用何种存储引擎,MySQL都会有一个frm
为后缀的文件,这个文件就是表结构定义。
frm还用来存放视图的定义,当创建了一个视图的时候,会创建形如view.frm
文件,由于它是文本文件,可以直接使用cat
命令查看定义情况
2.2 InnoDB存储引擎文件
一般来说,数据都会拆分较为合理的分层,以方便组织和维护数据存储及使用,对于InnoDB来说也是一样的,下面由宏观视角到局部组成层层递进,从表空间(tablespace)、段(segment)、区(extent)、页(page)、行(row)
来一探究竟。
表空间(tablespace)
表空间,作为InnoDB存储引擎逻辑结构的最高层,所有
InnoDB存储引擎采用表空间(tablespace)
的方式对数据进行存储,默认情况下会有一个ibdata1
的文件,这就是默认的表空间文件。
- 关于
ibdata1
的文件命名,猜测是 innodb data 1 是缩写,因为表空间可以通过参数innodb_data_file_path
来设置,用户是可以通过多个文件组成一个表空间的,这里是默认一个。适当的配置表空间来负载存储对于性能是有提升的,感兴趣可以自行了解,不在本文主要范畴内不展开。 - 可以通过
show variables like ‘%xxxx%’
命令来查看innodb的配置参数,innodb_data_file_path
可用来指定表空间文件存放路径,innodb_file_per_table
用来指定是否每个表一个表空间,这里默认是开启的,所以用户基于innodb存储引擎的表都会有一个名为表名.ibd
独立的表空间,而不需要存放在默认表空间中,更加清晰和便于后续维护,对于存储和查询性能来说也是较为友好的。 - 一般用户表空间,每张表都会有两个文件结对出现:
表名.frm
表示表结构,它是MySQL范畴的,和存储引擎无关表名.ibd
表示表空间,它是Innodb独有的,用来存储该表的数据、索引、和插入缓冲BITMAP等信息,其余信息如回滚(undo)信息、插入缓存冲索引页、系统事务信息、二次写缓冲(Double write buffer)等还是存放在默认的表空间(或者可以叫做共享表空间),因此共享表空间由于需要存储和管理这些数据也是会不断增加的
段(segment)
表空间是由各个段组成的,常见的段有数据段
、索引段
、回滚段
等
区(extent)
区是由连续页组成的空间,每个区的大小为1MB
,为了保证区中页的连续性,InnoDB存储引擎每次会从磁盘申请4~5个区。InnoDB存储引擎中页的大小为16KB,即一个区中一共有64个连续的页。
- 区大小:1MB = 1024KB
页大小:16KB
1024KB / 16KB = 64个 - 创建的表默认大小为96KB,先用32个页大小的碎片页存放数据,使用完之后才会申请64个连续页。目的是对于小表,或者是undo类型的段可以申请较少空间,节省磁盘容量
页(page)
InnoDB同大多数数据库一样,也有页(Page)的概念,它是InnoDB磁盘管理的最小单位。默认每个页的大小是16KB。
以下来源于《MySQL技术内幕 - InnoDB存储引擎 第2版》
页的类型 |
解释 |
FIL_PAGE_INDEX |
B+ 树叶节点 |
FIL_PAGE_UNDO_LOG |
undo log 页 |
FIL_PAGE_INODE |
索引节点 |
FIL_PAGE_IBUF_FREE_LIST |
Insert Buffer 空闲列表 |
FIL_PAGE_TYPE_ALLOCATED |
该页为最新分配 |
FIL_PAGE_IBUF_BITMAP |
Insert Buffer位图 |
FIL_PAGE_TYPE_SYS |
系统页 |
FIL_PAGE_TYPE_TRX_SYS |
事务系统数据 |
FIL_PAGE_TYPE_FSP_HDR |
File Space Header |
FIL_PAGE_TYPE_XDES |
扩展描述页 |
FIL_PAGE_TYPE_BLOB |
BLOB页 |
行(row)
InnoDB是行存储数据库(row-oriented),每个页存放的行记录有硬性要求,最多允许存放16KB / 2-200行的记录,即7992行记录。
2.3 InnoDB数据页结构
这里先列举下最全的InnoDB数据页结构信息,其实InnoDB数据页结构组成会根据Page的类型差异化的进行组织的,不同Page类型内的子结构构成不一样,但是一般来说,无论何种类型的页,都一定包含File Header
、File Trailer
这两种子结构,其他的根据页类型有所差异。
以下来源于《MySQL技术内幕 - InnoDB存储引擎 第2版》
主结构 |
大小 (字节) |
子结构 |
大小 (字节) |
说明 |
File Header(文件头) |
38 |
FIL_PAGE_SPACE_OR_CHKSUM |
4 |
该值代表页的 值 |
FIL_PAGE_OFFSET |
4 |
表空间中 。 |
||
FIL_PAGE_PREV |
4 |
当前页的 ,B+Tree特性决定了叶子结点必须是双向列表 |
||
FIL_PAGE_NEXT |
4 |
当前页的 ,B+Tree特性决定了叶子结点必须是双向列表 |
||
FIL_PAGE_LSN |
8 |
代表该页最后被修改的日志序列位置 |
||
FIL_PAGE_TYPE |
2 |
InnoDB存储引擎 |
||
FIL_PAGE_FILE_FLUSH_LSB |
8 |
如果页存放在系统共享表空间,表示文件至少被更新到了该LSN值;对于独立表空间,都为0 |
||
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID |
4 |
代表页属于哪个表空间 |
||
Page Header(页头) |
56 |
PAGE_N_DIR_SLOTS |
2 |
在Page Directory页目录中的Slot槽数 |
PAGE_HEAP_TOP |
2 |
堆中第一个记录的指针,记录在页中是根据堆的形式存放的 |
||
PAGE_N_HEAP |
2 |
堆中的记录数。一共占用2字节,但是第15位表示行记录格式 |
||
PAGE_FREE |
2 |
指向可重用空间的首指针 |
||
PAGE_GARBAGE |
2 |
已删除记录的字节数,即行记录结构中delete flag为1的记录大小的总数 |
||
PAGE_LAST_INSERT |
2 |
最后插入记录的位置 |
||
PAGE_DIRECTION |
2 |
最后插入的方向 |
||
PAGE_N_PAGE |
2 |
一个方向连续插入记录的数量 |
||
PAGE_N_RECS |
2 |
该页中记录的数量 |
||
PAGE_MAX_TRX_ID |
8 |
修改当前页面的最大事务ID,注意该值仅在Secondary Index中定义 |
||
PAGE_LEVEL |
2 |
当前页在索引树中的位置,0x00代表叶节点,叶节点总是在第0层 |
||
PAGE_INDEX_ID |
8 |
索引ID,表示当前页属于哪个索引 |
||
PAGE_BTR_SEG_LEAF |
10 |
B+树数据页非叶子结点所在段的segment header。注意该值仅在B+树的Root页中定义 |
||
PAGE_BTR_SEG_TOP |
8 |
B+树数据页所在段的segment header。注意该值仅在B+树的Root页中定义 |
||
Infimum 和 Supremum Records |
动态 |
- |
- |
虚拟的行记录,用来限定记录的边界。Infimum比该页中任何主键值都要小,Supremum比任何可能大的值还要大。 |
User Records (用户记录,即行记录) |
动态 |
- |
- |
实际存储行记录,B+树索引组织的 |
Free Space(空闲空间) |
动态 |
- |
- |
空闲空间,链表数据结构,当一条记录被删除后,该空间会被加入到空闲链表中 |
Page Directory(页目录) |
动态 |
- |
- |
稀疏目录,记录数据的相对位置,一个槽(Slots)可能拥有多条记录,二叉查找的结果只是一个粗略的结果。 |
File Trailer(文件结尾信息) |
8 |
FIL_PAGE_END_LSN |
8 |
检测页是否完整写入磁盘。前4字节代表页的checksum值,最后4字节和File Header中的FIL_PAGE_LSN相同。checksum的默认算法是crc32 |
2.4 聚簇索引
谈论完表空间,再深入到表空间里面来看下数据在表中具体是如何组织和存放的。InnoDB存储引擎中,表中的数据都是根据主键(Primary Key)
顺序组织存放的,如果用户在创建表时没有指定主键,InnoDB存储引擎会按照以下方式来构建主键:
- ① 判断表中是否存在满足
非空的唯一索引(Unique Not Null)
条件的列,如果有则以此为主键;多个满足选择定义顺序的第一个 - ② 否则自动创建一个6字节大小的指针
通过 select _rowid
from [table] 可以查询到表的主键值
3. 环境准备
- Innodb_ruby源码地址:https://github.com/jeremycole/innodb_ruby
- innodb_ruby学习手册:https://github.com/jeremycole/innodb_ruby/wiki#page-dump
- innodb文件视图汇总:https://github.com/jeremycole/innodb_diagrams
- 下载ruby:mac下安装需要依赖ruby,其他安装参考https://github.com/jeremycole/innodb_ruby/wiki#page-dump
brew install ruby
环境安装完毕后,创建一张用户数据表用来学习和测试验证innodb的数据存储和相关特性
CREATE TABLE `user_info`
(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID',
`user_name` varchar(32) NOT NULL COMMENT '用户姓名',
`age` int(3) NOT NULL COMMENT '用户年龄',
`sex` char(1) NOT NULL COMMENT '用户性别',
`address` varchar(128) NOT NULL COMMENT '用户住址',
`phone` varchar(15) NOT NULL COMMENT '联系电话',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_id` (`user_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8 COMMENT ='用户信息表'
4. innodb ruby命令使用
通过 show variables like ‘datadir’
查看mysql数据文件存放目录,比如我本机在 /usr/local/mysql/data/
下,直接进入,然后就可以执行innodb ruby相关命令了
4.1 空间文件结构(Space File Structure)
4.1.1 列出全部表空间信息(system-spaces)
innodb_space -s ibdata1 system-spaces
name |
pages |
indexes |
(system) |
4864 |
7 |
mysql/engine_cost |
6 |
1 |
mysql/gtid_executed |
6 |
1 |
mysql/help_category |
7 |
2 |
… … |
… … |
… … |
test/user_info |
8 |
3 |
上面说过ibdata1
是innodb的系统表空间,是共享的,因此能看到这里包含了一些系统自身的表,也包含用户自定义的表信息,下面主要聚焦定义好的用户表信息来展开探究。
4.1.2 列出某个表的索引信息(space-indexes)
innodb_space -s ibdata1 -T test/user_info space-indexes
id |
name |
root |
fseg |
fseg_id |
used |
allocated |
fill_factor |
184 |
PRIMARY |
3 |
internal |
1 |
1 |
1 |
100.00% |
184 |
PRIMARY |
3 |
leaf |
2 |
0 |
0 |
0.00% |
185 |
uk_user_id |
4 |
internal |
3 |
1 |
1 |
100.00% |
185 |
uk_user_id |
4 |
leaf |
4 |
0 |
0 |
0.00% |
186 |
idx_create_time |
5 |
internal |
5 |
1 |
1 |
100.00% |
186 |
idx_create_time |
5 |
leaf |
6 |
0 |
0 |
0.00% |
这里可以看到有三个索引:PRIMARY 主键
uk_user_id 唯一索引
idx_create_time 普通索引
每个索引都有两条记录
4.1.3 列出页面类型区域(space-page-type-regions)
列出表空间中页的类型以及分布,这里我们来看下测试表 user_info
innodb_space -s ibdata1 -T test/user_info space-page-type-regions
start |
end |
count |
type |
0 |
0 |
1 |
FSP_HDR |
1 |
1 |
1 |
IBUF_BITMAP |
2 |
2 |
1 |
INODE |
3 |
5 |
3 |
INDEX |
6 |
7 |
2 |
FREE (ALLOCATED) |
可以看到,在测试表user_info中包含了不同类型的页(Page),下面来逐一说明各类型页的作用:
- FSP_HDR
每一个表空间的第一个页为FSP_HDR(file space header)
页,这个页保存了FSP header结构,这个结构保存了这个表空间的大小,以及完全没有被使用的extents,fragment的以及inode使用情况等等。 - IBUF_BITMAP
这个页就是插入缓存bitmap页,用于记录插入缓冲区的一些信息。主要用于跟踪随后的每个page的change buffer信息,使用4个bit来描述每个page的change buffer信息。 - INODE
该页用一个链表存储表空间中所有段(file segments);之前说段是由若干个extents组成,其实段除了extents之外,还有32个单独分配的"碎片"页组成,因为有些段可能用不到一个区,所以这里主要是为了节省空间。
4.1.4 列出页面类型摘要(space-page-type-summary)
列出表空间中页(Page)类型摘要信息
innodb_space -s ibdata1 -T test/user_info space-page-type-summary
type |
count |
percent |
description |
INDEX |
3 |
37.50 |
B+Tree index |
ALLOCATED |
2 |
25.00 |
Freshly allocated |
FSP_HDR |
1 |
12.50 |
File space header |
IBUF_BITMAP |
1 |
12.50 |
Insert buffer bitmap |
INODE |
1 |
12.50 |
File segment inode |
这里看到的信息和1.3中展示的基本类似,更多的是空间占比的展示。这里的INDEX
有数量是3,分别对应我们创建的三个索引:PRIMARY 主键
uk_user_id 唯一索引
idx_create_time 普通索引
4.1.5 表空间的区域快照(space-extents-illustrate)
innodb_space -s ibdata1 -T test/user_info space-extents-illustrate
Page Type |
Pages |
Ratio |
█ System |
3 |
37.50% |
█ Index 184 (test/user_info.PRIMARY) |
1 |
12.50% |
█ Index 185 (test/user_info.uk_user_id) |
1 |
12.50% |
█ Index 186 (test/user_info.idx_create_time) |
1 |
12.50% |
░ Free space |
2 |
25.00% |
这里的数据比较有意思,对比1.4对比看。System包含FSP_HDR
、IBUF_BITMAP
、INODE
,其他一致。
4.1.6 LSN空间年龄说明(space-lsn-age-illustrate)
innodb_space -s ibdata1 -T test/film space-lsn-age-illustrate
4.2. 页面结构(Page Structure)
4.2.1 查看页的概述情况
对于大多数的InnoDB中的页面类型都可以解析其结构内容
我们可以使用1.3已经获取到的表空间Page索引位来进行查看每一个页的情况,这里直接列出表空间中的索引位(从0开始)和页类型
index |
Page Type |
0 |
FSP_HDR |
1 |
IBUF_BITMAP |
2 |
INODE |
3、4、5 |
INDEX |
6、7 |
FREE (ALLOCATED) |
FSP_HDR
页的信息
innodb_space -s ibdata1 -T test/user_info -p 0 page-account
Accounting for page 0:
Page type is FSP_HDR (File space header, header page (page 0) for each tablespace file).
Extent descriptor for pages 0-63 is at page 0, offset 158.
Extent is not fully allocated to an fseg; may be a fragment extent.
Page is marked as used in extent descriptor.
Extent is in free_frag list of space.
IBUF_BITMAP
页的信息
innodb_space -s ibdata1 -T test/user_info -p 1 page-account
Accounting for page 1:
Page type is IBUF_BITMAP (Insert buffer bitmap, bookkeeping for insert buffer writes to be merged).
Extent descriptor for pages 0-63 is at page 0, offset 158.
Extent is not fully allocated to an fseg; may be a fragment extent.
Page is marked as used in extent descriptor.
Extent is in free_frag list of space.
INODE
页的信息
innodb_space -s ibdata1 -T test/user_info -p 2 page-account
Accounting for page 2:
Page type is INODE(File segment inode, bookkeeping for file segments).
Extent descriptor for pages 0-63 is at page 0, offset 158.
Extent is not fully allocated to an fseg; may be a fragment extent.
Page is marked as used in extent descriptor.
Extent is in free_frag list of space.
INDEX
页的信息,一共有三个,分别来看下,首先是PRIMARY 主键
innodb_space -s ibdata1 -T test/user_info -p 3 page-account
Accounting for page 3:
Page type is INDEX (B+Tree index, table and index data stored in B+Tree structure).
Extent descriptor for pages 0-63 is at page 0, offset 158.
Extent is not fully allocated to an fseg; may be a fragment extent.
Page is marked as used in extent descriptor.
Extent is in free_frag list of space.
Page is in fragment array of fseg 1.
Fseg is in internal fseg of index 184.
Index root is page 3.
Index is test/user_info.PRIMARY.
第二个索引是uk_user_id 唯一索引
innodb_space -s ibdata1 -T test/user_info -p 4 page-account
Accounting for page 4:
Page type is INDEX (B+Tree index, table and index data stored in B+Tree structure).
Extent descriptor for pages 0-63 is at page 0, offset 158.
Extent is not fully allocated to an fseg; may be a fragment extent.
Page is marked as used in extent descriptor.
Extent is in free_frag list of space.
Page is in fragment array of fseg 3.
Fseg is in internal fseg of index 185.
Index root is page 4.
Index is test/user_info.uk_user_id.
第三个索引是 idx_create_time 普通索引
innodb_space -s ibdata1 -T test/user_info -p 5 page-account
Accounting for page 5:
Page type is INDEX (B+Tree index, table and index data stored in B+Tree structure).
Extent descriptor for pages 0-63 is at page 0, offset 158.
Extent is not fully allocated to an fseg; may be a fragment extent.
Page is marked as used in extent descriptor.
Extent is in free_frag list of space.
Page is in fragment array of fseg 5.
Fseg is in internal fseg of index 186.
Index root is page 5.
Index is test/user_info.idx_create_time.
最后是6,7两个页,都是FREE (ALLOCATED)
类型
innodb_space -s ibdata1 -T test/user_info -p 6 page-account
Accounting for page 6:
Page type isALLOCATED
(Freshly allocated, page type field has not been initialized).
Extent descriptor for pages 0-63 is at page 0, offset 158.
Extent is not fully allocated to an fseg; may be a fragment extent.
Page is marked as free in extent descriptor.
Extent is in free_frag list of space.
4.2.2 转储页面内容
对于大多数的InnoDB中的页面类型都可以解析其结构内容
innodb_space -s ibdata1 -T test/user_info -p 3 page-dump
这里输出内容较多,可自行尝试
4.2.3 汇总页面内的所有记录
值得注意的是,这个命令只能对存储了数据行的索引
页数据进行使用并进行解析,我们事先插入一条数据,如下:
insert into user_info (user_id, user_name, age, sex, address, phone, create_time, update_time)
values ('1','user001','20','1','北京市海淀区中关村',13800001122,now(),now())
下面先对PRIMARY 主键
页数据进行解析
innodb_space -s ibdata1 -T test/user_info -p 3 page-records
Record 130: (id=1) → (
user_id=1,
user_name=“user001”,
age=20,
sex=“1”, address=“\xE5\x8C\x97\xE4\xBA\xAC\xE5\xB8\x82\xE6\xB5\xB7\xE6\xB7\x80\xE5\x8C\xBA\xE4\xB8\xAD\xE5\x85\xB3\xE6\x9D\x91”,
phone=“13800001122”,
create_time=“2022-06-03 13:39:57”,
update_time=“2022-06-03 13:39:57”
)
可以看到节点数据存储的是 id = 1,而且行数据信息明细是挂载在主键上的,这也就是常说主键是一种聚簇索引
的原因。
接下来,对uk_user_id 唯一索引
页数据解析
innodb_space -s ibdata1 -T test/user_info -p 4 page-records
Record 125: (user_id=1) → (id=1)
可以看到节点数据存储的是 user_id = 1,而且还存储了关联的数据行的主键
值 id = 1。解析完数据后可以关联下索引覆盖
、回表查询
等概念,数据行存储在聚簇索引
上,因此通过主键查询是效率较高的,如果是使用主键之外的索引进行查询的话,如果查询的数据正好在索引上,那么它的查询效率和通过主键进行查询没有太大差异,都是基于B+Tree,因此效率也是较高的。如果查询的数据是通过非主键盘索引完成检索且数据不完全在当前索引上需要通过主键指针回溯到主键索引上进行全部数据的返回,也就是回表操作。
最后,对idx_create_time 普通索引
页数据解析,同上。
innodb_space -s ibdata1 -T test/user_info -p 5 page-records
Record 125: (create_time=“2022-06-03 13:39:57”) → (id=1)
4.2.4 转储页目录的内容
同样,这个命令也是只能对索引
类型页进行使用和解析
下面先对PRIMARY 主键
页数据进行解析
innodb_space -s ibdata1 -T test/user_info -p 3 page-directory-summary
slot |
offset |
type |
owned |
key |
0 |
99 |
infimum |
1 |
|
1 |
112 |
supremum |
2 |
4.2.5 展示页面内容数据分布
我们还是使用1.3已经获取到的表空间Page索引位来进行查看每一个页的情况,这里直接列出表空间中的索引位(从0开始)和页类型,关于每个类型页的结构情况可以参考上面陈述过的InnoDB页结构。
index |
Page Type |
0 |
FSP_HDR |
1 |
IBUF_BITMAP |
2 |
INODE |
3、4、5 |
INDEX |
6、7 |
FREE (ALLOCATED) |
先来看下FSP_HDR
页的情况
innodb_space -s ibdata1 -T test/user_info -p 0 page-illustrate
Region Type |
Bytes |
Ratio |
█ FIL Header |
38 |
0.23% |
█ FSP Header |
112 |
0.68% |
█ Extent Descriptor (free_frag) |
40 |
0.24% |
█ Extent Descriptor (unused) |
10200 |
62.26% |
█ FIL Trailer |
8 |
0.05% |
░ Garbage |
0 |
0.00% |
Free |
5986 |
36.54% |
IBUF_BITMAP
页的情况
innodb_space -s ibdata1 -T test/user_info -p 1 page-illustrate
Region Type |
Bytes |
Ratio |
█ FIL Header |
38 |
0.23% |
█ Insert Buffer Bitmap |
8192 |
50.00% |
█ FIL Trailer |
8 |
0.05% |
░ Garbage |
0 |
0.00% |
Free |
8146 |
49.72% |
INODE
页的情况
innodb_space -s ibdata1 -T test/user_info -p 2 page-illustrate
Region Type |
Bytes |
Ratio |
█ FIL Header |
38 |
0.23% |
█ Inode List Entry |
12 |
0.07% |
█ Inode (used) |
1152 |
7.03% |
█ Inode (free) |
15168 |
92.58% |
█ FIL Trailer |
8 |
0.05% |
░ Garbage |
0 |
0.00% |
Free |
6 |
0.04% |
INDEX
页的情况,先来看下PRIMARY 主键
innodb_space -s ibdata1 -T test/user_info -p 3 page-illustrate
Region Type |
Bytes |
Ratio |
█ FIL Header |
38 |
0.23% |
█ Index Header |
36 |
0.22% |
█ File Segment Header |
20 |
0.12% |
█ Infimum |
13 |
0.08% |
█ Supremum |
13 |
0.08% |
█ Record Header |
10 |
0.06% |
█ Record Data |
87 |
0.53% |
█ Page Directory |
4 |
0.02% |
█ FIL Trailer |
8 |
0.05% |
░ Garbage |
0 |
0.00% |
Free |
16155 |
98.60% |
再来看下uk_user_id 唯一索引
innodb_space -s ibdata1 -T test/user_info -p 4 page-illustrate
Region Type |
Bytes |
Ratio |
█ FIL Header |
38 |
0.23% |
█ Index Header |
36 |
0.22% |
█ File Segment Header |
20 |
0.12% |
█ Infimum |
13 |
0.08% |
█ Supremum |
13 |
0.08% |
█ Record Header |
5 |
0.03% |
█ Record Data |
16 |
0.10% |
█ Page Directory |
4 |
0.02% |
█ FIL Trailer |
8 |
0.05% |
░ Garbage |
0 |
0.00% |
Free |
16231 |
99.07% |
接下来,看下idx_create_time 普通索引
innodb_space -s ibdata1 -T test/user_info -p 5 page-illustrate
Region Type |
Bytes |
Ratio |
█ FIL Header |
38 |
0.23% |
█ Index Header |
36 |
0.22% |
█ File Segment Header |
20 |
0.12% |
█ Infimum |
13 |
0.08% |
█ Supremum |
13 |
0.08% |
█ Record Header |
5 |
0.03% |
█ Record Data |
12 |
0.07% |
█ Page Directory |
4 |
0.02% |
█ FIL Trailer |
8 |
0.05% |
░ Garbage |
0 |
0.00% |
Free |
16235 |
99.09% |
最后,看下FREE (ALLOCATED)
页的情况
innodb_space -s ibdata1 -T test/user_info -p 6 page-illustrate
Region Type |
Bytes |
Ratio |
█ FIL Header |
38 |
0.23% |
█ FIL Trailer |
8 |
0.05% |
░ Garbage |
0 |
0.00% |
Free |
16338 |
99.72% |
4.3. 索引结构(Index Structure)
4.3.1 索引递归
通过跟踪整个 B+Tree 来递归索引(执行完整的索引扫描)(通过递归扫描所有页面,而不仅仅是按列表扫描叶子页面)
先来遍历下PRIMARY 主键
innodb_space -s ibdata1 -T test/user_info -I PRIMARY index-recurse
ROOT NODE #3: 1 records, 97 bytes
RECORD: (id=1) → (user_id=1, user_name=“user001”, age=20, sex=“1”, address=“\xE5\x8C\x97\xE4\xBA\xAC\xE5\xB8\x82\xE6\xB5\xB7\xE6\xB7\x80\xE5\x8C\xBA\xE4\xB8\xAD\xE5\x85\xB3\xE6\x9D\x91”, phone=“13800001122”, create_time=“2022-06-03 13:39:57”, update_time=“2022-06-03 13:39:57”)
再来看下索引uk_user_id 唯一索引
innodb_space -s ibdata1 -T test/user_info -I uk_user_id index-recurse
ROOT NODE #4: 1 records, 21 bytes
RECORD: (user_id=1) → (id=1)
最后看下索引idx_create_time 普通索引
innodb_space -s ibdata1 -T test/user_info -I idx_create_time index-recurse
ROOT NODE #5: 1 records, 17 bytes
RECORD: (create_time=“2022-06-03 13:39:57”) → (id=1)
4.3.2 索引记录偏移量
像 index-recurse 一样递归索引,但打印索引页中每条记录的偏移量
先来看下PRIMARY 主键
innodb_space -s ibdata1 -T test/user_info -I PRIMARY index-record-offsets
page_offset |
record_offset |
5 |
130 |
再来看下uk_user_id 唯一索引
innodb_space -s ibdata1 -T test/user_info -I uk_user_id index-record-offsets
page_offset |
record_offset |
4 |
125 |
最后看下索引idx_create_time 普通索引
innodb_space -s ibdata1 -T test/user_info -I idx_create_time index-record-offsets
page_offset |
record_offset |
5 |
125 |
4.3.3 索引级摘要
打印给定级别的所有索引页的摘要
先来看下PRIMARY 主键
innodb_space -s ibdata1 -T test/user_info -I PRIMARY -l 0 index-level-summary
page |
index |
level |
data |
free |
records |
min_key |
3 |
187 |
0 |
97 |
16155 |
1 |
id=1 |
再来看下uk_user_id 唯一索引
innodb_space -s ibdata1 -T test/user_info -I uk_user_id -l 0 index-level-summary
page |
index |
level |
data |
free |
records |
min_key |
4 |
188 |
0 |
21 |
16231 |
1 |
user_id=1 |
最后看下idx_create_time 普通索引
innodb_space -s ibdata1 -T test/user_info -I idx_create_time -l 0 index-level-summary
page |
index |
level |
data |
free |
records |
min_key |
5 |
189 |
0 |
17 |
16235 |
1 |
create_time=“2022-06-03 13:39:57” |
4.4 行记录结构(Record Structure)
可以借助4.3.2的得到的record-offset
来配置-R
的参数值
先来看下PRIMARY主键
innodb_space -s ibdata1 -T test/user_info -p 3 -R 130 record-dump
Record at offset 130
Header:
Next record offset : 112
Heap number : 2
Type : conventional
Deleted : false
Length : 10
System fields:
Transaction ID: 186438
Roll Pointer:
Undo Log: page 425, offset 272
Rollback Segment ID: 61
Insert: true
Key fields:
id: 1
Non-key fields:
user_id: 1
user_name: “user001”
age: 20
sex: “1”
address: “\xE5\x8C\x97\xE4\xBA\xAC\xE5\xB8\x82\xE6\xB5\xB7\xE6\xB7\x80\xE5\x8C\xBA\xE4\xB8\xAD\xE5\x85\xB3\xE6\x9D\x91”
phone: “13800001122”
create_time: “2022-06-03 13:39:57”
update_time: “2022-06-03 13:39:57”
4.5 行记录历史(Record History)
一条记录的历史(undo log日志)
innodb_space -s ibdata1 -T test/user_info -p 3 -R 130 record-history
Transaction |
Type |
Undo record |
(n/a) |
insert |
(id=1) → () |
可以看到此刻只有一条 insert
记录,下面执行一条修改语句,如下:
update user_info set age =21 where id = 1
Transaction |
Type |
Undo record |
186438 |
update_existing |
(id=1) → (age=20, update_time=“2022-06-03 13:39:57”) |
(n/a) |
insert |
(id=1) → () |
可以看到此刻新增了一条update_existing
类型记录,记录了修改前数据快照情况,下面再继续执行一条修改语句,如下:
update user_info set age =22 where id = 1
Transaction |
Type |
Undo record |
186444 |
update_existing |
(id=1) → (age=21, update_time=“2022-06-06 14:40:21”) |
186438 |
update_existing |
(id=1) → (age=20, update_time=“2022-06-03 13:39:57”) |
(n/a) |
insert |
(id=1) → () |
5. 小结
至此,innodb常用的文件结构、innodb ruby
工具的常见用法基本概述了一遍,具体的使用场景和后续深入学习了解,可以通过结合B+Tree数据结构的存储特性、事务隔离级别等展开,将工具和具体场景深度结合来更为具象的了解innodb存储引擎的魅力和未知的神秘内部构造。
6. 参考
使用innodb_ruby分析InnoDb索引文件
MySQL · 引擎特性 · InnoDB 文件系统之文件物理结构
MySQL技术内幕 - InnoDB存储引擎 第2版(基于MySQL5.6)