MySQL InnoDB 行记录存储结构分析

2023年 7月 19日 50.4k 0

前言

工作中我们基本上都是用MySQL的InnoDB存储引擎,但是大家有去了解过它的底层存储结构吗,想必绝大部分人不知道,或者说不知道怎么查相关知识,刚好来看这篇文章就对了!

数据表的文件构成

Mysql的存储行为是由Innodb存储引擎去具体实现的,在windows下安装Mysql后有data(数据库存放的地方)的文件夹,linux一般在/var/lib/mysql文件件。

创建数据库和表后我们可以在data目录先看到数据库对应名称文件夹,文件夹有opt、frm、ibd三种文件:

  • db.opt,用来存储当前数据库的默认字符集和字符校验规则。
  • demo1.frm ,t_order 的表结构会保存在这个文件
  • demo1.ibd,t_order 的表数据会保存在这个文件。表数据既可以存在共享表空间文件(文件名:ibdata1,在data目录下)里,也可以存放在独占表空间文件(文件名:表名字.ibd)

表空间的组成结构

先看图,先对表空间结构做个大概了解,形成一个概念

InnoDB存储引擎中,对段的管理都是由引擎自身所完成,我们已看到段有几种类型,它是不同类型的区组成的集合,一般分为索引段(B+树非叶子节点区)、数据段(B+树非叶子节点区)、回滚段(回滚数据区)。

也就是说InnoDB 对 B+ 树的叶节点和叶子节点进行了区别对待,也就是说叶子节点有自己独有的区,非叶子节点也有自己独有的区,如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请到的区中的话,进行范围扫描的效率就大幅降低,而不同的区的集合就组成了不同的段。

我们知道B+树的每一层中的页都会形成一个双向链表,如果是以页为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能不是连续的,也许离得非常远,这种情况下进行 随机I/O 是会很慢的。

因此,应该尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可以使用所谓的 顺序I/O。

区在物理位置上由连续的64个页组成,InnoDB 中的页大小默认是 16KB,所以一个区的大小是 64*16KB= 1MB,这样使得页的双向链表在物理位置也是相邻的,从而进行顺序I/O,加快了查询效率!

在表数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按区为单位分配,甚至在表中的数据特别多的时候,可以一次性分配多个连续的区。

