在介绍索引之前,我们有必要了解一下InnoDB底层的逻辑存储结构,因为索引是基于这个底层逻辑存储结构创建的。截止到目前,我们所展示的都仅仅是物理磁盘中的逻辑视图,接下来我们就来看看底层的视图。
1、ibd文件组织结构
现在我们打开一个表空间ibd文件,看看里面都是如何组织数据的?
如下图,表空间由段(segment)、区(extent)、页(page)组成。
InnoDB最小的存储单位是页,默认每个页大小是16k。
而InnoDB存储引擎是面向行的(row-oriented),数据按行进行存放,每个页规定最多允许存放的行数=16k/2 - 200,即7992行。
段:如数据段、索引段、回滚段等。InnoDB存储引擎是B+树索引组织的,所以数据即索引,索引即数据。B+树的叶子节点存储的都是数据段的数据。
2、数据页结构[1]
名称 | 占用空间 | 描述 |
---|---|---|
Fil Header | 38 byte | 页的基本信息,如所属表空间,上一页和下一页指针。 |
Page Header | 56 byte | 数据页专有的相关信息 |
Infimun + Supremum | 26 byte | 两个虚拟的行记录,用于限定记录的边界 |
User Records | 动态分配 | 实际存储的行记录内容 |
Free Space | 动态调整 | 尚未使用的页空间 |
Page Directory | 动态调整 | 页中某些记录的相对位置 |
Fil Trailer | 8 byte | 校验页是否完整 |
关于Infimun和Supremum:首次创建索引时,InnoDB会在根页面中自动设置一个最小记录和一个最高记录,并且永远不会删除它们。最低记录和最高记录可以视为索引页开销的一部分。最初,它们都存在于根页面上,但是随着索引的增长,最低记录将存在于第一或最低叶子页上,最高记录将出现在最后或最大关键字页上。
3、行记录结构描述[2]
先来讲讲Compact行记录格式,Compact是MySQL5.0引入的,设计目标是高效的存储数据,让一个页能够存放更多的数据,从而实现更快的B+树查找。
名称 | 描述 |
---|---|
变长字段长度列表 | 字段大小最多用2个字节表示,也就是最多限制长度:2^16=65535个字节;字段大小小于255字节,则用1个字节表示; |
NULL标志位 | 记录该行哪些位置的字段是null值 |
记录头信息 | 记录头信息信息,固定占用5个字节 |
列1数据 | 实际的列数据,NULL不占用该部分的空间 |
列2数据 | |
… |
记录头用于将连续的记录链接在一起,并用于行级锁定。
每行数据除了用户定义的列外,还有两个隐藏列:
- 6个字节的事务ID列;
- 7个字节的回滚指针列;
- 如果InnoDB没有指定主键,还会增加一个6个字节的rowid列;
而记录头信息包[3]含如下内容:
名称 | 大小(bit) | 描述 |
---|---|---|
() | 1 | 未知 |
() | 1 | 未知 |
deleted_flag | 1 | 该行是否已被删除 |
min_rec_flag | 1 | 如果该记录是预定义的最小记录,则为1 |
n_owned | 4 | 该记录拥有的记录数 |
heap_no | 13 | 索引堆中该条记录的排序号 |
record_type | 3 | 记录类型:000 普通,001 B+树节点指针,010 Infimum,011 Supremum,1xx 保留 |
next_record | 16 | 指向页中下一条记录 |
更详细的页结构参考官网:22.2 InnoDB Page Structure
更详细的行结构参考官网:22.1 InnoDB Record Structure
更详细的行格式参考官网:14.11 InnoDB Row Formats
根据以上格式,可以得出数据页内的记录组织方式:
3.1、MySQL中varchar最大长度是多少
上面表格描述我们知道,一个字段最长限制是65535个字节,这是存储长度的限制。
而MySQL中对存储是有限制的,具体参考:8.4.7 Limits on Table Column Count and Row Size
- MySQL对每个表有4096列的硬限制,但是对于给定的表,有效最大值可能会更少;
- MySQL表的每行行最大限制为65,535字节,这是逻辑的限制;实际存储的时候,表的物理最大行大小略小于页面的一半。如果一行的长度少于一页的一半,则所有行都将存储在本地页面内。如果它超过一页的一半,那么将选择可变长度列用于外部页外存储,直到该行大小控制在半页之内为止。
而实际能够存储的字符是跟编码有关的。
背景知识:
MySQL 4.0版本以下,varchar(10),代表10个字节,如果存放UTF8汉字,那么只能存3个(每个汉字3字节);
MySQL 5.0版本以上,varchar(10),指的是10个字符,无论存放的是数字、字母还是UTF8汉字(每个汉字3字节),都可以存放10个,最大大小是65532字节;
因此,Mysql5根据编码不同,存储大小也不同。
那么假设我们使用的是utf8编码,那么每个字符最多占用3个字节,也就是最多定义varchar(21845)个字符,如果是ascii编码,一个字符相当于一个字节,最多定义varchar(65535)个字符,下面我们验证下。
我们尝试创建一个这样的字段:
1 | CREATE TABLE `t10` ( `id` int(11) NOT NULL, |
发现提示这个错误:
1 | mysql> alter table t10 add `str` varchar(65535) DEFAULT NULL; |
原因是按照以上的行格式介绍,变长字段长度列表
记录也需要占用空间,占用2个字节,另外这里是允许为空字段,在8位之内,所以NULL标志位占用1个字节,所以我们总共可以存储的字符数是:
65535 - 2 - 1 - 4 - 4=65534
其中 -2 个字节表示变长字段列表,-1表示NULL标志位,两个-4表示两个int类型字段占用大小
所以实际上能够容纳的varchar大小为:65524,我们验证下:
3.2、行记录超过页大小如何存储
MySQL表的内部表示具有65,535字节的最大行大小限制。InnoDB
对于4KB,8KB,16KB和32KB innodb_page_size
设置,表的最大行大小(适用于本地存储在数据库页面内的数据)略小于页面的一半 。如果包含 可变长度列的InnoDB
行超过最大行大小,那么将选择可变长度列用于外部页外存储。
可变长度列由于太长而无法容纳在B树页面上,这个时候会把可变长度列存储在单独分配的磁盘页面上,这些页面称为溢出页面
,这些列称为页外列
。页外列的值存储在由溢出页面构成的单链接列表
中。
InnoDB
存储引擎支持四种行格式:REDUNDANT
,COMPACT
, DYNAMIC
,和COMPRESSED
。不同的行格式,对溢出的阈值和处理方式有所区别,详细参考:14.11 InnoDB Row Formats。
COMPACT行格式处理方式
使用COMPACT
行格式的表将前768个字节的变长列值(VARCHAR
, VARBINARY
和 BLOB
和 TEXT
类型)存储在B树节点内的索引记录中,其余的存储在溢出页上。
如果列的值等于或小于768个字节,则不使用溢出页,因此可以节省一些I / O。
如果查过了768个字节,那么会按照如下方式进行存储:
DYNAMIC行格式处理方式
DYNAMIC
行格式提供与COMPACT
行格式相同的存储特性,但改进了超长可变长度列的存储能力和支持大索引键前缀。
InnoDB
可以完全在页外存储过长的可变长度列值(针对 VARCHAR
, VARBINARY
和 BLOB
和 TEXT
类型),而聚集索引记录仅包含指向溢出页的20字节指针。大于或等于768字节的固定长度字段被编码为可变长度字段。
表中大字段引发的问题
如果一个表中有过多的可变长度大字段,导致一行记录太长,而整个时候使用的是COMPACT行格式,那么就可能会插入数据报错。
如,页面大小是16k,根据前面描述我们知道,MySQL限制一页最少要存储两行数据,如果很多可变长度大字段,在使用COMPACT的情况下,仍然会把大字段的前面768个字节存在索引页中,可以算出最多支持的大字段:
1024 * 16 / 2 / 768 = 10.67
,那么超过10个可变长度大字段就会插入失败了。这个时候可以把row format改为:DYNAMIC。
References
MySQL Internals Manual - innodb page structure[EB/OL]. (2020-05-04). Retrieved 2020-0530, from https://dev.mysql.com/doc/internals/en/innodb-page-structure.html ↩︎
official.MySQL Internals Manual - innodb record structure[EB/OL]. (2020-05-04). Retrieved 2020-0530, from https://dev.mysql.com/doc/internals/en/innodb-record-structure.html ↩︎
姜承尧. MySQL技术内幕-InnoDB存储引擎第二版[M]. 机械工业出版社, 2013-5:104. ↩︎