为什么InnoDB存储引擎采用B+树索引模型,这篇来总结下B+的原理。在MySQL中,主要有四种类型的索引:B-Tree、Hash、FullText和R-Tree。B-Tree索引是很多数据库管理系统中最主要的索引类型,主要是因为B-Tree索引的存储结构在数据库的检索中有非常优异的表现。其中InnoDB使用的是B+Tree,是在B-Tree基础上做了很小的改造。
索引是一种数据结构,用于帮助我们在大量数据中(数据量大了索引才显得有意义)快速定位到我们想要查找的数据。 索引最形象的比喻就是图书的目录。
索引按数据结构分可分为哈希表,有序数组,搜索树,跳表:
MySQL索引使用的数据结构主要有BTree索引 和 哈希索引 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快。其余大部分场景,建议选择BTree索引。
哈希索引存在一些缺点:
MySQL的索引模型使用的是B树中的B+Tree,但对于主要的两种存储引擎的实现方式是不同的:
MyISAM: B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。聚集索引和非聚集索引的叶子节点都会存储数据的文件地址。
InnoDB: 其数据文件本身就是索引文件。数据即索引,索引即数据。相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引(或聚集索引)”。而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方。在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。
B+树索引,是从二叉查找树、平衡二叉树和B树这三种数据结构演化来的。
首先,让我们先看一张图:
从图中可以看到,为user表建立了一个二叉查找树的索引。图中的圆为二叉查找树的节点,节点中存储了键(key)和数据(data)。
键对应user表中的id,数据对应user表中的行数据。二叉查找树的特点就是任何节点的左子节点的键值都小于当前节点的键值,右子节点的键值都大于当前节点的键值。 顶端的节点我们称为根节点,没有子节点的节点我们称之为叶节点。
如果需要查找id=12的用户信息,利用创建的二叉查找树索引,查找流程如下:
将根节点作为当前节点,把12与当前节点的键值10比较,12大于10,然后把当前节点的右子节点作为当前节点。
继续把12和当前节点的键值13比较,发现12小于13,把当前节点的左子节点作为当前节点。
把12和当前节点的键值12对比,12等于12,满足条件,从当前节点中取出data,即id=12,name=xm。
利用二叉查找树只需要3次即可找到匹配的数据。如果在表中一条条的查找的话,我们需要6次才能找到。
利用二叉查找树可以快速的找到数据。但是,如果二叉查找树是这样的构造:
可以看到二叉查找树变成了一个链表。如果查找id=17的用户信息,需要查找7次,相当于全表扫描。
导致这个现象的原因其实是二叉查找树变得不平衡了,也就是高度太高了,从而导致查找效率的不稳定。
为了解决这个问题,我们需要保证二叉查找树一直保持平衡,就需要用到平衡二叉树了。
平衡二叉树又称AVL树,在满足二叉查找树特性的基础上,要求每个节点的左右子树的高度差不能超过1。
平衡二叉树和非平衡二叉树的对比:
由平衡二叉树的构造可以发现第一张图中的二叉树是一棵平衡二叉树。
平衡二叉树保证了树的构造是平衡的,当我们插入或删除数据导致平衡二叉树不平衡时,平衡二叉树会进行调整树上的节点来保持平衡。
平衡二叉树相比于二叉查找树来说,查找效率更稳定,总体的查找速度也更快。
和内存相比,从磁盘中读取数据的速度会慢上百倍千倍甚至万倍,所以,应当尽量减少从磁盘中读取数据的次数。 另外,从磁盘中读取数据时,都是按照磁盘块来读取的。
如果用树作为索引的数据结构,那每查找一次数据就需要从磁盘中读取一个节点,也就是我们说的一个磁盘块,因为平衡二叉树是每个节点只存储一个键值和数据,说明每个磁盘块只存储一个键值和数据!所以如果要存储海量数据,平衡二叉树的节点会很多,高度也会很高,查找数据时就会进行多次IO,效率会很低。
所以,为了解决平衡二叉树的这个弊端,应该寻找一种单个节点可以存储多个键值和数据的平衡树。也就是接下来要说的B树。
B树(Balance Tree)即为平衡树的意思,如图所示:
图中的p为指向子节点的指针,二叉查找树和平衡二叉树其实也有。图中的每个节点称为页,页就是上面说的磁盘块,因为mysql中数据读取的基本单位都是页,所以叫做页更符合mysql中索引的底层数据结构。
从上图可以看出,B树相对于平衡二叉树,每个节点存储了更多的键值(key)和数据(data),并且每个节点拥有更多的子节点,子节点的个数一般称为阶,上述图中的B树为3阶B树,高度也会很低。
基于这个特性,B树查找数据读取磁盘的次数将会很少,数据的查找效率也会比平衡二叉树高很多。
假如要查找id=28的记录,那么在B树中查找的流程如下:
是一种多路搜索树,有如下特征:
1、任意非叶子节点最多只有M个子节点,且M>2;
2、根节点的子节点数为[2,M];
3、除根节点以外的非叶子节点的子节点数为[M/2,M];
4、每个节点存放至少M/2-1(往上取整)和至多M-1个关键字(至少2个关键字);
5、非叶子节点的关键字个数 = 指向子节点的指针个数 - 1;
6、非叶子节点的关键字:K[1],K[2],…,K[M - 1];且K[i] < K[i + 1];
7、非叶子节点的指针:P[1],P[2],…,P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M - 1]的子树,其中P[i]指向关键字属于(K[i - 1],K[i])的子树;
8、所有叶子结点位于同一层;
如:(M=3)
B-树的搜索,从根节点开始,对节点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子节点;重复,直到所对应的子节点指针为空,或已经是叶子结点;
B-树的特性:
1、关键字集合分布在整棵树中;
2、任何一个关键字出现且只出现在一个结点中;
3、搜索有可能在非叶子结点结束;
4、其搜索性能等价于在关键字全集内做一次二分查找;
5、自动层次控制;
由于限制了除根节点以外的非叶子结点,至少有M/2个子节点,确保了结点的最低利用率。所以B-树的性能总是等价于二分查找(与M值无关),也就没有B树平衡的问题。
由于M/2的限制,在插入结点时,如果结点已满,需要将结点分裂为两个各占M/2的结点;删除结点时,需将两个不足M/2的兄弟节点合并;
B+树结构图:
B+树是B-树的变体,也是多路搜索树,定义和B-基本相同,除了:
1、非叶子结点的子树指针与关键字个数相同;
2、非叶子结点的子树指针P[i],指向关键字值属于[K[i],K[i + 1]]的子树(B-树是开区间);
3、为所有叶子结点增加一个链指针;
4、所有关键字都在叶子结点中。
如:(M = 3)
B+的搜索过程与B-基本相同,区别是B+树只有达到叶子结点才命中,其性能也等价于在关键字全集做一次二分查找;
B+的特性:
1、所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
2、不可能在非叶子节点命中;
3、非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
4、更适合文件索引系统;
B+树非叶子节点上是不存储数据的,仅存储键值,而B树节点中不仅存储键值,也会存储数据。之所以这么做是因为在数据库中页的大小是固定的,innodb中页的默认大小是16KB。如果不存储数据,就可以存储更多的键值,相应的树的阶数(节点的子节点数)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的IO次数会再次减少,数据查询的效率也会更快。另外,B+树的阶数是等于键值的数量的,如果我们的B+树一个节点可以存储1000个键值,那么3层B+树可以存储1000×1000×1000=10亿个数据。一般根节点是常驻内存的,所以一般我们查找10亿数据,只需要2次磁盘IO。
因为B+树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的。那么B+树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。而B树因为数据分散在各个节点,要实现这一点是很不容易的。
B+树中各个页之间是通过双向链表连接的,叶子节点中的数据是通过单向链表连接的。B树我们也可以对各个节点加上链表。其实这些不是它们之前的区别,是因为在innodb存储引擎中,索引就是这样存储的,准确的说应该是聚集索引。
如图:浅蓝色的块叫磁盘块,每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1包含数据项17和35、指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。真实的数据存放在叶子结点:3、5、9、10、13、15、28、29、36、60、75、79、90、99。非叶子结点只存储数据项和指针,如17、35并不存在于真实数据中。
B+树的查找过程:
如图,如果要查找数据项29,那么首先会把磁盘块1从磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘IO)可以忽略,通过磁盘块1的P2指针的磁盘地址把磁盘块P3从磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中进行二分查找找到29,结束查询,总共3次IO。
真实的情况是,3层的B+树可以表示上百万的数据,如果上百万的数据查找只需要3次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,显然成本很高。
B+树的性质:
1、从上面的分析,得知==IO次数取决于B+树的高度h,假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h = log(m + 1)N,当数据量N一定,m越大,h越小;而m = 磁盘块的大小/数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。==这就是为什么每个数据项,即索引字段要尽量小,比如int占4个字节,bigint占8个字节,满足存储的情况下选择int;这也就是为什么B+树要求把真实的数据放在叶子结点,磁盘块的数据项可以放更多,树高度也就矮胖了,如果放在非叶子结点,数据项等于1时会退化成线性表。
2、当B+树的数据项是复合的数据结构,比如(name,age,sex)的时候,B+树是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,B+树会优先比较name来确定下一步的搜索方向,如果name相同再依次比较age和sex,最后得到检索的数据;但是当(20,F)这样的没有name的数据,B+树就不知道下一步该查哪个节点,因为建立搜索树的时候name是第一个比较因子,必须先根据name来搜索才知道下一步的搜索方向。比如当(张三,F)这样的数据,B+树可以用name来指定搜索方向,但下一个字段age缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据,这就是索引的最左前缀匹配原则
一颗B+树最多可以存放多少条数据:
答案是两千万左右。
在MySQL中,B+树索引按照存储方式的不同分为聚集索引和非聚集索引。
聚集索引(聚簇索引、主键索引):以innodb作为存储引擎的表,表中的数据都会有一个主键,即使你不创建主键,系统也会帮你创建一个隐式的主键。这是因为innodb是把数据存放在B+树中的,而B+树的键值就是主键,在B+树的叶子节点中,存储了表中所有的数据。这种以主键作为B+树索引的键值而构建的B+树索引,我们称之为聚集索引。
非聚集索引(非聚簇索引、非主键索引):以主键以外的列值作为键值构建的B+树索引,称之为非聚集索引。非聚集索引与聚集索引的区别在于非聚集索引的叶子节点不存储表中的数据,而是存储该列对应的主键,想要查找数据还需要根据主键再去聚集索引中进行查找,这个过程,称为回表,但是如果要查询的列都是索引列(通过覆盖索引),就不需要回表,跟主键索引查找一样,查一次就OK。
还是这张B+树索引图,这就是聚集索引。假设要查找id>=18并且id<40的用户数据,sql语句为select * from user where id>=18 and id <40,id为主键。具体的查找过程如下:
一般根节点都是常驻内存的,直接从内存中读取。
从内存中读取到页1,要查找id>=18 and id <40的范围值,我们首先需要找到id=18的键值。
从页1中可以找到键值18,此时需要根据指针p2,定位到页3。
从页3中查找数据,需要拿着p2指针去磁盘中进行读取页3。
从磁盘中读取页3后将页3放入内存中,然后进行查找,可以找到键值18,然后再拿到页3中的指针p1,定位到页8。
同样的再去磁盘中将页8读取到内存中。
因为页中的数据是链表进行连接的,而且键值是按照顺序存放的,此时可以根据二分查找法定位到键值18。此时因为已经找到一条满足条件的数据,就是键值18对应的数据。因为是范围查找,而且此时所有的数据都存在于叶子节点中,并且是有序排列的,那么我们就可以对页8中的键值依次进行遍历查找并匹配满足条件的数据。我们可以一直找到键值为22的数据,然后页8中就没有数据了,此时我们需要拿着页8中的p指针去读取页9中的数据。
同样的再去磁盘中将页9读取到内存中,并通过和页8中一样的方式进行数据的查找,直到将页12加载到内存中,发现41大于40,此时不满足条件,查找到此终止。
最终找到满足条件的所有数据为:(18,kl),(19,kl),(22,hj),(24,io),(25,vg),(29,jk),(31,jk),(33,rt),(34,ty),(35,yu),(37,rt),(39,rt)。总共12条记录。
首先,这个非聚集索引表示的是用户幸运数字的索引(为什么是幸运数字?一时兴起想起来的:-)),此时表结构是这样的:
在叶子节点中,不在存储所有的数据了,存储的是键值和主键。
对于叶子节点中的x-y,比如1-1。左边的1表示的是索引的键值,右边的1表示的是主键值。如果我们要找到幸运数字为33的用户信息,对应的sql语句为select * from user where luckNum=33。
查找的流程跟聚集索引一样,就不介绍了。最终会找到主键值47,找到主键后需要再到聚集索引中查找具体对应的数据信息,此时又回到了聚集索引的查找流程。 查找流程图:
innoDB 是按数据页来读写数据的,当要读取一条数据的时候是先将本页数据全部读入内存,然后找到对应数据,而不是直接读取,每页数据的默认大小为 16KB。
当一个数据页需要更新的时候,如果内存中有该数据页就直接更新,如果没有该数据页则在不影响数据一致性的前提下将更新操作先缓存到 change buffer 中,在下次查询需要访问这个数据页的时候再写入更新操作,除了查询会将 change buffer 写入磁盘,后台线程也会定期将 change buffer 写入到磁盘中。对于唯一索引来说,所有的更新操作都要先判断这个操作是否会违反唯一性约束,因此唯一索引的更新无法使用 change buffer 而普通索引可以,唯一索引更新比普通索引更新多一个唯一性校验的过程。