Innodb读取数据的时候,并不是按照行来读取数据的,InnoDB 的数据是按【页】为单位来读写的,当需要读一条记录的时候,并不是将这个行记录从磁盘读出来,而是以页为单位,将其整体读入内存。

  • InnoDB 的数据是按【页】为单位来读写的,也就是说,当需要读一条记录的时候,并不是将这个行记录从磁盘读出来,而是以页为单位,将其整体读入内存。
  • 默认每个页的大小为 16KB,也就是最多能保证 16KB 的连续存储空间。
  • 页是 InnoDB 存储引擎磁盘管理的最小单元,数据库每次读写都是以【页】为单位的,一次最少从磁盘中读取 16K 的内容到内存中。
  • MySQL也是以【行 row】进行存储的,图中对于行的描画图是 COMPACT格式,这也是重点需要了解的格式,而不同的行格式,存储的结构也不同。

    InnoDB 行格式类型

    行格式:就是记录在磁盘上的存放形式或者说存储结构

    InnoDB 存储引擎设计了 4 种行格式,分别是 Redundant、Compact、Dynamic和 Compressed ,后三个都是紧凑型行格式,为的是存放更多的行记录。

    Redundant 行格式比较古老了, MySQL 5.0 版本之前用的行格式,现在基本不用了,我们知道有这个格式就行了

    Compact 行格式在MySQL 5.0 之后引入,在MySQL5.1版本中,默认设置为Compact行格式,一条完整的记录其实可以被分为记录的额外信息和记录的真实数据两大部分。

    Dynamic 和 Compressed 它们的行格式都和 Compact 挺像,只是在 处理溢出列数据和Compact不同 ,MySQL5.7 版本之后,默认使用 Dynamic 行格式。

    Compact 行格式图解

    从上面我们知道Compact和Dynamic 和 Compressed很像,那么我们就Compact行格式展开进一步了解,了解了Compact就等同于对其他也做了了解。

    从图中我们可以看到Compact行格式下,一条记录分为 【记录的额外信息】和【记录的真实数据】两部分,我们的列数据是在真实数据部分,我们再分别对这些内容进行更具体的描述。

    记录的额外信息

    额外信息为的是更好的管理记录,分为变长字段长度列表、NULL值列表、记录头信息

    我们来创建一个表来看看变长字段具体是存的,表结构如下,行格式 Compact,本文对于行记录的实际存储案例基于这张表:

    CREATE TABLE `demo1` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `col1` varchar(45) COLLATE utf8_bin DEFAULT NULL,
      `col2` varchar(45) COLLATE utf8_bin DEFAULT NULL,
      `col3` int(11) DEFAULT NULL,
      `col4` char(5) COLLATE utf8_bin DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=ascii ROW_FORMAT=COMPACT;
    

    并插入三条数据,demo1表中的各个列都使用的是ascii字符集(每个字符只需要1个字节来进行编码)

    1:变长字段列信息

    针对VARCHAR、TEXT、BLOB这类变长字段,列中实际存储了多少数据是不固定的,因此除了要把数据本身存下来,还需要记下它的长度,COMPACT将变长列的实际长度按照字段的顺序,逆序存储在变长字段长度列表里。

    变长字段存储空间分为两部分:真正的数据部分、该数据占用的字节数

    从demo1表的第一条记录来看各个字段占用的字节数,因为是变长字段, id、col3(int)、col(char)这三个字段可以不用管

  • clo1字段是varchar ,值是zs,占用两个字节的空间,十六进制 0x02;
  • clo2字段是varchar ,值是lsa,占用三个字节的空间,十六进制 0x03;
  • 第一行行记录填入变长字段长度列表后的示意图如下:

    逆序排列的目的是为了让位置靠前的记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率

    2:NULL值列表

  • 当某些字段是null值时,才显示在null值列表
  • null值列表是通过bit位来进行标识的,一个字段占一个比特位,bit位按字段逆序排列
  • 字段值为null的bit位为1,否则为0
  • null 值列表必须用整数个字节的位表示(1字节8位),如果使用的二进制位个数不足整数个字节,则在字节的高位补 0
  • 要注意的是null值列表并不是固定的1个字节,如果一条记录中有9个字段的值都是null,那么null值列表大小将是两个字节大小,依次类推。

    结合这些特性,我们来看看一条记录中存在null值和不存在null值在null值列表中的样子,我们记录使用上面表demo1的结构和数据,其中id是主键不能为null,不在讨论范围内,表中null字段不超过8个,这三条记录对应的null值列表如下:

    第一条记录:

    第二条记录:

    第三条记录:

    3:记录头信息

    记录头其实包含了很多信息,如图,我们着重了解红色部分几个比较重要的。

    • delete_flag :删除标记 0未删除、1已删除,我们执行 detele 删除记录的时候,并不会真正的删除记录,只是将这个记录的 delete_flag 标记为 1。 (所有的被删除掉的记录会组成一个垃圾链表,记录在这个链表中占用的空间被称为可重用空间。之后若是有新的记录插入到表中,它们就可以覆盖掉被删除的这些记录占用的存储空间了)
    • next_record:记录与记录之间是通过链表组织的,它表示当前记录的真实数据到下一条记录的真实数据的距离,指向的是下一条记录的「记录头信息」和「真实数据」之间的位置。 这个位置刚好向左读就是记录头信息,向右读就是真实数据,该值为【正】表示下一条记录在它的后面,为【负】表示下一条记录在它的前面(这里都是按字节去找位置)
    • record_type:表示当前记录的类型,0:表示普通记录,1:表示B+树非叶子节点记录,2:表示最小记录(Infimum),3:表示最大记录(Supremum)

    记录的真实数据

    我们看隐藏字段 row_id、trx_id、roll_ptr 感觉是不是在哪里遇到过,只要你了解过Mysql的MVCC机制就很熟悉这几个字段

    • row_id:如果我们指定了主键或者唯一约束列,那么就没有 row_id 隐藏字段了。如果既没有指定主键,又没有唯一约束,InnoDB 才会为记录添加 row_id 隐藏字段。row_id不是必需的,占用 6 个字节。
    • trx_id:记录创建这条记录/最后一次修改该记录的事务 ID, trx_id是必需的,占用 6 个字节。
    • roll_ptr:回滚指针,记录的是记录上一个版本的指针,roll_ptr 是必需的,占用 7 个字节。

    其他字段就是我们创建表的时候定义的各个列字段了。

    总结

    通篇下来,感觉对InnoDB实际的存储结构有了更深的认识,当然也会产生不少问题,比如:

    1:一行记录除了 TEXT、BLOBs 类型的列,限制最大为 65535 字节,那么能具体分析分析吗?

    2:行溢出了会怎么样,因为一页就16kb,16384字节,是小于65535 字节的

    3:为什么设计表的时候字段会选择not null?

    等等,这些问题将会在下次进行总结,就不在这里用大篇幅展开了,毕竟一次看文字少点看得不会那么累!

    参考资料:

    《MySQL 是怎样运行的》

    InnoDB的存储结构

    相关文章

    Oracle如何使用授予和撤销权限的语法和示例
    Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
    下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
    社区版oceanbase安装
    Oracle 导出CSV工具-sqluldr2
    ETL数据集成丨快速将MySQL数据迁移至Doris数据库

    发布评论