1. 引子
InnoDB存储引擎支持以下几种常见的索引:
❑B+树索引
❑全文索引
❑哈希索引
2. B+树索引
在介绍B+树索引之前先介绍与之密切相关的一些算法与数据结构,帮助理解。
2.1 数据结构与算法
2.1.1 二分查找法
二分查找法(binary search)也称为折半查找法,用来查找一组有序的记录数组中的某一记录,其基本思想是:将记录按有序化(递增或递减)排列,在查找过程中采用跳跃式方式查找,即先以有序数列的中点位置为比较对象,如果要找的元素值小于该中点元素,则将待查序列缩小为左半部分,否则为右半部分。通过一次比较,将查找区间缩小一半。
2.1.2 二叉查找树
在介绍B+树前,需要先了解一下二叉查找树。B+树是通过二叉查找树,再由平衡二叉树,B树演化而来。相信在任何一本有关数据结构的书中都可以找到二叉查找树的章节,二叉查找树是一种经典的数据结构。
图中的数字代表每个节点的键值,在二叉查找树中,左子树的键值总是小于根的键值,右子树的键值总是大于根的键值。
二叉查找树可以任意地构造,同样是2、3、5、6、7、8这五个数字,也可以按照下图的方式建立二叉查找树。
此图的平均查找次数和顺序查找差不多。显然这棵二叉查找树的查询效率就低了。因此若想最大性能地构造一棵二叉查找树,需要这棵二叉查找树是平衡的,从而引出了新的定义——平衡二叉树,或称为AVL树。
平衡二叉树的定义如下:首先符合二叉查找树的定义,其次必须满足任何节点的两个子树的高度最大差为1。
平衡二叉树的查询速度的确很快,但是维护一棵平衡二叉树的代价是非常大的。通常来说,需要1次或多次左旋和右旋来得到插入或更新后树的平衡性。对于图一所示的平衡树,当用户需要插入一个新的键值为9的节点时,需做如图所示的变动:
这里通过一次左旋操作就将插入后的树重新变为平衡的了。但是有的情况可能需要多次。除了插入操作,还有更新和删除操作,不过这和插入没有本质的区别,都是通过左旋或者右旋来完成的。因此对一棵平衡树的维护是有一定开销的,不过平衡二叉树多用于内存结构对象中,因此维护的开销相对较小。
2.1.3 B树
B树的结构如下:
可以看到非叶子节点中也保存了数据,另外黄色的部分表示指向子节点的指针。另外B树相比于二叉树的特点是一个节点可以保存多个数据。
2.1.4 B+树
在B+树中,所有记录节点都是按键值的大小顺序存放在同一层的叶子节点上,由各叶子节点指针进行连接。先来看一个B+树,其高度为2,每页可存放4条记录,如图一所示。
B+树和B树的区别?
- B+树的数据都保存在叶子节点中,非叶子节点只保存索引;B树的数据既保存在叶子节点,也保存在非叶子节点。
- B+树的叶子节点之间通过指针相互连接,方便直接在叶子节点中进行指定范围内的查询;B+树的叶子节点之间并为连接。
2.1.3.1 B+树的插入操作
B+树的插入必须保证插入后叶子节点中的记录依然排序,同时需要考虑插入到B+树的三种情况,每种情况都可能会导致不同的插入算法。
这里用一个例子来分析B+树的插入。例如,对于图5-6中的这棵B+树,若用户插入28这个键值,发现当前Leaf Page和Index Page都没有满,直接进行插入即可,之后得图二:
接着再插入70这个键值,这时原先的Leaf Page已经满了,但是Index Page还没有满,符合表5-1的第二种情况,这时插入Leaf Page后的情况为55、55、60、65、70,并根据中间的值60来拆分叶子节点,可得图三:
最后插入键值95,这时符合表中讨论的第三种情况,即Leaf Page和Index Page都满了,这时需要做两次拆分,得到图四:
可以看到,不管怎么变化,B+树总是会保持平衡。但是为了保持平衡对于新插入的键值可能需要做大量的拆分页(split)操作。因为B+树结构主要用于磁盘,页的拆分意味着磁盘的操作,所以应该在可能的情况下尽量减少页的拆分操作。因此,B+树同样提供了类似于平衡二叉树的旋转(Rotation)功能。
旋转发生在Leaf Page已经满,但是其的左右兄弟节点没有满的情况下。这时B+树并不会急于去做拆分页的操作,而是将记录移到所在页的兄弟节点上。在通常情况下,左兄弟会被首先检查用来做旋转操作,因此再来看图二的情况,若插入键值70,其实B+树并不会急于去拆分叶子节点,而是去做旋转操作,得到如图五所示的操作:
可以看到通过将记录移到兄弟节点上后,省去了拆分的操作。
2.2 B+树索引
B+树索引的本质就是B+树在数据库中的实现。但是B+索引在数据库中有一个特点是高扇出性,因此在数据库中,B+树的高度一般都在2~4层,这也就是说查找某一键值的行记录时最多只需要2到4次IO,这倒不错。
数据库中的B+树索引可以分为聚集索引(clustered inex)和辅助索引(secondary index) ,但是不管是聚集还是辅助的索引,其内部都是B+树的,即高度平衡的,叶子节点存放着所有的数据。聚集索引与辅助索引不同的是,叶子节点存放的是否是一整行的信息。
2.2.1 聚集索引
之前已经介绍过了,InnoDB存储引擎表是索引组织表,即表中数据按照主键顺序存放。而聚集索引(clustered index)就是按照每张表的主键构造一棵B+树,将一个表的全部行数据分到所有叶子节点中进行保存。一张表只能有一个聚集索引。
优点如下:
对于主键的排序查找和范围查找速度非常快。叶子节点的数据就是用户所要查询的数据。如用户需要查询一张注册用户的表,查询最后注册的10位用户,由于B+树索引是双向链表的,用户可以快速找到最后一个数据页,并取出10条记录。
范围查询(range query),即如果要查找主键某一范围内的数据,通过叶子节点的上层中间节点就可以得到页的范围,之后直接读取数据页即可。
2.2.2 辅助索引
对于辅助索引(Secondary Index,也称非聚集索引),叶子节点并不包含行记录的全部数据。叶子节点除了包含键值以外,每个叶子节点中的索引行中还包含了一个书签(bookmark)。该书签用来告诉InnoDB存储引擎哪里可以找到与索引相对应的行数据。由于InnoDB存储引擎表是索引组织表,因此InnoDB存储引擎的辅助索引的书签就是相应行数据的聚集索引键。下图显示了InnoDB存储引擎中辅助索引与聚集索引的关系。
辅助索引的存在并不影响数据在聚集索引中的组织,因此每张表上可以有多个辅助索引。当通过辅助索引来寻找数据时,InnoDB存储引擎会遍历辅助索引并通过叶级别的指针获得指向主键索引的主键,然后再通过主键索引来找到一个完整的行记录。举例来说,如果在一棵高度为3的辅助索引树中查找数据,那需要对这棵辅助索引树遍历3次找到指定主键,如果聚集索引树的高度同样为3,那么还需要对聚集索引树进行3次查找,最终找到一个完整的行数据所在的页,因此一共需要6次逻辑IO访问以得到最终的一个数据页。
聚集索引和辅助索引的区别?
- 最主要的区别就是聚集索引的叶子节点保存了最终的数据,而辅助索引的叶子节点保存了指向数据的指针,最终数据还得靠这个指针继续去查询。
- Mysql数据库中,InnoDB引擎使用的聚集索引,MyISAM引擎使用的是辅助索引。MyISAM引擎实现中,索引文件和数据文件是分开的,而InnoDB引擎中索引和数据是放同一个文件中的。
上述的解释并不意味着聚集索引一定比辅助索引快。比如使用select count(*) from table;
这种命令的时候,实际mysql内部最终并不是通过聚集索引实现查询的,而是内部优化器选用辅助索引实现查询。这样做的原因是该查询并不查询数据,只是查询数据条数,相比于聚集索引,辅助索引由于只保存了索引值,而不保存数据值,因此占用空间更小,查找更快。
2.2.3 联合索引
联合索引是指对表上的多个列进行索引。前面讨论的情况都是只对表上的一个列进行索引。联合索引的创建方法与单个索引创建的方法一样,不同之处仅在于有多个索引列。例如以下代码创建了一张t表,并且索引idx_a_b是联合索引,联合的列为(a,b):
CREATE TABLE t(
a INT,
b INT,
PRIMARY KEY(a),
KEY idx_a_b(a,b)
)ENGINE=INNODB
那么何时需要使用联合索引呢?在讨论这个问题之前,先来看一下联合索引内部的结果。从本质上来说,联合索引也是一棵B+树,不同的是联合索引的键值的数量不是1,而是大于等于2。接着来讨论两个整型列组成的联合索引,假定两个键值的名称分别为a、b,如图:
从图可以观察到多个键值的B+树情况。其实和之前讨论的单个键值的B+树并没有什么不同,键值都是排序的,通过叶子节点可以逻辑上顺序地读出所有数据,就上面的例子来说,即(1,1)、(1,2)、(2,1)、(2,4)、(3,1)、(3,2)。数据按(a,b)的顺序进行了存放(key中的两个数字都进行了排序)。
最左匹配原则
联合索引有一个原则:最左匹配原则。如上述示例中我们创建了一个由a和b组成的索引,如果查询语句写作如下:
select * from t where a='1' and b='2';
那么会使用联合索引进行进行查询,如果只写了a,也是会使用联合索引:
select * from t where a= '1';
但是如果只写了b作为查询条件,那么就不会使用联合索引:
select * from t where b='2';
除此之外,最左匹配原则还有下面几个特点:
- 通过最左匹配原则,mysql会一直向右匹配直到遇到范围查询(>,<,between,like)就会停止匹配,比如
a=3 and b=4 and c<5 and d=6
如果建立(a,b,c,d)的索引,d实际是用不到索引的,如果a=3 and b=4 and d=6 nad c<5
则建立(a,b,c,d)的索引,那abcd都用到了索引进行查询。 - =和in可以乱序,不影响索引的建立,如
a = 3 and b=4 and c=5 and d=6
这种情况下abcd的条件都会顺便换位置,不影响。可以乱序的原因是:mysql优化器会对sql进行优化,重新将顺序调整为设置索引的顺序。
3. 全文索引
3.1 概述
通过前面章节的介绍,已经知道B+树索引的特点,可以通过索引字段的前缀(prefix)进行查找。例如,对于下面的查询B+树索引是支持的:
SELECT * FROM blog WHERE content like'xxx%'
上述SQL语句可以查询博客内容以xxx开头的文章,并且只要content添加了B+树索引,就能利用索引进行快速查询。然而实际这种查询不符合用户的要求,因为在更多的情况下,用户需要查询的是博客内容包含单词xxx的文章,即:
SELECT * FROM blog WHERE content like'%xxx%'
根据B+树索引的特性,上述SQL语句即便添加了B+树索引也是需要进行索引的扫描来得到结果。类似这样的需求在互联网应用中还有很多。例如,搜索引擎需要根据用户输入的关键字进行全文查找,电子商务网站需要根据用户的查询条件,在可能需要在商品的详细介绍中进行查找,这些都不是B+树索引所能很好地完成的工作。
全文检索(Full-Text Search)是将存储于数据库中的整本书或整篇文章中的任意内容信息查找出来的技术。它可以根据需要获得全文中有关章、节、段、句、词等信息,也可以进行各种统计和分析。
3.2 倒排索引
全文检索通常使用倒排索引(inverted index)来实现。倒排索引同B+树索引一样,也是一种索引结构。它在辅助表(auxiliary table)中存储了单词与单词自身在一个或多个文档中所在位置之间的映射。这通常利用关联数组实现,其拥有两种表现形式:
❑inverted file index,其表现形式为{单词,单词所在文档的ID}
❑full invertedindex,其表现形式为{单词,(单词所在文档的ID,在具体文档中的位置)}
4. 哈希索引
哈希索引即先通过哈希算法得出哈希值作为地址再进行保存。使用哈希索引理论上复杂度为1,比B+树的效率更高,但是却不常使用作为数据的索引实现。原因在于哈希索引内部查询条件为哈希值,由于没有顺序,所以不能进行范围查找,而只能使用等于查找,同时也没有asc,desc等排序功能。
哈希索引可能存在的另外一个问题是:遇到大量Hash值相同的情况下,效率不一定会比B+树索引高,即极端情况下会变成线性结构。
5. 总结
索引是建的越多越好吗?
不是。
- 数据量小的表不需要建立索引,建立索引会增加额外的开销。
- 数据变更需要维护索引,更多的索引意味着更多的维护成本
- 更多的索引意味着更多的空间