文章目录
- 索引的定义
- 索引的常见模型
- 哈希表
- 有序数组
- 二叉搜索树
- InnoDB的索引模型
- 索引维护
- 页分裂
- 页合并
- 页分裂和页合并的影响
- 避免页分裂
- 覆盖索引
- 最左前缀原则
- 索引下推
索引的定义
索引的出现其实就是为了提高数据查询的效率,就像书的目录一样。一本500页的书,如果你想快速找到其中的某一个知识点,在不借助目录的情况下,那寻找起来是很费劲的。同样,对于数据库的表而言,索引其实就是它的“目录”。
索引的常见模型
哈希表
哈希表是一种以**键 - 值(key - value)**存储数据的结构,只要输入待查找的键即key,就可以找到其对应的值即Value。哈希的思路很简单,把值放在数组里,用一个哈希函数把key换算成一个确定的位置,然后把value放在数组的这个位置。
不可避免地,多个key值经过哈希函数的换算,会出现同一个值的情况。处理这种情况的一种方法是,拉出一个链表。
假设,现在维护者一个身份证信息和姓名的表,需要根据身份证号查找对应的名字,这是对应的哈希表索引的示意图如下:
图中,USER2和USER4根据身份证号算出来的值都是N,但没关系,后面还跟了一个链表。假设要查找ID_2对应的名字,处理步骤就是首先,将ID_2通过哈希函数算出N;然后,按顺序遍历,找到USER2。
需要注意的是,图中四个ID_N的值并不是递增的,这样做的好处是增加新的User时速度会很快,只需要往后追加。但缺点是,因为不是有序的,所以哈希索引做区间查询速度很慢,要全表扫描。
所以,哈希表这种结构适用于只有等值查询的场景,比如Memcached以及其他一些NoSQL引擎。
有序数组
有序数组在等值查询和范围查询场景中的性能就都非常优秀。还是这个根据身份证查名字的例子,如果使用有序数组来实现的话,示意图如下所示:
假设身份证号没有重复,这个数组就是按照身份证号递增的顺序保存的。这个时候如果要查ID_2对应的名字,用二分法就可以快速得到,这个时间复杂度是O(log(N))
如果仅仅看查询效率,有序数组就是最好的数据机构了。但是,在需要更新数据的时候就麻烦了,往中间插入一个记录就必须得挪动后面所有的记录,成本太高。
所以,有序数组索引只适用于静态存储引擎,比如你要保存的是某一年某个城市的所有人口信息,这类不会再修改的数据。
二叉搜索树
二叉搜索树的特点是:父节点左子树左右节点的值小于父节点的值,右子树所有节点的值大于父节点的值。这样如果要查询ID_2的话,按照图中的搜索顺序就是按照USER_A -> USER_C -> USER_F -> USER_2这个路径得到,时间复杂度是O(log(N)).
InnoDB的索引模型
在InnoDB中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。又因为前面我们提到的,InnoDB使用B+树索引模型,所以数据都是存储在B+树中的。
每一个索引在InnoDB里面对应一颗B+树。
假设,我们有一个主键列为ID的表,表中又字段k,并且k上有索引。
表中 R1~R5 的 (ID,k) 值分别为 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),两棵树的示例示意图如下。
从图中不难看出,根据叶子节点的内容,索引类型分为主键索引和非主键索引。
主键索引(也称为聚簇索引),叶子节点存的是整行数据。非主键索引(也称为二级索引),叶子节点内容是主键的值。
主键索引和普通索引查询的区别;
- 如果语句是 select * from T where ID=500,即主键查询方式,则只需要搜索 ID 这棵 B+ 树;
- 如果语句是 select * from T where k=5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表。
索引维护
InnoDB的索引是以B+树的形式构建的,而且B+树的节点都是一个数据页。B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护,也就是节点分裂和节点合并。因为我们是以数据页为基本单位构造的B+树,那么B+树的节点分裂和节点合并就会造成数据页的页分裂和页合并。
页分裂
假如,现在我们插入一个新的一条记录,他的索引值是 3,那么他就要按照顺序插入到页 20 中,在索引值为 1,2 的记录的后面。而如果这个索引页已经满了,那么就需要触发一次页分裂。
页分裂是指将该页面中的一部分索引记录移动到一个新的页面中,从而为新记录腾出空间。这样可以保持 B+树的平衡和性能。
当我们插入一组无序的数据的时候,那么就可能导致多次的页分裂的情况。
而且某些情况下,可能会从叶子节点一路分裂到根节点(B+树的非叶子节点也存储数据,也可能分裂)。
页合并
既然会有页分裂那么就会有页合并。
页分裂发生在插入阶段,那么页合并就是发生在删除阶段。
当我们删除索引下的数据的时候,叶子节点的数据就会变得比较稀疏,此时 B+树就会触发合并操作。
页分裂和页合并的影响
- 页分裂和合并是涉及大量数据移动和重组的操作。频繁进行这些操作会增加数据库的/O 负担和 CPU 消耗,影响数据库的整体性能。
- 分裂和合并可能导致 B+树索引结构频繁调整,这个过程也会影响插入及删除操作的性能。
- 频繁的页分裂和合并可能会导致磁盘上存在较多的空间碎片,新分出的一个页一般会有很多空闲空间,使得数据库表占用更多的磁盘空间,而导致浪费。
避免页分裂
- 设置主键的时候尽量选择能够自增的字段
- 不要使用varchar类型作为ID主键
- 在生产全局唯一ID的时候尽量不要使用UUID,UUID不是自增的。
覆盖索引
回到这张图,对于有上述两个索引的表T来说,执行sql语句select * from T where k between 3 and 5
,执行流程如下:
- 在k索引树上找到k=3的记录吗,取得ID=300;
- 再到ID索引树查到ID=300对应的R3;
- 在k索引树取下一个值k=5,取得ID=500;
- 再回到ID索引树查到ID=500对应的R4;
- 在k索引树取下一个值k=6,不满足条件,循环结束
在这个过程中,回到主键索引树搜索的过程,称为回表。
如果执行的语句是 select ID from T where k between 3 and 5
,这时只需要查 ID 的值,而 ID 的值已经在 k 索引树上了,因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引 k 已经“覆盖了”我们的查询需求,我们称为覆盖索引。由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
最左前缀原则
B+ 树这种索引结构,可以利用索引的“最左前缀”,来定位记录。
不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。
在建立联合索引的时候,如何安排索引内的字段顺序。这里我们的评估标准是,索引的复用能力。因为可以支持最左前缀,所以当已经有了 (a,b) 这个联合索引后,一般就不需要单独在 a 上建立索引了。因此,第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
如果既有联合查询,又有基于 a、b 各自的查询呢?查询条件里面只有 b 的语句,是无法使用 (a,b) 这个联合索引的,这时候不得不维护另外一个索引,也就是说需要同时维护 (a,b)、(b) 这两个索引。
索引下推
现在我们知道,对于联合索引(a, b),在执行 select * from table where a > 1 and b = 2
语句的时候,只有 a 字段能用到索引,那在联合索引的 B+Tree 找到第一个满足条件的主键值(ID 为 2)后,还需要判断其他条件是否满足(看 b 是否等于 2),那是在联合索引里判断?还是回主键索引去判断呢?
- 在 MySQL 5.6 之前,只能从 ID2 (主键值)开始一个个回表,到「主键索引」上找出数据行,再对比 b 字段值。
- 而 MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。