一文带你了解MySQL之InnoDB 记录结构【上篇】

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,高可用系列 2核4GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 到现在为⽌,MySQL对于我们来说还是⼀个⿊盒,我们只负责使⽤客户端发送请求并等待服务器返回结果,表中的数据到底存到了哪⾥?以什么格式存放的?MySQL是以什么⽅式来访问的这些数据?这些问题我们统统不知道?之前的文章我们大致知道MySQL服务器上负责对表中数据的读取和写⼊⼯作的部分是存储引擎,⽽服务器⼜⽀持不同类型的存储引擎,⽐如InnoDB、MyISAM、Memory等,不同的存储引擎⼀般是由不同的⼈为实现不同的特性⽽开发的,真实数据在不同存储引擎中存放的格式⼀般是不同的,甚⾄有的存储引擎⽐如Memory都不⽤磁盘来存储数据,也就是说关闭服务器后表中的数据就消失了。

前言


到现在为⽌,MySQL对于我们来说还是⼀个⿊盒,我们只负责使⽤客户端发送请求并等待服务器返回结果,表中的数据到底存到了哪⾥?以什么格式存放的?MySQL是以什么⽅式来访问的这些数据?这些问题我们统统不知道?之前的文章我们大致知道MySQL服务器上负责对表中数据的读取和写⼊⼯作的部分是存储引擎,⽽服务器⼜⽀持不同类型的存储引擎,⽐如InnoDB、MyISAM、Memory等,不同的存储引擎⼀般是由不同的⼈为实现不同的特性⽽开发的,真实数据在不同存储引擎中存放的格式⼀般是不同的,甚⾄有的存储引擎⽐如Memory都不⽤磁盘来存储数据,也就是说关闭服务器后表中的数据就消失了。由于InnoDB是MySQL默认的存储引擎,也是我们最常⽤到的存储引擎,所以本文带你了解InnoDB存储结构。当我们熟悉了⼀个存储引擎的数据存储结构之后,其他的存储引擎都是依葫芦画瓢。


一、InnoDB⻚简介

InnoDB是⼀个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。⽽真正处理数据的过程是发⽣在内存中的,所以需要把磁盘中的数据加载到内存中,如果是处理写⼊或修改请求的话,还需要把内存中的内容刷新到磁盘上。⽽我们知道读写磁盘的速度⾮常慢,和内存读写差了⼏个数量级,所以当我们想从表中获取某些记录时,InnoDB采取的⽅式是:将数据划分为若⼲个⻚,以⻚作为磁盘和内存之间交互的基本单位,InnoDB中⻚的⼤⼩⼀般为 16K,也就是在⼀般情况下,⼀次最少从磁盘中读取16KB的内容到内存中,⼀次最少把内存中的16KB内容刷新到磁盘中。


二、InnoDB⾏格式

我们平时是以记录为单位来向表中插⼊数据的,这些记录在磁盘上的存放⽅式也被称为⾏格式或者记录格式。设计InnoDB存储引擎到现在为⽌设计了4种不同类型的⾏格式,分别是:

Compact

Redundant

Dynamic

Compressed

随着时间的推移,他们可能会设计出更多的⾏格式,但是不管怎么变,在原理上⼤体都是差不多


三、指定行格式的语法

create table表名 (列的信息) row_format=⾏格式名称

or

alter table 表名 row_format=⾏格式名称

比如我们在testdb库中创建一张学习的表demo,可以这样指定它的行格式

mysql> use testdb;

Database changed

mysql> create table demo1( c1 varchar(10), c2 varchar(10) not null, c3 char(10), c4 varchar(10), c5 varchar(1024)) charset=ascii row_format=compact;

Query OK, 0 rows affected (0.01 sec)


可以看到我们刚刚创建的这个表的⾏格式就是compact,我们还指定了这个表的字符集为ascii,因为ascii字符集只包括空格、标点符号、数字、⼤⼩写字⺟和⼀些不可⻅字符。我们现在向这个表中插⼊两条记录


mysql> insert into demo1 values('aaaaa','bbbb','ccc','dd','e');

Query OK, 1 row affected (0.00 sec)

mysql> insert into demo1 values('eeeee','ffff',null,null,'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz');

Query OK, 1 row affected (0.01 sec)

现在表中的记录就是这个样⼦的


