Skip to content

MySQL 一行记录是怎么存储的?

MySQL 的数据存放在哪个文件?

MySQL 存储的行为是由存储引擎实现的,不同的存储引擎保存的文件自然也不同。 对于InnoDB

每个数据库以 database 为名的目录存放在mysql数据文件夹下

文件夹通过mysql命令行 show variables like 'datadir'; 查看,不同的安装方式不一定 一张数据库表的数据是保存在「表名字.ibd」的文件里的,这个文件也称为独占表空间文件。

表空间文件的结构是怎么样的?

1、段(segment)

表空间是由各个段(segment)组成的,段是由多个区(extent)组成的。段一般分为数据段、索引段和回滚段等。

  • 索引段:存放 B + 树的非叶子节点的区的集合;
  • 数据段:存放 B + 树的叶子节点的区的集合;
  • 回滚段:存放的是回滚数据的区的集合,事务隔离的 MVCC 利用回滚段实现了多版本查询数据。

2、区(extent)

区的大小确实是 1MB(16KB 页 ×64)

  • 当表数据量较小时,先按页分配;
  • 超过半区后切换为按区分配,避免碎片化;
  • 以顺序I/O的优势加速查询

3、页(page)

最小单元默认每个页的大小为 16KB意味着数据库每次读写都是以 16KB 为单位的,一次最少从磁盘中读取 16K 的内容到内存中,一次最少把内存中的 16K 内容刷新到磁盘中。

4、行(row)

数据库表中的记录都是按行(row)进行存放的,每行记录根据不同的行格式,有不同的存储结构。

InnoDB 行格式有哪些?

InnoDB 提供了 4 种行格式,分别是 Redundant、Compact、Dynamic 和 Compressed 行格式。其中Redundant废弃,其余三个格式类似

Compact 行格长这样:

可以看到,一条完整的记录分为「记录的额外信息」和「记录的真实数据」两个部分。

记录的额外信息

记录的额外信息包含 3 个部分:变长字段长度列表、NULL 值列表、记录头信息。

1. 变长字段长度列表

只出现在数据表有变长字段的时候。

varchar(n) 和 char(n) 的区别是char 是定长的,varchar 是变长的。

要把数据占用的大小存起来,存到「变长字段长度列表」里面,读取数据的时候才能根据这个「变长字段长度列表」去读取对应长度的数据。TEXT、BLOB 等变长字段也是这么实现的

2. NULL 值列表

如果存在允许 NULL 值的列,则每个列对应一个二进制位(bit),二进制位按照列的顺序逆序排列。

  • 二进制位的值为1时,代表该列的值为 NULL。
  • 二进制位的值为0时,代表该列的值不为 NULL。

另外,NULL 值列表必须用整数个字节的位表示(1 字节 8 位),如果使用的二进制位个数不足整数个字节,则在字节的高位补 0

当数据表的字段都定义成 NOT NULL 的时候,这时候表里的行格式就不会有 NULL 值列表了,所以建议这样设置节省至少1字节空间。(9个字段就需要两个字节空间)

3. 记录头信息

记录头信息中包含的内容很多,我就不一一列举了,这里说几个比较重要的:

  • delete_mask:标识此条数据是否被删除。

    • delete 操作不会立即删除,只是将 delete_mask 标记为 1。响应更快一点。
    • 对于MVCC而言,如果某条记录被删除了,但是因为这条记录物理上依旧存在,所以不会导致出现查询的记录出现减少(因为如果实际物理删除了,那么也就找不到这一行的trx_id是否在当前事务可见,其他事务删除会导致当前事务的多次查询结果集不同)
    • InnoDB有一个后台进程(称为“purge”进程)会定期扫描并删除这些已标记为删除的记录(且这些记录没有被任何事务引用,类似于JVM的垃圾回收),从而回收磁盘空间。这个过程是异步的,不会影响正常的事务处理。当这条记录被purge进程实际清理了之后,就不能再恢复了
  • next_record:下一条记录的位置。记录与记录之间是通过链表组织的。

    • next_record指向下一条记录在聚簇索引中的逻辑位置。帮助InnoDB在读取数据时能够按照聚簇索引的顺序遍历记录
  • record_type:表示当前记录的类型,0 表示普通记录,1 表示 B+树非叶子节点记录,2 表示最小记录,3 表示最大记录

记录的真实数据

记录真实数据部分除了我们定义的字段,还有三个隐藏字段,分别为:row_id、trx_id、roll_pointer,我们来看下这三个字段是什么。

  • row_id
    • 不是必须
    • 没有指定主键和唯一索引时 InnoDB自动添加
    • 占用6个字节
  • trx_id
    • 事务 id,表示这个数据是由哪个事务生成的。
    • trx_id 是必需的,占用 6 个字节。
  • roll_pointer
    • 这条记录上一个版本的指针。
    • roll_pointer 是必需的,占用 7 个字节。

varchar(n) 中 n 最大取值为多少?

varchar(n) 字段类型的 n 代表的是最多存储的字符数量,并不是字节大小。 ascii:一个字符一个字节 UTF-8:一个字符三个字节 utf8mb4:一个字符四个字节

MySQL 规定除了 TEXT、BLOBs 这种大对象类型之外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过 65535 个字节

一行记录最大能存储 65535 字节的数据,但是这个是包含「变长字段字节数列表所占用的字节数」和「NULL 值列表所占用的字节数」。所以,我们在算 varchar(n) 中 n 最大值时,需要减去这两个列表所占用的字节数。

可用字符数,变长字段字节数2个字节(一个字节最大255,两个字节65535)

  • 如果允许为NULL,剩余65535-2-1=65532(一个 NULL 值列表)
  • 否则剩余 65535-2 = 65533
  • 如果是ascii字符及,那么就是字符数量
  • 如果是UTF-8,需要除上3
  • 如果是utf8mb4,需要除上4
  • 多个字段,需要减去其他字段的长度

行溢出后,MySQL 是怎么处理的?

MySQL 中磁盘和内存交互的基本单位是页,一个页的大小一般是 16KB,也就是 16384字节,而一个 varchar(n) 类型的列最多可以存储 65532字节,一些大对象如 TEXT、BLOB 可能存储更多的数据,这时一个页可能就存不了一条记录。这个时候就会发生行溢出,多的数据就会存到另外的「溢出页」中

如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。

  • Compact 行格式:多余部分放在「溢出页」,当前页存储部分数据和指向溢出页的地址
  • Compressed 和 Dynamic 行格式:当前页只存储指向溢出页的地址,所有数据都在溢出页

正在精进