Mysql是我们平时用的比较多的数据存储工具,那么当执行insert向Mysql中插入一条数据的时候,Mysql是如何存储这条的数据的呢?下面我们来聊聊这个话题。
1、Mysql的数据存放在位置
Mysql的数据是保存在磁盘上,数据具体保存在磁盘的哪个文件上是由存储引擎决定的,Mysql支持多种存储引擎(如InnoDB、MyISAM等),不同的存储引擎保存的文件也存在一定的差异。以Mysql默认的存储引擎InnoDB为例,InnoDB存储引擎将文件存放在的位置如下图所示:
图片
在InnoDB存储引擎中,每当创建一个database的时候都会在/var/lib/mysql/目录里面创建一个以database为名的目录,然后表结构和表数据的文件都会存放在这个目录下,如下图所示的表gonggao数据表文件:
图片
在database为名的目录存在三个文件,这些文件的含义如下所示:
(1)db.opt
用来存储当前数据库的默认字符集和字符校验规则。
(2)gonggao.frm
gonggao的表结构会保存在这个文件。在Mysql中建立一张表都会生成一个.frm 文件,该文件是用来保存每个表的元数据信息的,主要包含表结构定义。
(3)gonggao.ibd
gonggao的表数据会保存在这个文件。当然表数据既可以存在共享表空间文件(文件名:ibdata1)里,也可以存放在独占表空间文件(文件名:表名字.ibd)。
通过参数innodb_file_per_table控制可以设置表数据存储位置,若设置参数innodb_file_per_table=1,则会将存储的数据、索引等信息单独存储在一个独占表空间(MySQL5.6.6版本开始,该参数的默认值为1,既就是表的数据都存放在一个独立的.ibd文件)。Mysql8后,frm文件和idb文件合并成一个文件了,没有frm文件了。
2、idb文件的结构
表空间由段(segment)、区(extent)、页 (page,InnoDB与磁盘交互的单位)、行(row,具体存储数据) 组成,InnoDB存储引擎的逻辑存储结构如下图:
图片
(1)行
数据库表中的记录都是按行(row)进行存放的,每行记录根据不同的行格式,有不同的存储结构。
(2)页
记录是按照行来存储的,但是数据库的读取并不以行为单位,否则一次读取只能处理一行数据,效率会非常低。InnoDB 的数据是按页为单位来读写的,当读一条记录的时候,并不是将这个行记录从磁盘读出来,而是以页为单位,将其整体读入内存。默认每页的大小为16KB,也就是最多能保证16KB的连续存储空间,当然这个16KB也是可以通过参数调整。
页是InnoDB存储引擎磁盘管理的最小单元,每次数据库读写都是以16KB为单位的,一次最少从磁盘中读取16KB的内容到内存中。
(3)区
InnoDB存储引擎是用B+树来组织数据的。B+树中每一层都是通过双向链表连接起来的,如果是以页为单位来分配存储空间,那么链表中相邻的两个页之间的物理位置并不是连续的,可能离得非常远,那么磁盘查询时就会有大量的随机I/O,随机I/O会非常慢。
区可以保证多个页连续存储,这样在一个区里面的数据进行读取的时候可以使用顺序I/O。如果表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区为单位分配。每个区的大小为 1MB,对于16KB的页来说,连续的64个页(1024/16 = 64)会被划为一个区,这样就使得链表中相邻的页的物理位置也相邻,就能使用顺序I/O读取。
(4)段
表空间是由各个段(segment)组成的,段是由多个区(extent)组成的。段一般分为数据段(存放B+树的叶子节点的区的集合)、索引段(存放B+树的非叶子节点的区的集合)和回滚段(存放的是回滚数据的区的集合)等。段是以区为单位保证多个区形成一个段。
3、InnoDB的行格式
行格式是一条记录的存储结构,InnoDB 提供了4种行格式,分别是Redundant、Compact、Dynamic和Compressed行格式。
Redundant是很古老的行格式了,MySQL5.0版本之前用的行格式,现在基本没人用了。
Compact是一种紧凑的行格式,设计的初衷就是为了让一个数据页中可以存放更多的行记录,从MySQL5.1版本之后,行格式默认设置成Compact。Compact的格式如下所示:
图片
Dynamic和Compressed两个都是紧凑的行格式,它们的行格式都和Compact差不多,因为都是基于Compact进行改进的。从MySQL5.7版本之后,默认使用Dynamic行格式。
4、Compact的格式详解
图片
记录的额外信息有3个部分组成,分别是变长字段长度列表、null值列表、记录头信息。
4.1、变长字段长度列表
Mysql支持一些变长的数据类型,比如 VARCHAR(m)、VARBINARY(n)、TEXT 类型、BLOB类型,这些数据类型修饰列称为变长字段,变长字段中存储多少字节的数据不是固定的,所以在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。
在Compact 行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表。创建如下的用户表:
用户表的字符集设置成ASCII,行格式设置为Compact格式,我们来看看行格式中的“变长字段长度列表”是怎样存储的,分成如下情况:
(a)变长字段不为null
向用户表中添加一条数据,如下图所示:
图片
由于表的字符集设置成ASCII,所以针对每个列的数据:
name 列的值为a,真实数据占用的字节数是1字节,十六进制0x01;mobile列的值为789,真实数据占用的字节数是3字节,十六进制0x03;id列和age列不是变长字段,所以不用考虑。
这些变长字段的真实数据占用的字节数会按照列的顺序逆序存放,所以变长字段长度列表里的内容是0301,变长字段长度列表存放下图所示:
图片
(b)存在一个变长字段为null
图片
name列的值为a,真实数据占用的字节数是1字节,十六进制0x01;mobile列的值为null,null是不会存放在行格式中记录的真实数据部分里的,所以变长字段长度列表里不需要保存值为null的变长字段的长度;id列和age列不是变长字段,所以依旧不用考虑。变长字段长度列表存放下图所示:
变长字段字节数列表不是必须的,当数据表没有变长字段的时候,这时候表里的行格式就不会有变长字段长度列表了,目的是节省空间。所以变长字段长度列表只出现在数据表有变长字段的时候。
4.2 null值列表
表中的某些列可能会存储null值,如果把这些null值都放到记录的真实数据中会比较浪费空间,所以Compact行格式把这些值为null的列存储到null值列表中。
如果存在允许null值的列,则每个列对应一个二进制位(bit),二进制位按照列的顺序逆序排列,二进制值的含义如下表所示:
二进制值 | 含义 |
1 | 该列的值为null |
0 | 该列的值不为null |
另外,值列表必须用整数个字节的位表示,如果使用的二进制位个数不足整数个字节,则在字节的高位补0。
(1)行数据无字段值为null
图片
InnoDB是用整数字节的二进制位来表示null值列表的,现在不足8位,所以要在高位补0,如下图所示:
图片
使用十六进制可以表示为0x00。
(2)行数据存在null值字段
图片
此时的mobile字段值为null,使用二进制表示如下图所示:
图片
使用十六进制表示就是0x02
null值列表不是必须的,当数据表的字段都定义成NOT NULL的时候,这时候表里的行格式就不会有null值列表了。所以在设计数据库表的时候,通常都是建议将字段设置为NOT NULL,目的是为了节省至少1字节的空间(null值列表至少占用1字节空间)。
null值列表的空间不是固定1字节的,当一条记录有12个字段值是允许为null,那么就会创建2字节空间的null值列表。
4.3、记录头信息
记录头的信息如下图所示:
图片
(1)delete_mask:标识此条数据是否被删除。从这里可以知道,我们执行detele删除记录的时候,并不会真正的删除记录,只是将这个记录的delete mask标记为1。
(2)next_record:下一条记录的位置,记录与记录之间是通过链表组织的。
(3)record_type:表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录。
(4)预留位1 (1 bit):该位暂时未被使用。
(5)预留位2(1 bit):该位暂时未被使用。
(6)min_rec_mask (1 bit):B+树的每层非叶子节点中的最小记录都会添加该标记。如果是最小记录,则该位为1;否则为0。
(7)n_owned(4bits):表示当前记录拥有的记录数。使用4个bits来表示,可以表示的最大值为15。
(8)heap_no (13 bits):表示当前记录在记录堆中的位置信息。使用13个bits来表示,可以表示的最大值为8191。
4.4 记录的真实数据
记录真实数据部分除了我们定义的字段,还有三个隐藏字段,分别为:row_id、trx_id、roll_pointer,如下图所示:
图片
(1)row_id
如果我们建表的时候指定了主键或者唯一约束列,那么就没有row id隐藏字段了。如果既没有指定主键,又没有唯一约束,那么InnoDB就会为记录添加row_id 隐藏字段。row_id不是必需的,占用6个字节。
(2)trx_id
事务id,表示这个数据是由哪个事务生成的。trx_id是必需的,占用6个字节。
(3)roll_pointer
这条记录上一个版本的指针。roll_pointer是必需的,占用7个字节。
5、Mysql的数据存储
Mysql规定除了TEXT、BLOB这种大对象类型之外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过65535个字节。换句话讲,一行记录除了TEXT、BLOB类型的列,限制最大为65535字节。
varchar(n)字段类型的n代表的是最多存储的字符数量,并不是字节大小。如果要计算varchar(n)最大能允许存储的字节数,与数据库表的字符集有关,因为字符集代表着1个字符要占用多少字节,如ASCII字符集中1个字符占用1字节,那么varchar(100)意味着最大能允许存储100字节的数据。
在ASCII字符集中下,varchar(n)中的n可以取65535吗?答案是不行的,为什么呢?新建一张表,如下所示:
一行数据的最大字节数65535,其实是包含变长字段长度列表和null值列表所占用的字节数的。
图片
所以在计算varchar(n) 中n最大值时,需要减去变长字段长度列表和null值列表占用的字节数。
65535 = 真实数据长度 + 变长字段长度列表长度 + null值列表长度
那么我们创建的test表中varchar的n最大可以取多少呢?分析如下所示:
(1)name字段是允许为null的,所以会用1字节(8个bit位)来表示null值列表。
(2)如果变长字段允许存储的最大字节数小于等于255字节,就会用1字节表示变长字段长度;如果变长字段允许存储的最大字节数大于255字节,就会用2字节表示变长字段长度。
所以varchar(n)中n最大值= 65535 -2(name字段的最大字节数大于255字节,所以需要2个字节)-1(null值列表的字节数)=65532。
当前是在字符集为ACSII下的计算方式,如果字符集是UTF-8时,一个字符最多需要三个字节,所以varchar(n)的n最大取值就是65532 / 3=21844。
6、行溢出
Mysql中磁盘和内存交互的基本单位是页,一个页的大小一般是16KB,也就是16384字节,而一个varchar(n)类型的列最多可以存储65532字节,一些大对象如TEXT、BLOB可能存储更多的数据,此时一个页可能就存不了一条记录。这个时候就会发生行溢出,多的数据就会存到另外的溢出页中。
InnoDB存储引擎会自动将溢出的数据存放到溢出页中,Compact行格式在发生行溢出后的处理方式是在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在溢出页中,然后真实数据处用20字节存储指向溢出页的地址,从而可以找到剩余数据所在的页,如下图所示:
图片
Compressed和Dynamic这两个行格式与Compact相比,他们的主要区别在于处理行溢出数据时存在一定的差异。这两种格式采用完全行溢出方式,如下图所示:
图片
记录的真实数据处不会存储该列的一部分数据,只存储20个字节的指针来指向溢出页,实际的数据都存储在溢出页中。