mysql> select * from demo1;

+-------+------+------+------+------------------------------------------------------------------------------------------------------------------------------------+

| c1    | c2   | c3   | c4   | c5                                                                                                                                 |

+-------+------+------+------+------------------------------------------------------------------------------------------------------------------------------------+

| aaaaa | bbbb | ccc  | dd   | e                                                                                                                                  |

| eeeee | ffff | NULL | NULL | abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz |

+-------+------+------+------+------------------------------------------------------------------------------------------------------------------------------------+

2 rows in set (0.00 sec)


演示表的内容也填充好了,现在我们就来看看各个⾏格式下的存储⽅式到底有啥不同吧


四、Compact⾏格式


如图,⼀条完整的记录其实可以被分为记录的额外信息和记录的真实数据两⼤部分,下边我们详细看⼀下这两部分的组成

微信图片_20230525224742.png

4.1 记录的额外信息

这部分信息是服务器为了描述这条记录添加的⼀些信息,这些额外信息分为3类,分别是变⻓字段⻓度列表、NULL值列表和记录头信息,我们分别看⼀下


4.1.1 变⻓字段⻓度列表

我们知道MySQL⽀持⼀些变⻓的数据类型,⽐如varchar(M)、varbinary(M)、各种TEXT类型,各种BLOB类型,我们也可以把拥有这些数据类型的列称为变⻓字段,变⻓字段中存储多少字节的数据是不固定的,所以我们在存储真实数据的时候需要顺便把这些数据占⽤的字节数也存起来,这样才不⾄于把MySQL服务器搞懵,所以这些变⻓字段占⽤的存储空间分为两部分:


真正的数据内容

占⽤的字节数

在Compact⾏格式中,把所有变⻓字段的真实数据占⽤的字节⻓度都存放在记录的开头部位,从⽽形成⼀个变⻓字段⻓度列表,各变⻓字段数据占⽤的字节数按照列的顺序逆序存放。


我们拿demo1表中的第⼀条记录来举个例⼦。因为demo1表的、c1、c2、c4、c5列都是变⻓的数据类型,所以这四个列的值的字节⻓度都需要保存在记录开头处,我们来看⼀下第⼀条记录各变⻓字段内容字节的⻓度:


列名 存储内容 字节长度(十进制) 内容长度(十六进制)

c1 ‘aaaaa’ 5 0x05

c2 ‘bbbb’ 4 0x04

c4 ‘dd’ 2 0x02

c5 ‘e’ 1 0x01

⼜因为这些⻓度值需要按照列的逆序存放,所以最后变⻓字段⻓度列表的字节串⽤⼗六进制表示,效果就是:01 02 04 05,把这个字节串组成的变⻓字段⻓度列表填⼊上边的示意图中的效果如下:

微信图片_20230525224835.png


我们也可以查看底层存储文件:demo1.ibd,用16进制编辑器打开,我这里使用的是Notepad++和他的HEX-Editor插件。可以找到如下的数据域(可能会有其中 mysql 生成的行数据不一样,但是我们创建的行数据内容应该是一样的,而且数据长度应该是一摸一样的,可以搜索01 02 04 05字符,找到这些数据):

微信图片_20230525224858.png


由于第⼀⾏内容占⽤的字节数⽐较⼩,⽤1个字节就可以表示,但是如果变⻓列的内容占⽤的字节数⽐较多,可能就需要⽤2个字节来表示。具体⽤1个还是2个字节来表示真实数据占⽤的数,InnoDB有它的⼀套规则,这里我们⾸先声明⼀下W、M和L的意思:


假设某个字符集中表示⼀个字符最多需要使⽤的字节数为W


也就是使⽤show charset语句的结果中的Maxlen列,⽐⽅说utf8mb4字符集中W就是4,utf8字符集中的W就是3,gbk字符集中的W就是2,ascii字符集中的W就是1


对于变⻓类型varchar(M)来说,这种类型表示能存储最多M个字符,所以这个类型能表示的字符串最多占⽤的字节数就是M×W。


假设它实际存储的字符串占⽤的字节数是L


所以确定使⽤1个字节还是2个字节表示真正字符串占⽤的字节数的规则就是这样:


如果M×W <= 255,那么使⽤1个字节来表示真正字符串占⽤的字节数


