参考:小林coding、javguide
持续更新
MySQL中B+树的真实数据结构
数据库的I/O操作的最小单位是页,同理,InnoDB是以数据页为单位进行读写的,InnoDB数据页的默认大小是16KB。在MySQL中,B+树的每一个节点就是一个数据页,每次读取一个数据页,就需要一次磁盘I/O(这个理解非常重要,同学们务必记住)。
数据页包含七个部分:
文件头中有两个指针,分别指向上一个和下一个数据页,这也是为什么我们说,在MySQL中,B+树的叶子节点是按照一个双向链表组织的。
用户记录和空闲空间部分用来存储数据(可能是索引key + 数据,也可能是索引key + 页指针,这取决于是叶子节点还是非叶子节点,这个页指针指向的数据页中,最小的索引key就是当前这个索引key),这些记录按照索引key顺序形成单向链表(比如在主键索引中,索引key就是主键,或者说给某列建立了一个普通索引,这个列就是索引key,就是按照这个列的值的顺序)。由于单向链表检索时需要遍历链表上的所有节点才能完成检索,检索效率低,因此数据页中就有了页目录。
最小记录和最大记录是两条虚拟记录,它们存储了比当前数据页最小索引key还要小的数据页的指针。
页目录会将数据页中所有的行记录划分成几个组,每个组都有一个槽,槽指向每个组的最后一条记录。第一个分组只包含一个记录,这个记录称为数据页中的最小记录(索引值最小),与此对应的还有最大记录,最大记录在最后一个分组,但是这个分组不限制只能包含一个记录。另外,最后一个分组的记录条数范围在1-8条,其他分组的记录条数在4-8条,每个分组的最后一条记录的头信息中会存储该组有多少条记录。如图:
当我们要查找一条记录的时候,就根据槽指向的记录做二分查找,定位到记录在哪个分组之后,再遍历该分组找到符合条件的记录。
举个例子:
以上面那张图举个例子,5个槽的编号分别为0,1,2,3,4,我想查找主键为11的用户记录:
- 先二分查找得出槽中间位是(0+4)/2=2,2号槽里最大的记录为8。因为11>8,所以需要从2号槽后继续搜索记录;
- 再使用二分搜索出2号和4槽的中间位是(2+4)/2=3,3号槽里最大的记录为12。因为11<12,所以主键为11的记录在3号槽里;
- 这里有个问题,槽对应的值都是这个组的主键最大的记录,如何找到组里最小的记录?比如槽3对应最大主键是12的记录,那如何找到最小记录9。解决办法是:通过槽3找到槽2对应的记录,也就是主键为8的记录。主键为8的记录的下一条记录的指针指向的就是槽3当中主键最小的9记录,这样就解决了这个问题。
- 然后开始向下搜索2次,定位到主键为11的记录,取出该条记录的信息即为我们想要查找的内容。
说完了怎么在页中找数据,说回最重点的,在B+树中我们怎么查询数据。以下我们以主键索引为例,如图:
可以看出,B+树的每一个节点就是一个数据页。只有叶子节点存储索引key + 数据,非叶子节点只存放索引key + 子节点的地址。
根据上图举个例子,讲讲如何在B+树中查找索引key为6的数据:
- 从根节点开始,通过刚才讲解的如何在页中进行数据查找的方法,查询到对应的分组,这个分组中有索引key1和索引key7,因为6比7小,比1大,所以到索引key1中指向的数据页30中去查找数据。
- 同样的方式定位反复迭代,最后定位到数据页16,再根据在页中查找数据的方式,获取到最终数据,如果是聚簇索引,data就是最终数据。如果是非聚簇索引,data就是主键,再拿着主键回到聚簇索引中查找即可,这个过程称之为回表。
索引常见面试题
索引是什么?
索引是数据的目录
索引如何分类?
我们可以按照四个角度来分类索引:
- 按数据结构分类:B+树索引表、哈希索引、全文索引
- 按物理存储分类:聚簇索引、非聚簇索引
- 按字段特性分类:主键索引、唯一索引、普通索引、联合索引
- 按字段个数分类:单列索引、联合索引
按数据结构分类
InnoDB默认采用的索引数据结构就是B+树,B+树的每一个节点都是一个数据页。
在InnoDB存储引擎中,B+树是一个多叉树,叶子节点中存放索引或者具体的数据,非叶子节点只存储索引,不存放具体的数据,并且每个节点中的索引都会按照索引值顺序存放。每个叶子节点中都有两个指针,分别指向上一个叶子节点和下一个叶子节点,因此所有叶子节点形成了一个双向链表。
为什么InnoDB选择了B+树作为索引的数据结构呢?讲一下以下数据结构在数据存储这方面的缺点就懂了:
1、B树
B+树只在叶子节点存储数据,而B树的非叶子节点也要存储数据,
另外,由于B+树采用双链表连接,相比于B树更适合做范围查询。
2、哈希
假如我们的查询是一个等值查询,显然只需要通过一次哈希运算,找到对应的bucket,就能定位到数据,时间复杂度为O(1)。
但是如果做范围查询呢?比如:
select * from user where id >= 1 and id <= 500
这种情况下,意味着我们需要从1到500做500次哈希运算,再找到对应的bucket定位数据,因此哈希不适合做范围查询。如果更进一步,不是像id这种整数,而是浮点数呢?比如这种:
select * from product where price >= 1 and price <= 500
显然这种情况下,哈希表数据结构就无能为力了。
3、二叉查找树