2.2.1. MySQL中一行记录是怎么存储的?
MySQL的数据存储在那个文件?
每创建一个 database(数据库)都会在 /var/lib/mysql/ 目录里面创建一个以 database 为名的目录,然后保存表结构和表数据的文件都会存放在这个目录里。
在数据库目录中包含三个文件:
- db.opt:存储当前数据库的默认字符集和字符校验规则
- 表名.ibd:存储表数据,也称为独占表空间文件,每张表都有独立的.ibd文件。
- 表名.firm:存储表结构,每建立一张表都生成一个.firm文件
表空间文件的结构:
表空间由段(segment)、区(extent)、页(page)、行(row)组成
- 行(row)
表中的记录都是按行存放,每行记录根据不同的行格式,有不同的存储结构
- 页(page)
InnoDB 的数据是按「页」为单位来读写的,每个页默认空间大小是16kb,是InnoDB 存储引擎磁盘管理的最小单元,常见的有数据页、undo 日志页、溢出页等等,行记录是用数据页管理的。
- 区(extent)
B+ 树中每一层都是通过双向链表连接的,以页为单位来分配存储空间,链表中相邻的两个页之间的物理位置并不连续,磁盘查询时就会有大量的随机I/O,随机 I/O 是非常慢的。当表中数据量大时,不再以页为单位给索引分配空间,而是按照区。每个区大小为1MB,连续的64的页会被划分为一个区,使链表中相邻的页的物理位置也相邻,就能使用顺序 I/O 了。
- 段(segment)
表空间由各个段组成,段由多个区组成。段一般分为数据段、索引段、和回滚段等。
-
-
- 索引段:存放 B + 树的非叶子节点的区的集合;
- 数据段:存放 B + 树的叶子节点的区的集合;
- 回滚段:存放的是回滚数据的区的集合。
-
InnoDB 行格式有哪些?
行格式(row_format),是一条记录的存储结构。
InnoDB 提供了 4 种行格式,分别是 Redundant、Compact、Dynamic和 Compressed 行格式。
-
-
- Redundant 古老的行格式, MySQL 5.0 版本之前用的行格式,现在基本没人用了。
- MySQL 5.0 之后引入了 Compact, 是一种紧凑的行格式,为了让一个数据页中可以存放更多的行记录,从 MySQL 5.1 版本之后,行格式默认设置成 Compact。
- Dynamic 和 Compressed 两个都是紧凑的行格式,它们的行格式都和 Compact 差不多,都是基于 Compact 改进一点东西。从 MySQL5.7 版本之后,默认使用 Dynamic 行格式。
-
COMPACT 行格式长什么样?
一条完整的记录分为「记录的额外信息」和「记录的真实数据」两个部分。
记录的额外信息:
- 变长字段长度列表:
存放记录的真实数据占用的大小,读取时根据「变长字段长度列表」读取对应长度的数据,其他变长字段同理。
用一个实例来进行说明:
CREATE TABLE `t_user` (
`id` int(11) NOT NULL,
`name` VARCHAR(20) DEFAULT NULL,
`phone` VARCHAR(20) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
-- 字符集是 ascii(所以每一个字符占用的 1 字节)
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;
-- 例如,表中有3条记录
+-------------+-------------+-------------------+-------------------+
| id| name | phone | age |
+-------------+-------------+-------------------+-------------------+
| 1 | a | 123 | 18 |
+-------------+-------------+-------------------+-------------------+
| 2 | bb | 1234 | NULL |
+-------------+-------------+-------------------+-------------------+
| 3 | ccc | NULL | NULL |
+-------------+-------------+-------------------+-------------------+
-
- 第一条记录:
name字段的值是a,真实数据占用1字节。phone字段的值是123,真实数据占用3字节。age和id不是变长字段。
这些变长字段的真实数据占用的字节数会按照字段的顺序逆序存放。
-
- 第三条记录:phone字段是NULL,NULL 不会存放在行格式中记录的真实数据。
为什么「变长字段长度列表」的信息要按照逆序存放?
因为「记录头信息」中指向下一个记录的指针,指向的是下一条记录的「记录头信息」和「真实数据」之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据,比较方便。
还可以使位置靠前的 记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率。
同样的道理, NULL 值列表的信息也需要逆序存放。
- NULL值列表
就是用来存储NULL值的,每个允许NULL值的字段对应一个二进制位,按照字段的顺序逆序排列。二进制位的值是1时,表示该字段是NULL,为0表示不为NULL。NULL值列表必须整数个字节表示,使用的二进制位数不够整个字节,在字节高位补0。
根据三条记录来看NULL值怎么存储的
-
- 第一条记录,没有NULL值,用整数字节的二进制位表示NULL,不足8位高位补0。
-
- 第二条记录,age字段是NULL,NULL值列表十六进制表示就是0x04。
-
- 第三条记录,phone和age字段都是NULL值,NULL值列表十六进制表示就是0x06。
三条记录NULL值列表填充完成后:
每个数据库表的行格式都有「NULL 值列表」吗?
不是,所有字段都是NOT NULL的时候就不会有NULL值列表了,都设置为NOT NULL还能节省至少1字节的存储空间。
- 记录头信息
记录头信息中包含的内容很多,列举几个比较重要的:
- delete_mask :标识此条数据是否被删除。执行 detele 删除记录的时候,并不会真正的删除记录,只是将这个记录的 delete_mask 标记为 1。
- next_record:下一条记录的位置。记录与记录之间是通过链表组织的。指向的是下一条记录的「记录头信息」和「真实数据」之间的位置。
- record_type:表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录。
记录的真实数据:
除了自己定义的字段,还有三个隐藏字段,分别为:row_id、trx_id、roll_pointer。
- row_id:建表的时候指定了主键或者唯一约束列,就没有 row_id 隐藏字段了。如果既没有指定主键,又没有唯一约束, InnoDB 就会为记录添加 row_id 隐藏字段。row_id不是必需的,占用 6 个字节。
- trx_id:事务id,表示这个数据是由哪个事务生成的。 trx_id是必需的,占用 6 个字节。
- roll_pointer:记录上一个版本的指针。roll_pointer 是必需的,占用 7 个字节。
VARCHAR()的最大取值范围是多少?
除了 TEXT、BLOB 类型的字段,限制最大为 65535 字节,注意是一行的总长度,不是一列。
要算 varchar(n) 最大能允许存储的字节数,要看数据库表的字符集,不同的字符集,1个字符占用字节不同,比如 ascii 字符集, 1 个字符占用 1 字节, varchar(100) 最大能允许存储 100 字节的数据。
65535个字节中是包含了「变长字段长度列表」和 「NULL 值列表」所占用字节数的,变长字段存储的字节数小于255字节占用1个字节,如果大于255字节占用两个字节。
计算VARCHAR(n)的最大值需要减去「变长字段长度列表」和 「NULL 值列表」所占用的字节数的,
65535-2-1=65532。
在 UTF-8 字符集下,一个字符最多需要三个字节,varchar(n) 的 n 最大取值就是 65532/3 = 21844。
行溢出后,MySQL是怎么处理的?
MySQL 中磁盘和内存交互的基本单位是页,一个页的大小一般是 16KB,也就是 16384字节,一个 varchar(n) 类型的列最多可以存储 65532字节,一些大对象如 TEXT、BLOB 可能存储更多的数据,这时一个页可能就存不了一条记录。这个时候就会发生行溢出,多的数据就会存到另外的「溢出页」中。
一个页存不下一条记录,记录的真实数据处存放部分数据,然后有20个字节存放溢出页的地址,剩余部分存到溢出页中。
Compressed 和 Dynamic采用完全溢出方式,有数据溢出时,记录的真实数据处只存放20字节的指针,指向溢出页,真实数据都存放在溢出页中。