要想了解数据库 InnoDB 引擎是怎么样存储数据的,必须先了解 B+Tree,了解之后才容易理解其存储原理
在 InnoDB 存储引擎中,也有页的概念,默认每个页的大小为 16K,也就是每次读取数据时都是读取 4*4K 的大小。
一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为 10^3)。也就是说一个深度为3的B+Tree索引可以维护10^3 * 10^3 * 10^3 = 10亿 条记录。
实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在2~4层。mysql的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作
假设我们现在有一个用户表,我们往里面写数据,如下图:
注意:在某个页内插入新数据时,为了减少数据的移动,通常是插入到当前行的后面或者是已删除行留下来的空间,所以在某一个页内的数据并不是完全有序的。为了数据访问的顺序性,每条记录中都有指向下一条记录的指针,以此构成了一个有序的链表
由于只有10条记录,可以放在同一页 page1上,如果每页只能存放10条记录,当第 11 条记录插入时,数据是怎么存放的呢?如下图:
存储过程如下:
1、创建新页 page2,将 page1 的数据复制到 page2上
2、创建新页 page3,将新数据插入到 page3中
3、原来的 page1 还作为根节点,但是变成了一个只存放索引(11)不存放数据的页了,它有两个子节点 page2 和 page3
这里有以下两个问题
1、为什么要复制 page1 数据到 page2 而不是创建一个新的 page1 作为根节点 ,原来的 page1 成为 page2,这样子就减少了数据复制开销?
如果是创建新的 page1 为根节点的话,其存储的物理地址可能会变,在 InnoDB 引擎中根结点是会预读到内存中的,如果根节点的地址变了,不利于数据查找了
2、插入第 11 条数据之后,节点裂变了,根据 B+Tree 的特性,它至少是 11 阶,而裂变之后每个节点的元素至少为 11/2 = 5个,那么数据分布应该是 1-5 key的数据放到 page2 中,6-11 key的数据放到 page3 中,根节点存放主键 key 6呢?
如果是这样的话,新页的利用率只有 50%,而且还会导致频繁的页(节点)分裂
由于这个缺点,InnoDB 对这一点做了优化,新插入的数据放到新创建的页,原有页的数据不移动
随着数据的不断写入,B+Tree数据存储如下图:
主键自增
每当数据页写满之后就会创建新的页来存储,这里其实有个隐含条件,那就是主键自增。
主键自增优势:插入效率高,页的利用率高
主键自增时新插入的数据不会影响到原有页的数据,不仅插入效率高,而且页的利用率也高。如果主键无序或者随机,每次插入数据可能会导致原有页频繁分裂,严重影响插入效率,同时页的利用率也比较低。这也是为什么在 InnoDB 中建议设置主键自增的原因
B+Tree 非叶子节点存放的都是主键索引,如果一个表中没有设置主键会怎么处理呢?
如果一个表中没有设置主键,默认会找一个建了唯一索引的列。如果唯一索引列也没有,则会生成一个隐形的字段作为主键
如果表频繁的插入和删除的话,会导致页产生碎片,降低页的空间利用率,即降低查询效率,这可以通过索引重建来消除碎片,提高查询效率
通常在B+Tree上有两个头指针,一个指向根节点,另一个指向 key(关键字)最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。
InnoDB 存储数据后,怎么做查找的呢?通过以下两个步骤查找
1、找到数据所在的页,这个过程就是 B+Tree 查找,即从根节点开始一直找到叶子节点(数据所在页)
2、在数据页内查找数据,读取 1 中查找到的叶子节点数据(页数据)到内存中,然后通过分块查找的方法找到具体数据
这个查找过程就像是到新华字典上查找某一个汉字,先定位到哪一页,然后在页中找到具体的汉字。
InnoDB 在页中使用了哪种策略快速查找某个主键呢?这就要我们从页结构了解开始,如下图:
左边区域:左边蓝色区域称为 Page Directory(页目录),这块区域由多个 Slot 组成,是一个稀疏索引结构,即一个槽中可能属于多个记录,最少属于 4 条记录,最多属于 8 条记录。槽内的数据是有序存放的,所以当我们寻找一条数据的时候可以先在槽中通过二分法查找到一个大致的位置
右边区域:数据区域,每一个数据页中都包含多条行数据。注意看图中最上面和最下面的两条特殊的行记录 Infimum 和 Supremum,这是两个虚拟的行记录。在没有其他用户数据的时候 Infimum 的下一条记录的指针指向 Supremum。当有用户数据的时候,Infimum 的下一条记录的指针指向当前页中最小的用户记录,当前页中最大的用户记录的下一条记录的指针指向 Supremum,至此整个页内的所有行记录形成一个单向链表
行记录(右边数据区)被 Page Directory 逻辑的分成了多个块,块与块之间是有序的,也就是说“4”这个槽指向的数据块内最大的行记录的主键都要比“8”这个槽指向的数据块内最小的行记录的主键要小。但是块内部的行记录不一定有序。每个行记录的都有一个 n_owned 的区域(图中粉红色区域),n_owned 标识这个块有多少条数据。伪记录 Infimum 的 n_owned 值总是 1,记录 Supremum 的 n_owned 的取值范围为[1,8],其他用户记录 n_owned 的取值范围[4,8]。并且只有每个块中最大的那条记录的 n_owned 才会有值,其他的用户记录的 n_owned 为 0。
因此当我们要找主键为 6 的记录时,分为下面的步骤:
1、先通过二分法在稀疏索引(左边页目录)中找到对应的槽,也就是 Page Directory 中“8”这个槽。“8”这个槽指向的是该数据块中最大的记录,而数据是单向链表结构,所以无法逆向查找。
2、由于8 槽无法找到数据,则只能找到上一个槽即“4”这个槽,然后通过“4”这个槽中最大的用户记录的指针沿着链表顺序查找到目标记录。
数据库中的B+Tree索引可以分为聚集索引(clustered index)和辅助索引(也称非聚集索引,secondary index)。
聚集索引:上面的InnoDB 引擎数据存储中的示例图在数据库中的实现即为聚集索引,聚集索引的 B+Tree 中的叶子节点存放的是整张表的行记录数据
辅助索引(非聚集索引):辅助索引的 B+Tree 中的叶子节点存放的是表的行数据的聚集索引键(key),即主键,和聚集索引不同的是它不存放行记录的全部数据
如果上面的用户表需要以“用户名字”建立一个辅助索引,是怎么实现的呢?辅助索引树如下图:
从上图可以看出,非叶子节点中存放的全部是辅助索引(用户名字),而叶子节点存放的则是辅助索引(用户名字)+ 主键 key
因此当我们使用辅助索引查找数据时,先通过辅助索引在辅助索引树中找到辅助索引对应的主键 key(聚集索引键),然后在用主键 key 到聚集索引树上查找对应的数据,这个过程称为回表。
InnoDB 与 MyISAM 引擎在存储数据和查找数据方面有什么不同呢?下面通过一张图来简单了解一下二者区别,MyISAM 聚集索引的存储结构如下图:
从上图可以看出,MyISAM 和 InnoDB 的不同主要体现在两个方面:
1、MyISAM 的 B+Tree 叶子节点数据区存放的不是数据,而是数据记录对应的地址
2、数据的存储不是按照主键顺序存放的,而是按照写入顺序存储的
结论:InnoDB 引擎数据在物理上是按主键 key 顺序存放,而 MyISAM 引擎数据在物理上按插入的顺序存放
由于MyISAM 的叶子结点不存放具体数据,因此辅助索引的存储结构与聚集索引类似,在使用辅助索引查找数据的时候通过辅助索引树就能直接找到数据的地址了,不需要回表,这比 InnoDB 的查找效率高呢