如果M×W > 255,则分为两种情况:


如果L <= 127,则⽤1个字节来表示真正字符串占⽤的字节数

如果L > 127,则⽤2个字节来表示真正字符串占⽤的字节数

也就是如果该可变字段允许存储的最⼤字节数(M×W)超过255字节并且真实存储的字节数(L)超过127字节,则使⽤2个字节,否则使⽤1个字节。

另外需要注意的⼀点是,变⻓字段⻓度列表中只存储值为⾮NULL 的列内容占⽤的⻓度,值为 NULL的列的⻓度是不储存的


我们查看第二行数据,第二行数据c5,它的字符串最多占⽤的字节数是1024,实际存储的字符串占用的字节是130,所以要用两个字节来表示长度,按照逆序存放 就是

微信图片_20230525224915.png

所以两行数据示意图如下:

微信图片_20230525224954.png



4.1.2 NULL值列表

我们知道表中的某些列可能存储NULL值,如果把这些NULL值都放到记录的真实数据中存储会很占地⽅,所以Compact⾏格式把这些值为NULL的列统⼀管理起来,存储到NULL值列表中,它的处理过程是这样的


⾸先统计表中允许存储NULL的列有哪些

我们前边说过,主键列、被NOT NULL修饰的列都是不可以存储NULL值的,所以在统计的时候不会把这些列算进去。⽐⽅说表demo1的4个列c1、c3、c4、c5 都是允许存储NULL值的,⽽c2列是被NOT NULL修饰,不允许存储NULL值。


如果表中没有允许存储 NULL 的列,则 NULL值列表也不存在了,否则将每个允许存储NULL的列对应⼀个⼆进制位,⼆进制位按照列的顺序逆序排列,⼆进制位表示的意义如下


⼆进制位的值为1时,代表该列的值为NULL。

⼆进制位的值为0时,代表该列的值不为NULL

因为表demo1有4个值允许为NULL的列,所以这4个列和⼆进制位的对应关系就是这样:

微信图片_20230525225015.png

MySQL规定NULL值列表必须⽤整数个字节的位表示,如果使⽤的⼆进制位个数不是整数个字节,则在字节的⾼位补0。

对于第⼀条记录来说,c1、c3、c4、c5 这4个列的值都不为NULL,所以它们对应的个二进制位,不足一个字节,所以在字节的高位补0,效果就是这样:

微信图片_20230525225039.png

所以第⼀条记录的NULL值列表⽤⼗六进制表示就是:0x00,以此类推,如果一个表中有9个允许为NULL,那这个记录的NULL值列表部分就需要2个字节来表示了

微信图片_20230525225059.png


第二条数据c3、c4为值都为NULL,所以这4个列对应的⼆进制位的情况就是:

微信图片_20230525225135.png



所以第⼆条记录的NULL值列表⽤⼗六进制表示就是:0x06,查看文件

微信图片_20230525225115.png


所以这两条记录在填充了NULL值列表后的示意图就是这样:




4.1.3 记录头信息

除了变⻓字段⻓度列表、NULL值列表之外,还有⼀个⽤于描述记录的记录头信息,它是由固定的5个字节组成。5个字节也就是40个⼆进制位,不同的位代表不同的意思,如图:

微信图片_20230525225209.png

这些⼆进制位代表的详细信息如下表:


