1.前言
MySQL 服务端负责对表中数据的读取和写入工作的部分是 存储引擎 ,而服务端又支持不同类型的存储引擎,如 InnoDB 、 MyISAM 、 Memory ,不同的存储引擎有不同的应用场景, 数据在不同存储引擎中存放的格式也不同的 。 InnoDB 是MySQL 默认的存储引擎,也是最常用到的存储引擎。
2.InnoDB页简介
页 是 MySQL 中磁盘和内存交互的基本单位,也是 MySQL 管理存储空间的基本单位。 InnoDB 存储引擎 将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小默认为 16 KB 。即一次IO读操作最少从磁盘中读取 16KB 的数据到内存中,一次IO写操作最少把内存中的 16KB内容刷新到磁盘中。
3.InnoDB行格式
记录 是向表中插入读取数据的最小单位,记录在磁盘上的存放方式也被称为 行格式 。InnoDB 存储引擎有四种 行格式 ,分别是 Compact 、 Redundant 、Dynamic 和 Compressed 。
3.1.行格式语法
在创建或者修改表的语句中指定行格式 :创建一个演示用的表 record_format_demo
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称
mysql> CREATE TABLE record_format_demo (
-> c1 VARCHAR(10),
-> c2 VARCHAR(10) NOT NULL,
-> c3 CHAR(10),
-> c4 VARCHAR(10)
-> ) CHARSET=ascii ROW_FORMAT=COMPACT;
mysql> INSERT INTO record_format_demo(c1, c2, c3, c4) VALUES('aaaa', 'bbb', 'cc', 'd'),
('eeee', 'fff', NULL, NULL);
mysql> SELECT * FROM record_format_demo;
+------+-----+------+------+
| c1 | c2 | c3 | c4 |
+------+-----+------+------+
| aaaa | bbb | cc | d |
| eeee | fff | NULL | NULL |
+------+-----+------+------+
3.2.COMPACT行格式
一条完整的记录被分为 记录的额外信息 和 记录的真实数据 两大部分。
3.2.1.记录的额外信息
额外信息分为3 类,分别是 变长字段长度列表 、 NULL值列表、 记录头信息。
3.2.1.1.变长字段长度列表
变长字段长度列表用来存储 VARCHAR(M)等变长字段真实数据占用的字节数。
在 Compact 行格式中, 把所有变长字段的真实数据占用的字节长度都存放在记录的开头部分即变长字段长度列表,各变长字段数据占用的字节数按照列的顺序 逆序 存放。 record_format_demo 表的 c1 、 c2 、 c4 列 都是 VARCHAR(10) 类型的,也就是变长的数据类型,所以这三个列的值的长度都需要保存在记录开头处,因为 record_format_demo 表中的各个列都使用的是 ascii 字符集,所以每个字符只需要1个字节来进行编码。
把这个字节串组成的 变长字段长度列表 填入上边的示意图中的效果就是:
当列的数据占用的字节数比较小时,用1个字节即可表示数据占用的字节数。
当列的数据占用的字节数比较大时,用2个字节即可表示数据占用的字节数。
假设某个字符集中表示一个字符最多需要使用的字节数为 W,即 utf8 字符集中的 W 是 3 , gbk 字符集中的 W 是 2 , ascii 字符集中的 W 是1 。假设对于变长类型 VARCHAR(M) 来说,这种类型表示能存储最多 M 个字符(注意是字符不是字节),所以这个列最多占用的字节数就是 M×W 。 假设它实际存储的字符串占用的字节数是 L 。 如果 M×W <= 255 ,那么使用1个字节来表示数据占用的字节数。(0xFF=255)
如果 M×W > 255 ,则分为两种情况:
如果 L <= 127 ,则用1个字节来表示真正字符串占用的字节数。
如果 L > 127 ,则用 2 个字节来表示真正字符串占用的字节数。
总结:如果该可变字段允许存储的最大字节数( M×W )超过 255 字节并且真实存储的字节数( L ) 超过127 字节,则使用 2 个字节,否则使用 1 个字节。
注: 变长字段长度列表中只存储值为 非 NULL 的列内容占用的长度,值为 NULL 的列的长度和定长字段的长度是不储存的 。
当表中所有列都不是变长数据类型时,则行格式就没有变长字段长度列表。
3.2.1.2.NULL 值列表
Compact行格式会把NULL值的列统一管理,存储到NULL值列表,这样NULL值就不会存放到 记录的真实数据 中去。主键列、被 NOT NULL 修饰的列是不可以存储 NULL 值的,所有NULL值列表不会存储主键列、被 NOT NULL 修饰的列的对应二进制位。
二进制位的值为 1 时,代表该列的值为 NULL 。 二进制位的值为 0 时,代表该列的值不为 NULL 。 和变长字段长度列表类似, 如果表中没有允许存储 NULL 的列,则 NULL 值列表 也不存在, 否则将每个允许存储 NULL 的列对应一个二进制位,二进制位按照列的顺序 逆序 排列。
因为表 record_format_demo 有 3 个值允许为 NULL 的列,所以这 3 个列和二进制位的对应关系:
MySQL 规定 NULL值列表 必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节的高位补 0 。 表 record_format_demo 只有 3 个值允许为 NULL 的列,对应 3 个二进制位,不足一个字节,所以在字节的高位补 0 :
对于第一条记录来说, c1 、 c3 、 c4 这 3 个列的值都不为 NULL ,所以它们对应的二进制位都是 0
对于第二条记录来说, c1 、 c3 、 c4 这 3 个列中 c3 和 c4 的值都为 NULL
3.2.1.3.记录头信息
用于描述记录的 记录头信息 ,它是由固定的 5 个字节组成。 5 个字节也就是 40 个二进制位。
3.2.2.记录的真实数据
对于 record_format_demo 表来说, 记录的真实数据 除了 c1 、 c2 、 c3 、 c4 这几个我们自己定义的列的数据以外, MySQL 会为每个记录默认的添加一些隐藏列。
InnoDB 表对主键的生成策略:优先使用用户自定义主键作为主键,如果用户没有定义主键,则选取一个 Unique 键作为主键,如果表中 Unique 键也没有定义的话,则 InnoDB 会为表默认添加一个名为 row_id 的隐藏列作为主键。所以我们从上表中可以看出: InnoDB 存储引擎会为每条记录都添加 transaction_id 和 roll_pointer 这两个列,但是 row_id 是可选的(在没有自定义主键以及 Unique 键的情况下才会添加该列) 。 隐藏列InnoDB 存储引擎会自己帮我们生成的。
因为表 record_format_demo 并没有定义主键,所以 MySQL 会为每条记录增加上述的 3 个列。
第1 条记录中 c3 列的值,它是 CHAR(10) 类型的,它实际存储的字符串是: 'cc' ,而 ascii 字符集中 的字节表示是 '0x6363' ,虽然表示这个字符串只占用了 2 个字节,但整个 c3 列仍然占用了 10 个字节的空 间,除真实数据以外的8 个字节的统统都用 空格字符 填充,空格字符在 ascii 字符集的表示就是 0x20 。
第2 条记录中 c3 和 c4 列的值都为 NULL ,它们被存储在了前边的 NULL值列表 处,在记录的真实数据处就不再冗余存储,从而节省存储空间。从这里可以看出NULL值列表可以节省存储空间。
3.2.3.CHAR(M)列的存储格式
record_format_demo 表的 c1 、 c2 、 c4 列的类型是 VARCHAR(10) ,而 c3 列的类型是 CHAR(10) ,在Compact 行格式下只会把变长类型的列的长度 逆序 存到 变长字段长度列表 中, 但是这是因为record_format_demo 表采用的是 ascii 字符集,这个字符集是一个定长字符集,也就是
说表示一个字符采用固定的一个字节,如果采用变长的字符集(也就是表示一个字符需要的字节数不确定,比如gbk 表示一个字符要 2 个字节、 utf8 表示一个字符要 3 个字节等)的话, c3 列的长度也会被存储到 变长字段长度列表 中。
mysql> ALTER TABLE record_format_demo MODIFY COLUMN c3 CHAR(10) CHARACTER SET utf8;
Query OK, 2 rows affected (0.02 sec)
Records: 2 Duplicates: 0 Warnings: 0
对于 CHAR(M) 类型的列来说,当列采用的是定长字符集时,该列占用的字节数不会被加到变长字 段长度列表,而如果采用变长字符集时,该列占用的字节数也会被加到变长字段长度列表。 变长字符集的 CHAR(M) 类型的列要求至少占用 M 个字节,而 VARCHAR(M) 却没有这个要
求。对于使用 utf8 字符集的 CHAR(10) 的列来说,该列存储的数据字节长度的范围是 10 ~ 30 个字节。即使向该列中存储一个空字符串也会占用 10 个字节,这是怕将来更新该列的值的字节长度大于原有值的字节 长度而小于10 个字节时,可以在该记录处直接更新,而不是在存储空间中重新分配一个新的记录空间,导致原有的记录空间成为所谓的碎片。
3.3.Redundant行格式
MySQL5.0 之前用的一种行格式,和 Compact 行格式类似。比较老不具体展开。
3.3.1.CHAR(M)列的存储格式
在 Redundant 行格式中十分干脆,不管该列使用的字符集是啥,只要是使用 CHAR(M) 类型,占用的真实数据空间就是该字符集表示一个字符最多需要的字节数和 M 的乘积。比方说使用 utf8 字符集的 CHAR(10) 类型的列占 用的真实数据空间始终为 30 个字节,使用 gbk 字符集的 CHAR(10) 类型的列占用的真实数据空间始终为 20 个字节。由此可以看出来,使用 Redundant 行格式的 CHAR(M) 类型的列是不会产生碎片的。
3.4. 行溢出数据
3.4.1. VARCHAR(M) 最多能存储的数据
对于 VARCHAR(M) 类型的列最多可以占用 65535 个字节。其中的 M 代表该类型最多存储的字符数量,如果我们使用 ascii 字符集的话,一个字符就代表一个字节。
mysql> CREATE TABLE varchar_size_demo(
-> c VARCHAR(65535)
-> ) CHARSET=ascii ROW_FORMAT=Compact;
ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not
counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to c
hange some columns to TEXT or BLOBs
从报错信息里可以看出, MySQL 对一条记录占用的最大存储空间是有限制的,除了 BLOB 或者 TEXT 类型的列之 外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过 65535 个字节。所以 MySQL 服 务器建议我们把存储类型改为 TEXT 或者 BLOB 的类型。这个 65535 个字节除了列本身的数据之外,还包括一些 其他的数据( storage overhead ),比如说我们为了存储一个 VARCHAR(M) 类型的列,其实需要占用 3 部分存储空间:
真实数据 真实数据占用字节的长度 NULL 值标识,如果该列有 NOT NULL 属性则可以没有这部分存储空间
如果该 VARCHAR 类型的列没有 NOT NULL 属性,那最多只能存储 65532 个字节的数据,因为真实数据的长度可能 占用2 个字节, NULL 值标识需要占用 1 个字节。
如果 VARCHAR 类型的列有 NOT NULL 属性,那最多只能存储 65533 个字节的数据,因为真实数据的长度可能占用 2个字节,不需要 NULL 值标识。
如果 VARCHAR(M) 类型的列使用的不是 ascii 字符集,那 M 的最大取值取决于该字符集
表示一个字符最多需要的字节数。在列的值允许为 NULL 的情况下, gbk 字符集表示一个字符最多需要 2 个字节,那在该字符集下, M 的最大取值就是 32766 (也就是: 65532/2 ),也就是说最多能存储 32766 个字符; utf8 字符集表示一个字符最多需要 3 个字节,那在该字符集下, M 的最大取值就是 21844 ,就是说最多能存储 21844 (也就是: 65532/3 )个字符。
3.4.2. 记录中的数据太多产生的溢出
MySQL 中磁盘和内存交互的基本单位是页,也就是说 MySQL 是以 页 为基本单位来管理存储空间的,我们的记录都会被分配到某个页 中存储。而一个页的大小一般是 16KB ,也就是 16384 字节,而一个 VARCHAR(M) 类型的列就最多可以存储 65532 个字节,这样就可能造成一个页存放不了一条记录的情况。
在 Compact 和 Reduntant 行格式中,对于占用存储空间非常大的列,在 记录的真实数据 处只会存储该列的一部分数据,把剩余的数据分散存储在几个其他的页中,然后 记录的真实数据 处用 20 个字节存储指向这些页的地址 (当然这20 个字节中还包括这些分散在其他页面中的数据的占用的字节数),从而可以找到剩余数据所在的页。
对于 Compact 和 Reduntant 行格式来说,如果某一列中的数据非常多的话,在本记录的真实
数据处只会存储该列的前 768 个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页中,这个过程也叫做 行溢出 ,存储超出 768 字节的那些页面也被称为 溢出页 。
不只是 VARCHAR(M) 类型的列,其他的 TEXT 、BLOB 类型的列在存储数据非常多的时候
也会发生行溢出。
3.4.3. 行溢出的临界点
MySQL 中规定一个页中至少存放两行记录。
每个页除了存放我们的记录以外,也需要存储一些额外的信息,额外信息加起来需要 136 个字节的空间,其他的空间都可以被用来存储记录。 每个记录需要的额外信息是 27 字节。 27 个字节包括下边这些部分:
2 个字节用于存储真实数据的长度 1 个字节用于存储列是否是 NULL 值 5 个字节大小的头信息 6 个字节的 row_id 列 6 个字节的 transaction_id 列 7 个字节的 roll_pointer 列 假设一个列中存储的数据字节数为 n ,那么发生 行溢出 现象时需要满足这个式子:
136 + 2×(27 + n) > 16384
求解这个式子得出的解是: n > 8098 。也就是说如果一个列中存储的数据不大于 8098 个字节,那就不会发生行溢出,否则就会发生 行溢出 。不过这个 8098 个字节的结论只是针对只有一个列的 表来说的,如果表中有多个列,那上边的式子和结论都需要改一改了,所以重点就是: 不用关注这个临界点是什么,只要知道如果一个行中存储了很大的数据时,可能发生行溢出的现象 。
3.5. Dynamic 和 Compressed 行格式
Dynamic 和 Compressed 行格式 ,MySQL 版本是 5.7的默认行格式是Dynamic。 这俩行格式和 Compact 行格式挺像,只不过在处理 行溢出 数据时有点儿分歧,它们不会在记录的真实数据处存储字段真实数据的前768 个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址。
Compressed 行格式和 Dynamic 不同的一点是, Compressed 行格式会采用压缩算法对页面进行压缩,以节省空间。