计算机存储结构如下图所示,从上到下依次为寄存器、高速缓存、主存储器、辅助存储器。其中主存储器,即我们常说的内存;辅助存储器也被称为外存,比较常见的就是磁盘、SSD等。在这个存储结构中,每一级存储的速度都比上一级慢很多,所以程序访问越上层存储中的数据,速度就会越快。
数据库索引因数据量较大,一般都是存储于外存中,而程序是在内存中执行的,这样就需要进行频繁的I/O操作,那么,为了减少I/O次数,该怎么做呢?我们知道,磁盘预读是按页操作的,如果每一页包含的信息量足够大,是不是就可以达成目的了。
索引设计需要考虑的第一个核心因素:保证每页包含尽可能多的“关键信息”,来减少磁盘I/O。
添加索引的目的,主要是为了提升数据库的查找速度。一般来说,可提升查找速度的数据结构有以下两种:
(1)哈希。比如HashMap,其查询、插入、删除的平均时间复杂度均是O(1);
(2)树。比如二叉查找树,其查询、插入、删除的平均时间复杂度均是O(log(n))。
可以看到,论时间复杂度,不管是读请求,还是写请求,哈希的性能会更好,可为什么DB却选择使用B+树呢?接下来,我将按“哈希表 -> 平衡二叉树 -> B树 -> B+树”的思路逐个进行分析。
索引设计需要考虑的第二个核心因素:结合DB各种搜索场景,选取更合适的数据存储结构。
假设采用HashMap存储,如果查询sql都是单行查询,比如
select * from user where name='zhangsan';
那么,采用哈希确实很快,但是,如果过滤条件是范围(<、>),排序(order by)等查询场景呢?其时间复杂度将退化为O(n)。假设我们采用的是“m叉查找树”,由于其本身是排好序的,其时间复杂度仍将是O(log(n)),即仍能保证其高效率。
所以,相比“m叉查找树”而言,后者更加合适。
哈希表:“指定数据”的定位较快,“范围查询”较慢。
平衡二叉树的结构如下图所示,可以认为它是升级版的二叉树,它有两个特征:
从图中可以看出,每个节点有且仅能存储一个记录,如果数据量大的话,树的高度将会很高,故而,当查询数据时,会产生很多次磁盘I/O。
相比哈希表而言,平衡二叉树支持范围查询,解决了哈希表的痛点。
B树的结构如下图所示,它有以下特点:
叶子节点和非叶子节点都存储数据(此特点会导致非叶子节点不能存储大量的索引)
采用中序遍历亦可获得所有节点。
从图中可以看出,每一个节点可以有多个子节点,且每一个节点(包括非叶子节点)均存储数据,采用中序遍历便可查找到所有数据。但是,数据库磁盘交互是按页为单位(MySQL默认为16K)的,如果数据量过多时,每个节点存储的键值会较少,进而树的高度比较高,导致磁盘I/O比较多。同时,在实际项目中,范围查询的SQL比较频繁,倘若采用B树作为索引结构,需要中序遍历很多节点,来收集符合筛选条件的数据集。因此,此结构某种程度来看,不是太合适。
从其特点可得知,它兼具了**“降低树高度,减少磁盘I/O”、“提升范围查询性能”**两个因素。
接下来,举一个例子来说明B+树怎么控制树的高度的。
我们假设一页大小是16KB,每个索引(主键)是bigint类型,即8B,指针为6B。那么每页能存储大约1000个索引(16KB/(8B+6B) \approx1000)。
那么,一颗3层的B+树能够存储多少索引呢?如下图:
大约能够存储10亿个索引。通常 B+ 树的高度在2-4层,由于 MySql 在运行时,根节点是常驻内存的,因此每次查找只需要大约2-3次IO。
结合索引的底层原理,我们在实际项目中构建索引时,需要注意以下几点:
拿到索引需要去磁盘中取数,所以谁的磁盘IO次数少,就用谁。
简单版本回答:
因为 B 树不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出),指针少的情况下要保存大量数据,只能增加树的高度,导致 IO 操作变多,查询性能变低。
详细版本回答:即本文内容。
问题来源:从一道索引数据结构面试题看B树、B+树
问题解答:联合索引在B+树上的结构介绍
联合索引(col1, col2,col3)也是一棵B+树,其非叶子节点存储的是第一个关键字的索引,而叶节点存储的则是三个关键字的数据,且按照col1、col2、col3的顺序进行排序。
col1表示的是年龄,col2表示的是姓氏,col3表示的是名字。如下图:
PS:对应地址指的是数据记录的地址。
如图,联合索引(年龄, 姓氏,名字),叶节点上data域存储的是三个关键字的数据。且是按照年龄、姓氏、名字的顺序排列的。
因此,如果执行的是:
select * from STUDENT where 姓氏='李' and 名字='安';
或者
select * from STUDENT where 名字='安';
那么当执行查询的时候,是无法使用这个联合索引的。因为联合索引中是先根据年龄进行排序的。如果年龄没有先确定,直接对姓氏和名字进行查询的话,就相当于乱序查询一样,因此索引无法生效。因此查询是全表查询。
如果执行的是:
select * from STUDENT where 年龄=1 and 姓氏='李';
那么当执行查询的时候,索引是能生效的,从图中很直观的看出,age=1的是第一个叶子节点的前6条记录,在age=1的前提下,姓氏=’李’的是前3条。因此最终查询出来的是这三条,从而能获取到对应记录的地址。
如果执行的是:
select * from STUDENT where 年龄=1 and 姓氏='黄' and 名字='安';
那么索引也是生效的。
而如果执行的是:
select * from STUDENT where 年龄=1 and 名字='安';
那么,索引年龄部分能生效,名字部分不能生效。也就是说索引部分生效。
因此我对联合索引结构的理解就是B+Tree是按照第一个关键字进行索引,然后在叶子节点上按照第一个关键字、第二个关键字、第三个关键字…进行排序。
最左原则
而之所以会有最左原则,是因为联合索引的B+Tree是按照第一个关键字进行索引排列的。
总结:
B-/B+树就是是为了磁盘或其它存储设备而设计的。
需要读写的数据多在磁盘中,需要多次读写磁盘文件(外存)时,适用B-树。
需要读写的数据多在内存中,需要在内存中多次读写数据时,适用红黑树。
B+树适用于数据库索引。
假定一个节点可以容纳100个值,那么3层的B树可以容纳100万个数据,如果换成二叉查找树(红黑树就是一种二叉查找树),则需要20层!假定操作系统一次读取一个节点,并且根节点保留在内存中,那么B树在100万个数据中查找目标值,只需要读取两次硬盘。