只是做了简单的总结和简述,很多地方并没有那么详尽,参考书本《MySQL是怎么运行的:从根上理解MySQL》


InnDB是怎么存储数据的

  • InnDB页的记录结构
  • COMPACT行格式
  • 其他行格式
  • InnDB的数据页结构
  • File Header
  • Page Header
  • Free Space(User Records Infimum+ supremum)
  • Page Directory
  • File Trailer
  • B+树 索引


InnDB页的记录结构

InnDB是一个将表中数据存储于硬盘中的存储引擎。与之区别的:Memory(存储于内存中),与之相似的:MylSAM;

MySQL INNER JOIN 不写ON mysql innerdb_字段


对于InnDB来说,数据是存储于磁盘,但是真正处理数据的过程发生在内存中,所以需要经过磁盘IO将 磁盘中的数据加载到内存。InnDB采取了很优秀的方式:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。 也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。

我们平时是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式或者记录格式。InnDB拥有着多种行格式,原理上大体相同,主要探讨COMPACT行格式。

COMPACT行格式

MySQL INNER JOIN 不写ON mysql innerdb_字段_02


如图,我们可以把一条记录分为 记录的额外信息记录的真实数据两大部分。

  • 记录的额外信息(简述)
    这部分信息是为了描述记录而不得不添加的额外的一些信息,额外信息分外三类 变长字段长度列表NULL值列表记录头信息
    边长字段长度列表:用于存储可变长(VARCHAR、TEXT)字段的真实数据占用的字节长度,并按照列的顺序逆序存放
    NULL值列表: 顾名思义,存储的即字段可为NULL的列的位置,逆序存放
    (二进制位1时,表示该列位NULL,反之则为0)
    记录头信息: 有多个字段,部分字段介绍如下:

字段名

介绍

delete_mask

标记记录是否被删除(删除则标记为1)

min_rec_mask

B+树每层非叶子节点的最小记录都会添加该标记

n_owned

表示到本条记录拥有的记录数

heap_no

表示当前记录在堆中的位置序号

record_type

表示记录类型,0为普通记录,1为B+树非叶子节点记录(目录记录),2表示最小记录,3表示最大记录

next_record

表示下一条记录的位置

  • 记录的真实数据
    对于InnDB来说,真实数据除了我们自己定义的数据字段之外还有2个或3个隐藏字段(取决于我们是否定义了主键或Unique键)

列名

描述

row_id

行ID,唯一标识记录,当我们定义了主键或者Unique键则不会有该字段

tx_id

事务ID,用于确保ACID

roll_poniter

回滚指针,同样是用于确保ACID

实际上这几个列的真正名称其实是:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR

其他行格式

除COMPACT之外还有Redundant等行格式,MySQL5.7默认行格式为Dynamic,与COMPACT相似,区别在于针对行溢出处理方式不同。
COMPACT会存储溢出数据的一部分数据(768字节),把剩余的数据分散存储至其他的页面,在记录真实的数据处使用20个字节存储指向这些页的地址。
Dynamic则会把所有的字节都从存储到其他的页面中,只在真实数据处存储其他页面的的地址

InnDB的数据页结构

数据页代表的就是这块16KB大小的存储空间

MySQL INNER JOIN 不写ON mysql innerdb_字段_03

File Header

文件头部针对各种类型的页都通用,它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁。

MySQL INNER JOIN 不写ON mysql innerdb_数据_04

Page Header

存储各种状态信息,具体各个字节的含义可以参考下方表格:

MySQL INNER JOIN 不写ON mysql innerdb_数据_05


对于PAGE_N_DIR_SLOTSPAGE_LAST_INSERT以及PAGE_N_RECS的含义可先看Page Directory理解。

Free Space(User Records Infimum+ supremum)

Free Spcae区域其实就是用于分配User Records区域的内存区域,当Free Space区域内存全部分配完成,则表明该页面数据量已达最大,需要分页。

infimum+supremum区域为最小记录和最大记录的存储区域

MySQL INNER JOIN 不写ON mysql innerdb_主键_06


记录头信息中的next_record列 表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量 可以通过一条记录找到它的下一条记录Infimum记录(也就是最小记录) 的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录)

MySQL INNER JOIN 不写ON mysql innerdb_字段_07

Page Directory

InnoDB会把页中的记录划分为若干个组,每个组的最后一个记录的地址偏移量作为一个槽,存放在Page Directory中,所以在一个页中根据主键查找记录是非常快的,分为两步:

  • 通过二分法确定该记录所在的槽。
  • 通过记录的next_record属性遍历该槽所在的组中的各个记录。

具体的记录槽分配过程这里就不作过多复述了。

MySQL INNER JOIN 不写ON mysql innerdb_主键_08

File Trailer

这个部分是和File Header中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为File Header在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,那么在File Header中的校验和就代表着已经修改过的页,而在File Trailer中的校验和代表着原先的页,二者不同则意味着同步中间出了错。
用于校验页的完整性

B+树 索引

通过InnDB页的介绍我们知道各个数据页可以组成一个双向链表,而每个数据页中的记录又按照主键从小到大的顺序组成了单向链表,每个数据页都会为存储在其中的记录生成页目录,通过主键查找记录,可以在页目录中使用二分法快速定位相对应的槽,然后在遍历对应槽中的记录。

InnDB形成B+树索引的过程:

  • 每当为某个表创建一个B+树索引(聚簇索引不是人为创建的,默认就有)的时候,都会为这个索引创建一个根节点页面。最开始表中没有数据的时候,每个B+树索引对应的根节点中既没有用户记录,也没有目录项记录。
  • 随后向表中插入用户记录时,先把用户记录存储到这个根节点中。
  • 当根节点中的可用空间用完时继续插入记录,此时会将根节点中的所有记录复制到一个新分配的页,比如页a中,然后对这个新页进行页分裂的操作,得到另一个新页,比如页b。这时新插入的记录根据键值(也就是聚簇索引中的主键值,二级索引中对应的索引列的值)的大小就会被分配到页a或者页b中,而根节点便升级为存储目录项记录的页。
  • 一个B+树索引的根节点自诞生之日起,便不会再移动

MySQL INNER JOIN 不写ON mysql innerdb_字段_09