我们平时是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为 行格式 或者 记录格式 。 InnoDB 到现在为止设计了4种不同类型的 行格式 ,分别是 Compact 、 Redundant 、Dynamic 和 Compressed 行格式。
mysql5.0之后的默认行格式为Compact , 5.7之后的默认行格式为dynamic
我们知道 MySQL 支持一些变长的数据类型,比如 VARCHAR(M) 、 VARBINARY(M) 、各种 TEXT 类型,各种 BLOB 类型,我们也可以把拥有这些数据类型的列称为 变长字段 ,变长字段中存储多少字节的数据是不固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来,这样才不至于把 MySQL 服务器搞懵,所以这些变长字段占用的存储空间分为两部分:
在 Compact 行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表**,各变长字段数据占用的字节数按照列的顺序逆序存放,**我们再次强调一遍,是逆序存放!
举个例子:
一个表中有c1、c2、c3三列数据为varchar,其中有一列数据存储了(“1234”,“123”,“1”),它们分别的字符长度就为04、03、01,若其使用ascii字符集存储,则每个的字节大小为,04、03、01(ascii用一字节表示一个字符,utf-8为3字节),则这一行在”变长字段长度列表“中存储的则为”01 03 04“(实际存储为二进制且没有空格)
由于上面的字符串都比较短,也就是说内容占用的字节数比较小,用1个字节就可以表示,但是如果变长列的内容占用的字节数比较多,可能就需要用2个字节来表示。具体用1个还是2个字节来表示真实数据占用的字节数, InnoDB 有它的一套规则,首先我们声明一下 W 、 M 和 L 的意思:
- 假设某个字符集中表示一个字符最多需要使用的字节数为 W ,比方说 utf8 字符集中的 W 就是 1-3 , ascii 字符集中的 W 就是1 。
- 对于 VARCHAR(M) 来说,表示此列最多能储存 M 个字符,所以这个类型能表示的字符串最多占用的字节数就是 M×W 。(比如:对于一个字符串”aaa“使用ascii表示则占用1*3个字节,而对于utf-8则为3*3个字节)
- 假设某字符串实际占用的字节数是 L 。
基于以上的声明,则使用1字节还是2 字节来表示变长字段长度的规则为:
W*M <= 255
: 使用一个字节表示此外,innoDB使用 字节的第一位作为标志位,如果第一位为0,则此字节就是一个单独的字段长度。如果为1,则该字节为半个字段长度。
对于一些占用字节数非常多的字段,比方说某个字段长度大于了16KB,那么如果该记录在单个页面中无法存储时,InnoDB会把一部分数据存放到所谓的溢出页中(我们后边会唠叨),在变长字段长度列表处只存储留在本页面中的长度,所以使用两个字节也可以存放下来。
另外需要注意的一点是,变长字段长度列表中只存储值为 非NULL 的列内容占用的长度,值为 NULL 的列的长度是不储存的 。
字符集utf-8,英文字符占用1个字节,中文字符3字节,对于char类型来说,若使用utf-8字符集,则char也属于 可变长字段
我们知道表中的某些列可能存储 NULL 值,如果把这些 NULL 值都放到 记录的真实数据中存储会很占地方,所以 Compact 行格式把这些值为 NULL 的列统一管理起来,存储到 NULL 值列表中,它的处理过程是这样的:
举个例子: 若有一张表,有c1 c2 c3 c4四个字段,其中c2 被NOT NULL修饰,则其NULL值列表 表示如下:
记录头信息部分如下图所示:
我们使用如下的sql语句插入几行数据:
INSERT INTO page_demo
VALUES
(1, 100, 'aaaa'),
(2, 200, 'bbbb'),
(3, 300, 'cccc'),
(4, 400, 'dddd');
则它们这几条数据记录在 页 的 User Records 部分为:
这个属性标记着当前记录是否被删除,占用1个二进制位,值为 0 的时候代表记录并没有被删除,为 1 的时候代表记录被删除掉了
被删除的记录还在页中。这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后把其他的记录在磁盘上重新排列需要性能消耗,所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的 垃圾链表 ,在这个链表中的记录占用的空间称之为所谓的 可重用空间 ,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。
B+树的每层非叶子节点中的最小记录都会添加该标记。上方插入的四条记录的 min_rec_mask 值都是 0 ,意味着它们都不是 B+ 树的非叶子节点中的最小记录。
当前组的最大记录,记录当前组有几个元素的字段。
具体参考页目录
这个属性表示当前记录在本 页 中的位置,从图中可以看出来,我们插入的4条记录在本 页 中的位置分别是: 2 、 3 、 4 、 5 。
0和1被分配给了最小记录和最大记录。
MySQL 会为每个记录默认的添加一些列(也称为 隐藏列 )
实际上这几个列的真正名称其实是:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR,我们为了美观才写成了row_id、transaction_id和roll_pointer。
row_id是可选的,表中没有主键的,则选取一个 Unique 键作为主键。如果表中连 Unique 键都没有定义的话,则 InnoDB 会为表默认添加一个名为row_id 的隐藏列作为主键。
roll_pointer 是一个指向记录对应的 undo日志 的一个指针。
我们知道对于 VARCHAR(M) 类型的列最多可以占用 65535 个字节。其中的 M 代表该类型最多存储的字符数量,如果我们使用 ascii 字符集的话,一个字符就代表一个字节。但是实际上,创建一张表并设置一个字段为VARCHAR(65535)
则会报错。
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部分存储空间:
如果该 VARCHAR 类型的列没有 NOT NULL 属性,那最多只能存储 65532 个字节的数据,因为真实数据的长度可能占用2个字节, NULL 值标识需要占用1个字节
如果 VARCHAR 类型的列有 NOT NULL 属性,那最多只能存储 65533 个字节的数据,因为真实数据的长度可能占用2个字节,不需要 NULL 值标识
相应的,如果不使用ascii字符集,而使用utf-8的话,则要按照3个字节一个字符来计算。
另外,这里我们只讨论了一张表只有一个字段的情况,实际上是一行数据最多只能储存上面那些字节。
记录中的数据太多产生的溢出
我们知道,一页最大为16KB也就是16384字节,而一个varchar类型的列最多可以储存65532字节,这样就可能造成一张数据页放不了一行数据的情况。
在 Compact 和 Reduntant 行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的一部分数据,把剩余的数据分散存储在几个其他的页中,然后 记录的真实数据 处用20个字节存储指向这些页的地址(当然这20个字节中还包括这些分散在其他页面中的数据的占用的字节数),从而可以找到剩余数据所在的页
对于 Compact 和 Reduntant 行格式来说,如果某一列中的数据非常多的话,在本记录的真实数据处只会存储该列的前 768 个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页中,这个过程也叫做 行溢出 ,存储超出 768 字节的那些页面也被称为 溢出页 。
行溢出的临界点
首先,MySQL 中规定一个页中至少存放两行记录。其次,以创建只有一个varchar(65532) 字段的表为例,的我们分析一下 一个页面的空间是如何利用的:
假设一个列中存储的数据字节数为n,那么发生 行溢出 现象时需要满足这个式子:136 + 2×(27 + n) > 16384
求解这个式子得出的解是: n > 8098 。也就是说如果一个列中存储的数据不大于 8098 个字节,那就不会发生行溢出 ,否则就会发生 行溢出 。
不过这个 8098 个字节的结论只是针对只有一个varchar(65532)
列的表来说的,如果表中有多个列,那上边的式子和结论都需要改一改了,所以重点就是: 不用关注这个临界点是什么,只要知道如果我们想一个行中存储了很大的数据时,可能发生 行溢出 的现象。
MySQL 版本 5.7 之后默认行格式是 Dynamic ,这俩行格式和 Compact 行格式挺像,只不过在处理行溢出数据时不同,它们不会在记录的真实数据处存储字段真实数据的前 768 个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址:
Compressed 行格式和 Dynamic 不同的一点是, Compressed 行格式会采用压缩算法对页面进行压缩,以节省空间。