名称 ⼤⼩(单位: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 表示下⼀条记录的相对位置

我们查看demo1.ibd存储文件:

微信图片_20230525225226.png


根据推理,很容易可以得到demo1数据表两条记录的记录头信息如下所示


第一行:00 00 10 00 3a

第二行:00 00 18 ff bb


根据记录头的构成,将上面的记录由十六进制转换为二进制分析

第一行:00000000 00000000 00010000 00000000 00110111

第二行:00000000 00000000 00011000 11111111 10111011


根据这些二进制数据,将数据按照记录头结构切分可以得到如下信息


预留位1(1b) 预留位2(1b) delete_mask(1b) min_rec_mask(1b) n_owned(4b) heap_no(13) record_type(3b) next_record(16b)

第一行 0 0 0 0 0000 00000000 00010(2) 000 00000000 00110111(55)

第二行 0 0 0 0 0000 00000000 00011(3) 000 11111111 10111011(-69)

第二行最后16个字节11111111 10111011是负数,补码+1,1000101,十进制就是-69


所以这两条记录在填充了记录头信息后的示意图就是这样:

微信图片_20230525225248.png

4.2 记录的真实数据

对于demo1表来说,记录的真实数据除了c1、c2、c3、c4、c5这⼏个我们⾃⼰定义的列的数据以外,MySQL会为每个记录默认的添加⼀些列(也称为隐藏列),具体的列如下:


列名 是否必须 占⽤空间 描述

row_id 否 6字节 ⾏ID,唯⼀标识⼀条记录

transaction_id 是 6字节 事务ID

roll_pointer 是 7字节 回滚指针

小提示:

实际上这⼏个列的真正名称其实是:DB_ROW_ID DB_TRX_ID、DB_ROLL_PTR,为了美观才写成了row_id、transaction_id和roll_pointer,这⾥需要提⼀下InnoDB表对主键的⽣成策略:优先使⽤⽤户⾃定义主键作为主键,如果⽤户没有定义主键,则选取⼀个Unique键作为主键,如果表中连Unique键都没有定义的话,则InnoDB会为表默认添加⼀个名为row_id的隐藏列作为主键。所以我们从上表中可以看出:InnoDB存储引擎会为每条记录都添加DB_TRX_ID和DB_ROLL_PTR这两个列,但是row_id是可选的(在没有自定义主键以及Unique键的情况下才会添加该列)。这些隐藏列的值不用我们操心,InnoDB存储引擎会自己帮我们生成的。


因为表demo1并没有定义主键,所以MySQL服务器会为每条记录增加上述的3个列。

微信图片_20230525225326.png


我们看看第一行内容的文件:

微信图片_20230525225338.png


我们给第一行记录加上的真实数据,如下

微信图片_20230525225351.png


参考ASCII码对照表:16进制的61对应a 62对应b 63对应c 20对应空格(char以外的7个字节的统统都用空格字符填充) 64对应d 65对应e,懵逼了吧


4.3 CHAR(M)列的存储格式

demo1表的c1、c2、c4、c5列的类型是VARCHAR(10),而c3列的类型是CHAR(10),demo1表采用的是ascii字符集,这个字符集是一个定长字符集,也就是说表示一个字符采用固定的一个字节,如果采用变长的字符集(也就是表示一个字符需要的字节数不确定,比如gbk表示一个字符要1~2个字节、utf8表示一个字符要1~3个字节等)的话,c3列的长度也会被存储到变长字段长度列表中。


对于CHAR(M)类型的列来说,当列采用的是定长字符集时,该列占用的字节数不会被加到变长字段长度列表,而如果采用变长字符集时,该列占用的字节数也会被加到变长字段长度列表。


小提示:

变长字符集的CHAR(M)类型的列要求至少占用M个字节,而VARCHAR(M)却没有这个要求。比方说对于使用utf8字符集的CHAR(10)的列来说,该列存储的数据字节长度的范围是10~30个字节。即使我们向该列中存储一个空字符串也会占用10个字节,这是怕将来更新该列的值的字节长度大于原有值的字节长度而小于10个字节时,可以在该记录处直接更新,而不是在存储空间中重新分配一个新的记录空间,导致原有的记录空间成为所谓的碎片。


我们创建一张demo2表来看一下:

mysql> create table demo2( c1 varchar(10), c2 varchar(10) not null, c3 char(10), c4 varchar(10), c5 varchar(1024)) charset=utf8 row_format=compact;

Query OK, 0 rows affected, 1 warning (0.03 sec)

mysql> insert into demo2 values('aaaaa','bbbb','ccc','dd','e');

Query OK, 1 row affected (0.01 sec)


这里就不唠叨了,直接打开demo2.ibd文件查看

微信图片_20230525225454.png


示意图中的效果如下:

微信图片_20230525225511.png

我们在插入一条数据


mysql> insert into demo2 values('一一一一一','一一 一','测试测试测试测测试','dd','e');


这条数据的字节数如下:5*3=15 3*3+1=10 3*9=27 2 1对应16进制逆序存放 01 02 1b oa of ,这里就不画图了,我们直接看demo2.ibd文件

微信图片_20230525225524.png


这篇文档到此结束了,后面在讲其他的行格式,行溢出。本篇概念性有点强,大家可以慢慢理解。


相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
5月前
|
存储 网络协议 关系型数据库
MySQL8.4创建keyring给InnoDB表进行静态数据加密
MySQL8.4创建keyring给InnoDB表进行静态数据加密
140 1
|
9月前
|
存储 缓存 关系型数据库
【MySQL进阶篇】存储引擎(MySQL体系结构、InnoDB、MyISAM、Memory区别及特点、存储引擎的选择方案)
MySQL的存储引擎是其核心组件之一,负责数据的存储、索引和检索。不同的存储引擎具有不同的功能和特性,可以根据业务需求 选择合适的引擎。本文详细介绍了MySQL体系结构、InnoDB、MyISAM、Memory区别及特点、存储引擎的选择方案。
1619 57
【MySQL进阶篇】存储引擎(MySQL体系结构、InnoDB、MyISAM、Memory区别及特点、存储引擎的选择方案)
|
5月前
|
SQL 缓存 关系型数据库
使用温InnoDB缓冲池启动MySQL测试
使用温InnoDB缓冲池启动MySQL测试
100 0
|
7月前
|
数据管理 关系型数据库 MySQL
数据管理服务DMS支持MySQL数据库的无锁结构变更
本文介绍了使用Sysbench准备2000万数据并进行全表字段更新的操作。通过DMS的无锁变更功能,可在不锁定表的情况下完成结构修改,避免了传统方法中可能产生的锁等待问题。具体步骤包括:准备数据、提交审批、执行变更及检查表结构,确保变更过程高效且不影响业务运行。
365 2
|
9月前
|
存储 关系型数据库 MySQL
MySQL存储引擎详述:InnoDB为何胜出?
MySQL 是最流行的开源关系型数据库之一,其存储引擎设计是其高效灵活的关键。InnoDB 作为默认存储引擎,支持事务、行级锁和外键约束,适用于高并发读写和数据完整性要求高的场景;而 MyISAM 不支持事务,适合读密集且对事务要求不高的应用。根据不同需求选择合适的存储引擎至关重要,官方推荐大多数场景使用 InnoDB。
221 7
|
3月前
|
人工智能 运维 关系型数据库
数据库运维:mysql 数据库迁移方法-mysqldump
本文介绍了MySQL数据库迁移的方法与技巧,重点探讨了数据量大小对迁移方式的影响。对于10GB以下的小型数据库,推荐使用mysqldump进行逻辑导出和source导入;10GB以上可考虑mydumper与myloader工具;100GB以上则建议物理迁移。文中还提供了统计数据库及表空间大小的SQL语句,并讲解了如何使用mysqldump导出存储过程、函数和数据结构。通过结合实际应用场景选择合适的工具与方法,可实现高效的数据迁移。
665 1
|
4月前
|
负载均衡 算法 关系型数据库
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
本文聚焦 MySQL 集群架构中的负载均衡算法,阐述其重要性。详细介绍轮询、加权轮询、最少连接、加权最少连接、随机、源地址哈希等常用算法,分析各自优缺点及适用场景。并提供 Java 语言代码实现示例,助力直观理解。文章结构清晰,语言通俗易懂,对理解和应用负载均衡算法具有实用价值和参考价值。
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
|
3月前
|
SQL 关系型数据库 MySQL
Go语言数据库编程:使用 `database/sql` 与 MySQL/PostgreSQL
Go语言通过`database/sql`标准库提供统一数据库操作接口,支持MySQL、PostgreSQL等多种数据库。本文介绍了驱动安装、连接数据库、基本增删改查操作、预处理语句、事务处理及错误管理等内容,涵盖实际开发中常用的技巧与注意事项,适合快速掌握Go语言数据库编程基础。
261 62
|
6月前
|
关系型数据库 MySQL Java
【YashanDB知识库】原生mysql驱动配置连接崖山数据库
【YashanDB知识库】原生mysql驱动配置连接崖山数据库
【YashanDB知识库】原生mysql驱动配置连接崖山数据库
|
2月前
|
SQL 存储 关系型数据库
MySQL功能模块探秘:数据库世界的奇妙之旅
]带你轻松愉快地探索MySQL 8.4.5的核心功能模块,从SQL引擎到存储引擎,从复制机制到插件系统,让你在欢声笑语中掌握数据库的精髓!

推荐镜像

更多