前言
索引是Mysql中常用到的一个功能,可以大大加快查询速度,同时面试中也是经常碰到。本文是学习Mysql索引的归纳总结。
索引采用的数据结构——B+ 树
本部分主要是参考自小林Coding
B+树的由来
二分查找可以每次缩减一半,从而提高查找效率。
但是二分查找,若使用线性结构,每次插入,都是需要移动其余剩下的全部元素,消耗巨大。
因此有了二分查找树。
但是二叉树若每次插入的都比其父节点大,则会演变为链表,从而使查询复杂度从 O(logn)降低为 O(n)。
因此有了自平衡二叉树,诸如AVL树或红黑树,其都是这样的自平衡二叉树。
但由于其本质还是一棵二叉树,所以会随着数据量增大,导致层数增加, IO操作增多(每一层IO多一次)。
因此有了B树。其每个节点允许有M个子节点,M是B树的阶,假设M为3,那就是阶为3的B树,其每个节点最多有2(M-1)个数据和3(M)个子节点。若超过,则分裂节点。
但是 B 树的每个节点都包含数据(索引+记录),每次IO都会需要查询到节点上记录的内容。若数据量大于索引的大小,那么在读取底层节点索引的时候,就会导致较多的IO操作。从而使性能受到巨大影响。
因此有了B+ 树,B+树和B树的结构其实相似,只是仅将数据存储在底层叶子节点。其余的子节点仅存储索引。从而解决了数据大、影响IO的问题。
其次,其底层叶子节点之间,既存了索引也存了记录。叶子节点之间通过链表连接起来。从而对于范围查询,可以大大提升效率。
B+树的结构
关于B++树的结构看图就好了。
B+树的叶子节点是单链表还是双向链表
网上很多探讨针对于“其叶子节点是单向链表还是双向链表”,包括小林也是做出了一次纠正。
在此文发现了一些结论与有趣的探讨。
顺着此,找到了一个结论——B+ 树中各个页之间是通过双向链表连接的,叶子节点中的数据是通过单向链表连接的。
为什么选用了B+ 树
这里直接复制小林的结论了。
B+ 树 vs B 树
B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,所以 B+Tree 的单个节点的数据量更小,在相同的磁盘 I/O 次数下,就能查询更多的节点。
另外,B+Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的基于范围的顺序查找,而 B 树无法做到这一点。
B+ 树 vs 二叉树
对于有 N 个叶子节点的 B+Tree,其搜索复杂度为O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。
在实际的应用当中, d 值是大于100的,这样就保证了,即使数据达到千万级别时,B+Tree 的高度依然维持在 3~4 层左右,也就是说一次数据查询操作只需要做 3~4 次的磁盘 I/O 操作就能查询到目标数据。
而二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 O(logN),这已经比 B+Tree 高出不少,因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多。
B+ 树 vs Hash
Hash 在做等值查询的时候效率贼快,搜索复杂度为 O(1)。
但是 Hash 表不适合做范围查询,它更适合做等值的查询,这也是 B+Tree 索引要比 Hash 表索引有着更广泛的适用场景的原因。
索引的分类
概述
按「数据结构」分类:B+tree索引、Hash索引、Full-text索引。
按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。
按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。
按「字段个数」分类:单列索引、联合索引。
B+ Tree vs Hash vs Full-text
B+ Tree就是现在常用到的那样,详细见上文。
Hash在上文的最后一点也提到过,效率更快,但是不适合范围查询。其中InnoDB引擎不支持,只有Memory引擎可以使用。InnoDB引擎中,有“自适应哈希索引”。具体见此文。
Full-text索引是更罕见的,甚至于小林Coding都没有写…具体见此文。说一个适用场景:搜索引擎的全文搜索。
聚簇索引 vs 二级索引(辅助索引)
聚簇索引就是如上文B++ Tree中写的那样,放一起。
二级索引与之相对,叶子节点中存放的也仅仅是数据的位置。真正需要时候,再通过回表操作去查找。在聚簇索引之上所建立的索引,都叫做二级索引(辅助索引)
将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因
这段对二级索引调用的描述感觉更加清晰。来源此文,里面也有相关更详细的讲解。
主键索引 vs 唯一索引 vs 普通索引 vs 前缀索引
· 主键索引:是在唯一索引的基础上又增加了不为空的约束(换言之,添加了唯一索引的字段,是可以包含NULL值的),即NOT NULL+UNIQUE,一张表里最多只有一个主键索引,当然一个主键索引中可以包含多个字段。
· 唯一索引:是在普通索引的基础上增加了数据唯一性的约束,一张表中可以同时存在多个唯一索引。
· 普通索引:是最基础的索引,这种索引没有任何的约束作用,它存在的主要意义就是提高查询效率。
· 前缀索引:前缀索引也叫局部索引,比如给身份证的前 10 位添加索引,类似这种给某列部分信息添加索引的方式叫做前缀索引。
单列索引 vs 联合索引
概念
单列索引:只在一个列上的索引被称作单列索引。一个表可以有多个单列索引,但这不是组合索引。
组合索引:两个或更多个列上的索引被称作组合索引,组合索引又叫联合索引。
最左前缀原则——按照最左优先的方式进行索引的匹配。
如果创建了一个 (a, b, c) 联合索引,如果查询条件是以下这几种,就可以匹配上联合索引:
where a=1;
where a=1 and b=2 and c=3;
where a=1 and b=2;
需要注意的是,因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要。
但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:
where b=2;
where c=3;
where b=2 and c=3;
因为(a, b, c) 联合索引,是先按 a 排序,在 a 相同的情况再按 b 排序,在 b 相同的情况再按 c 排序。所以,b 和 c 是全局无序,局部相对有序的,这样在没有遵循最左匹配原则的情况下,是无法利用到索引的。
索引优缺点(什么时候适合创建、不适合创建)
优缺点
优点——加快查询速度。
缺点:
1、需要占用物理空间,数量越大,占用空间越大。
2、创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增大。
3、会降低表的增删改的效率,因为每次增删改索引,B+ 树为了维护索引有序性,都需要进行动态维护。
是否创建
适合创建的场景
字段有唯一性限制的,比如商品编码;
经常用于 WHERE 查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。
经常用于 GROUP BY 和 ORDER BY 的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的。
⼤表中的关键列: 在⼤表中,如果查询的效率变得很低,可以考虑在关键列上创建索引。
表与表连接用于多表联合查询的约束条件的字段应当建立索引。
不适合创建的场景
⼩表: 对⼩表创建索引可能会带来额外的开销,因为在⼩数据集中扫描整个表可能⽐使⽤索引更快。
频繁的插⼊、更新和删除操作: 索引的维护成本会随着数据的插⼊、更新和删除操作⽽增加。如果表经常被修改,过多的索引可能会影响性能。
数据重复且分布平均的表字段:假如⼀个表有10万⾏记录,性别只有男和⼥两种值,且每个值的分布概率⼤约为50%,那么对这种字段建索引⼀般不会提⾼数据库的查询速度。MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。
很少被查询的列: 如果某列很少被⽤于查询条件,那么为它创建索引可能没有明显的性能提升。
查询结果总⾏数较少的表: 如果查询的结果集总⾏数很少,使⽤索引可能不会有太⼤的性能提升
索引失效的情景
1、like %的模糊匹配。like %xx
或者 like %xx%
会导致失效,like 'xx%'
不会。原因是类似于姓名,根据首字母排序,但名字第二三个字的顺序没有关联。
2、对索引使用函数/表达式运算。因为索引中存储的是原始数据,而不是处理后的字段,所以不能走索引查询。
3、对索引隐式转换。因为隐式转换等同于用了函数。
4、联合索引的非最左匹配。详细见上方讲解处,就是类似于姓名排序是按照的首字母,后面的字母无关联。
5、where语句中or的多个条件,其中一个不是索引列。
索引优化
见此文
1)如果MySQL估计使用索引比全表扫描还慢,则不会使用索引。
2)前导模糊查询不能命中索引。
3)数据类型出现隐式转换的时候不会命中索引,特别是当列类型是字符串,一定要将字符常量值用引号引起来。
4)复合索引的情况下,查询条件不包含索引列最左边部分(不满足最左原则),不会命中符合索引。
5)union、in、or都能够命中索引,建议使用in。
6)用or分割开的条件,如果or前的条件中列有索引,而后面的列中没有索引,那么涉及到的索引都不会被用到。
7)负向条件查询不能使用索引,可以优化为in查询。
负向条件有:!=、<>、not in、not exists、not like等。
8)范围条件查询可以命中索引。范围条件有:<、<=、>、>=、between等。
9)利用覆盖索引进行查询,避免回表。
被查询的列,数据能从索引中取得,而不用通过行定位符row-locator再到row上获取,即“被查询列要被所建的索引覆盖”,这能够加速查询速度。
10)建立索引的列,不允许为null。
单列索引不存null值,复合索引不存全为null的值,如果列允许为null,可能会得到“不符合预期”的结果集,所以,请使用not null约束以及默认值。
部分面试题补充
此部分参考自此文。不得不说还是java社区资源多…
非聚簇索引一定会回表查询吗?
不一定。如果涉及到查询语句所要求的字段,全部命中了索引,那么就不必再进行回表查询。
举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行select score from student where score > 90的查询时,在索引的叶子节点上,已经包含了score 信息,不会再次进行回表查询。
关于索引下推
此部分详细见此文
简单来说,(https://zhuanlan.zhihu.com/p/121084592)如果没有使用,那么流程是:检索到数据,返回Mysql服务器,判断是否符合。
如果使用了,则是:检索到数据,存储引擎判断是否符合,符合了再返回。
索引条件下推优化可以减少存储引擎查询基础表的次数,也可以减少MySQL服务器从存储引擎接收数据的次数。
为什么推荐使用自增主键作为索引?
结合B+Tree的特点,自增主键是连续的,在插入过程中尽量减少页分裂,即使要进行页分裂,也只会分裂很少一部分。并且能减少数据的移动,每次插入都是插入到最后。总之就是减少分裂和移动的频率。
参考资源
https://blog.csdn.net/qq_43592352/article/details/127351846
https://xiaolincoding.com/mysql/index/index_interview.html#%E6%8C%89%E5%AD%97%E6%AE%B5%E7%89%B9%E6%80%A7%E5%88%86%E7%B1%BB
https://mp.weixin.qq.com/s/lEx6iRRP3MbwJ82Xwp675w
https://tech.meituan.com/2014/06/30/mysql-index.html