索引的出现其实就是为了提高数据查询的效率,就像书的目录一样。一本 500 页的书,如果你想快速找到其中的某一个知识点,在不借助目录的情况下是比较难找的。同样,对于数据库的表而言,索引其实就是它的目录,在我的另一篇Blog数据库索引中提到,当表上定义foreign key,unique, primary key时,系统会自动为其创建索引,当数据表被删除时索引自动被删除。由于本文主要学习自《极客时间-MySQL48讲》所以在撰写本文时会用到大量相关图片。
索引概述
这部分我们从使用层面上了解下,索引有哪些适用场景,又有哪些优缺点,以及索引的分类有哪些。
索引使用场景
数据库中如果不使用索引,大概率会非常慢,所以一定需要创建索引。索引是建立在数据库表中的某些列的上面。因此,在创建索引的时候,应该仔细考虑在哪些列上可以创建索引,在哪些列上不能创建索引。一般来说,应该在这些列 上创建索引,例如:
- 在经常需要搜索的列上,可以加快搜索的速度;
- 在作为主键的列上,强制该列的唯一性和组织表中数据的排列结构;
- 在经常用在连接的列上,这些列主要是一些外键,可以加快连接的速度;
- 在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;
- 在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;
- 在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。
总之就是在进行一些常用的查询和排序的过程中使用索引的场景比较多。
索引优缺点
我们知道在很多场合下会使用索引,看下索引的优点:
- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
- 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
- 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
- 在使用分组和排序 子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
- 通过使用索引,可以在查询的过程中**,使用优化隐藏器,提高系统的性能**。
当然是用索引也是有一定缺点的,不可能完美
- 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
- 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
- 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
也就是以空间时间换性能,维护成本和储存成本比较高,
聚簇索引和非聚簇索引
聚簇索引和非聚簇索引是mysql数据库中两种主要的索引方式,不同的存储引擎支持索引类型是不一样,那这两种索引有什么不同呢?
聚簇索引
一种索引,该索引中键值的逻辑顺序决定了表中相应行的物理顺序。聚簇索引与字典相同,字典按字母顺序排列数据。由于聚集索引规定数据在表中的物理存储顺序,因此一个表只能包含一个聚集索引。但该索引可以包含多个列(联合索引),就像电话簿按姓氏和名字进行组织一样
其实,我们的汉语字典的正文本身就是一个聚集索引。比如,我们要查“安”字,就会很自然地翻开字典的前几页,因为“安”的拼音是“an”,而按照拼音排序汉字的字典是以英文字母“a”开头并以“z”结尾的,那么“安”字就自然地排在字典的前部。如果翻完了所有以“a”开头的部分仍然找不到这个字,那么就说明字典中没有这个字;同样的,如果查“张”字,那也会将的字典翻到最后部分,因为“张”的拼音是“zhang”。也就是说,字典的正文部分本身就是一个目录,不需要再去查其他目录来找到需要找的内容。我们把这种正文内容本身就是一种按照一定规则排列的目录称为聚集索引
非聚簇索引
一种索引,该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同
如果认识某个字,可以快速地从自动中查到这个字。但也可能会遇到不认识的字,不知道它的发音,这时候就不能按照刚才的方法找到要查的字,而需要去根据“偏旁部首”查到要找的字,然后根据这个字后的页码直接翻到某页来找到要找的字。但结合“部首目录”和“检字表”而查到的字的排序并不是真正的正文的排序方法,比如查“张”字,我们可以看到在查部首之后的检字表中“张”的页码是672页,检字表中“张”的上面是“驰”字,但页码却是63页,“张”的下面是“弩”字,页面是390页。很显然,这些字并不是真正的分别位于“张”字的上下方,现在看到的连续的“驰、张、弩”三字实际上就是他们在非聚集索引中的排序,是字典正文中的字在非聚集索引中的映射。我们可以通过这种方式来找到所需要的字,但它需要两个过程,先找到目录中的结果,然后再翻到所需要的页码。我们把这种目录纯粹是目录,正文纯粹是正文的排序方式称为非聚集索引。
二者的区别与联系
我们可以这么理解:聚簇索引索引的叶节点就是数据节点。而非聚簇索引的叶节点仍然是索引节点,只不过节点上存储一个指向对应的数据块的指针
索引实现原理
这部分我们从原理层面上了解下,索引的通用构建模型是什么样的,MySQL的默认存储引擎InnoDB使用的索引模型又是什么样的。
索引常见模型
索引的出现是为了提高查询效率,但是实现索引的方式却有很多种,所以这里也就引入了索引模型的概念。可以用于提高读写效率的数据结构很多,这里介绍三种常见、也比较简单的数据结构,它们分别是哈希表、有序数组和搜索树
哈希表
哈希表是一种以键 - 值(key-value)存储数据的结构,只要输入待查找的键即 key,就可以找到其对应的值即 Value。哈希的思路很简单,把值放在数组里,用一个哈希函数把 key 换算成一个确定的位置,然后把 value 放在数组的这个位置。不可避免地,多个 key 值经过哈希函数的换算,会出现同一个值的情况。处理这种情况的一种方法是,拉出一个链表。以下是一个按照身份证号查名字的示例:
需要注意的是,图中四个 ID_card_n 的值并不是递增的,而且同一个链表上的User2也不会与User4比较就直接追加到链表尾部,这样做的好处是增加新的 User 时速度会很快,只需要往后追加。但缺点是,因为不是有序的,所以哈希索引做区间查询的速度是很慢的。所以哈希表这种结构适用于只有等值查询的场景
这种模型使用场景较少,其优点和应用场景如下:
- 优点:Hash 索引结构的特殊性,其检索效率非常高,索引的检索可以一次定位,不像B+Tree 索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,所以 Hash 索引的查询效率要远高于B+Tree 索引。时间复杂度是O(1)
- 应用场合:依赖于这种单值查找的系统被称为 键-值存储;对于这种系统,尽可能地使用 hash 索引
缺点不言而喻,应用场合太小了,只有做等值查询时候才有意义,区间查询时效率极差。
有序数组
有序数组在等值查询和范围查询场景中的性能就都非常优秀。还是上面这个根据身份证号查名字的例子,如果我们使用有序数组来实现的话,示意图如下所示
这里我们假设身份证号没有重复,这个数组就是按照身份证号递增的顺序保存的。
- 等值查询,这时候如果你要查 ID_card_n2 对应的名字,用二分法就可以快速得到,这个时间复杂度是 O(log(N))
- 区间查询,要查身份证号在[ID_card_X, ID_card_Y]区间的 User,可以先用二分法找到 ID_card_X(如果不存在 ID_card_X,就找到大于 ID_card_X 的第一个 User),然后向右遍历,直到查到第一个大于 ID_card_Y 的身份证号,退出循环
如果仅仅看查询效率,有序数组就是最好的数据结构了。但是,在需要更新数据的时候就麻烦了,你往中间插入一个记录就必须得挪动后面所有的记录,成本太高(需要O(n)的时间复杂度),所以,有序数组索引只适用于静态存储引擎,比如你要保存的是 2017 年某个城市的所有人口信息,这类不会再修改的数据
搜索树
二叉搜索树也是经典数据结构了,这里简单回顾下。Binary Sort Tree,二叉搜索树或者是一棵空树,或者是具有下列性质的二叉树:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
- 它的左、右子树也分别为二叉排序树。
- 没有键值相等的节点。
二叉搜索树是按照关键升序排列,对每一个节点来说,比它节点值高的节点是它的中序后继,所以对二叉查找树进行中序遍历(左根右),即可得到有序的数列。它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡,所以我们这里也需要数是平衡的,也就是二叉搜索平衡树。
二叉搜索平衡树
为了保证插入效率,使用了平衡树,平衡二叉树(Self-balancing binary search tree)又被称为AVL树(有别于AVL算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树
还是上面根据身份证号查名字的例子,如果我们用二叉搜索树来实现的话,示意图如下所示
这样如果你要查 ID_card_n2 的话,按照图中的搜索顺序就是按照 UserA -> UserC -> UserF -> User2 这个路径得到。这个时间复杂度是 O(log(N)),当然为了维持 O(log(N)) 的查询复杂度,你就需要保持这棵树是平衡二叉树。为了做这个保证,更新的时间复杂度也是 O(log(N))
多叉搜索树
树可以有二叉,也可以有多叉。多叉树就是每个节点有多个儿子,儿子之间的大小保证从左到右递增。二叉树是搜索效率最高的,但是实际上大多数的数据库存储却并不使用二叉树。其原因是,索引不止存在内存中,还要写到磁盘上,因为二叉树树高过高,每次查询都需要访问过多节点,即访问数据块过多,而从磁盘随机读取数据块过于耗时
为了让一个查询尽量少地读磁盘,就必须让查询过程访问尽量少的数据块,那么,我们就不应该使用二叉树,而是要使用N叉树。
以 InnoDB 的一个整数字段索引为例,这个 N 差不多是 1200。这棵树高是 4 的时候,就可以存 1200 的 3 次方个值,这已经 17 亿了。考虑到树根的数据块总是在内存中的,一个 10 亿行的表上一个整数字段的索引,查找一个值最多只需要访问 3 次磁盘。其实,树的第二层也有很大概率在内存中,那么访问磁盘的平均次数就更少了
InnoDB索引模型
在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。又因为前面我们提到的,InnoDB 使用了 B+ 树索引模型,所以数据都是存储在 B+ 树中的,每一个索引在 InnoDB 里面对应一棵 B+ 树
B+树结构
一个m阶的B+树具有如下几个特征:
- 有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
- 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
- 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素
下图为一个B+树的结构:
在等值查询的时候:
区间查询:
B+树的优势显而易见:
- 单一节点存储更多的元素,使得查询的IO次数更少。
- 所有查询都要查找到叶子节点,查询性能稳定。
- 所有叶子节点形成有序链表,便于范围查询。
需要注意数据库的聚集索引(Clustered Index)中,叶子节点直接包含卫星数据。在非聚集索引(NonClustered Index)中,叶子节点带有指向卫星数据的指针