常见的存储引擎有InnoDB、myISAM、Memory,而mysql默认是InnoDB,不如深入InnoDB的底层,去看看数据在硬盘上到底是怎么存储的。
行格式
一条记录在硬盘上存放的方式称为行格式,InnoDB中常见有四种行格式,Compact,redundant,Dynamic,Compressed
Compact行格式
新建一张表,用作演示
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;
记录的额外信息
额外信息又由三部分组成,分别是变长字段长度列表、null值列表和记录头信息
变长字段长度列表
mysql的变量类型中,有varchar、varbinary这种变长的字段,那么这些字段的长度到底记录在哪里的呢?没错,就是存储在这变长字段长度列表中。
对于上面那张表,c1、c2、c4列都是变长列,会将他们==的每列占用的字节数按照列的顺序逆序存储==。
每列会用1或2个字节表示,当M*W<=255或(M*W>255并且L<=127)时,用一个字节,否则用两个字节。
- M代表字符集中一个字符最多需要的字节数
- W表示这一列最多存储的字符数,varchar(255)就是255
- L是实际存储的字符串占用的字节数
存储引擎在读取变长字段长度列表时,会先判断M*W,若小于255,会读取一个字节作为该列的长度。若大于255,则判断字节第一位是不是0,若为0,那么这个字节最大表示127(2^7-1),则读取一个字节,如果不是0,说明大于127,则读取两个字节作为长度。
注意:
- 对于变长字符集(例如utf8),char列占用的字节数也会记录
- 如果表中所有的列都不是变长,这个部分不需要有
- 只会存储值非null列的长度,如果一行中有一个varchar列值为null,这列将不会记录长度
null值列表
每列中都可能会存在null值,这部分将所有null值列集中起来存储,由于一个列要么是null,要么不是,只有两种状态,所以可以用二进制的每位来表示,二进制位值为1,代表该列为null,反之不为null。这部分也是按照列的顺序逆序存放
- 若表中每列都限制了不为null,那么这部分不需要有
- 若二进制位不满整数字节,高位用0填充
记录的头信息
这部分固定是5个字节,共有40个二进制位
| 名称 | 大小(单位: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 | 表示下一条记录的相对位置 |
记录的真实数据
除了我们自己定义的列,mysql会为每条记录生成隐藏列
| 列名 | 是否必须 | 占用空间 | 描述 |
|---|---|---|---|
| row_id | 否 | 6字节 | 行id,唯一标示一条记录 |
| transaction_id | 是 | 6字节 | 事务id |
| roll_pointer | 是 | 7字节 | 回滚指针 |
优先使用用户自定义主键,如果没定义,选择一个unique列作为主键,如果仍然没有,会生成row_id作为主键列。
行溢出数据
varchar最多能存储的数据
mysql对于一条记录占用的最大存储空间有限制,除了BLOB和TEXT外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度总和不能超过65535。
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 change some columns to TEXT or BLOBs
mysql>
上方这个报错是因为没有将变长字段长度列表和null值列表考虑进去,这里变长字段长度列表占用两个字节,null值占用一个字节,所以最多为65532位。
如果不是ascii字符集,则会按照字符集中一个字符最多占用的字节数来计算,比方说上方的表字符集是UTF8mb4,那么varchar最多为(65535-2-1)/4 = 16383。只是针对一列且是varchar的表,其他具体情况具体分析。
溢出的临界点
mysql以页作为基本单位管理存储空间,一个页的大小是16k,规定一页最少有两行数据,来计算一下数据占用多少字节会产生溢出(针对只有一列varchar且字符集是ASCII的表)。
- 首先每页需要占用132字节存储页的额外信息
- 每行需要额外27字节存储行的额外信息,包括 2个字节的变长字段长度列表 1个字节的null值列表 5个字节的头信息 19个字节的隐藏列(没定义主键,会生成row_id)
那么,可以计算
132+(27+n)*2<16384
n<8089
只针对这一种情况。总之,当一行中存储过多字节,就会产生溢出
溢出的数据如何存储的
既然一页最少存放两条记录,那么超出的数据怎么存储的呢?在compact中,只会在真实数据的地方存储该列的一部分,其余的分散在其他页中,然后在真实数据处用20个字节存储指向这些页的地址
Dynamic和Compressed行格式
Dynamic和Compact基本差不多,区别在于处理溢出的方式不同,Dynamic中溢出的列不会存储任何的真实数据,只存放指向溢出页的地址。
Compressed行格式会采用压缩算法对页面进行压缩