一、索引
在之前,我对索引有以下的认知:
- 索引可以加快数据库的检索速度
- 表经常进行
INSERT/UPDATE/DELETE
操作就不要建立索引了,换言之:索引会降低插入、删除、修改等维护任务的速度。 - 索引需要占物理和数据空间。
- 了解过索引的最左匹配原则
- 知道索引的分类:聚集索引和非聚集索引
- Mysql支持Hash索引和B+树索引两种
看起来好像啥都知道,但面试让你说的时候可能就GG了:
- 使用索引为什么可以加快数据库的检索速度啊?
- 为什么说索引会降低插入、删除、修改等维护任务的速度。
- 索引的最左匹配原则指的是什么?
- Hash索引和B+树索引有什么区别?主流的使用哪一个比较多?InnoDB存储都支持吗?
- 聚集索引和非聚集索引有什么区别?
- ........
1.1聊聊索引的基础知识
首先Mysql的基本存储结构是页(记录都存在页里边),(比如1000条数据,可以将1-100分成一页,101-200条分成一页 ....)
- 各个数据页可以组成一个双向链表
- 而每个数据页中的记录又可以组成一个单向链表
- 每个数据页都会为存储在它里边儿的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录
- 以其他列(非主键)作为搜索条件:只能从最小记录开始依次遍历单链表中的每条记录。
所以说,如果我们写select * from user where username = 'Java3y'
这样没有进行任何优化的sql语句,默认会这样做:
- 定位到记录所在的页
- 需要遍历双向链表,找到所在的页
- 从所在的页内中查找相应的记录
- 由于不是根据主键查询,只能遍历所在页的单链表了
很明显,在数据量很大的情况下这样查找会很慢!
//** 上面没有索引的情况 **/
1.2索引提高检索速度
索引做了些什么可以让我们查询加快速度呢? //** 下面有索引的情况 **/
其实就是将无序的数据变成有序(相对):因为有序,所以在范围查询时候效率会大大提高,这也是hash索引的缺点之一所在。
要找到id为8的记录简要步骤:
很明显的是:没有用索引我们是需要遍历双向链表来定位对应的页,现在通过**“目录”**就可以很快地定位到对应的页上了!
其实底层结构就是B+树,B+树作为树的一种实现,能够让我们很快地查找出对应的记录。
/** 就是我们的B+树原理,本质上B+树指向子树的地址,我们逻辑上把这个地址称为页**/
1.3索引降低增删改的速度
B+树是平衡树的一种。
平衡树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
如果一棵普通的树在极端的情况下,是能退化成链表的(树的优点就不复存在了)
B+树是平衡树的一种,是不会退化成链表的,树的高度都是相对比较低的(基本符合矮矮胖胖(均衡)的结构)【这样一来我们检索的时间复杂度就是O(logn)】!从上一节的图我们也可以看见,建立索引实际上就是建立一颗B+树。
- B+树是一颗平衡树,如果我们对这颗树增删改的话,那肯定会破坏它的原有结构。
- 要维持平衡树,就必须做额外的工作。正因为这些额外的工作开销,导致索引会降低增删改的速度
1.4哈希索引
除了B+树之外,还有一种常见的是哈希索引。
哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。
- 本质上就是把键值换算成新的哈希值,根据这个哈希值来定位。
简单地说,哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。
从上面的图来看,B+树索引和哈希索引的明显区别是:
-
如果是等值查询,那么哈希索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值;当然了,这个前提是,键值都是唯一的。如果键值不是唯一的,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据;
-
从示意图中也能看到,如果是范围查询检索,这时候哈希索引就毫无用武之地了,因为原先是有序的键值,经过哈希算法后,有可能变成不连续的了,就没办法再利用索引完成范围查询检索;
-
同理,哈希索引也没办法利用索引完成排序,以及like ‘xxx%’ 这样的部分模糊查询(这种部分模糊查询,其实本质上也是范围查询);
-
哈希索引也不支持多列联合索引的最左匹配规则;
-
B+树索引的关键字检索效率比较平均,不像B树那样波动幅度大,在有大量重复键值情况下,哈希索引的效率也是极低的,因为存在所谓的哈希碰撞问题。
补充:
在MySQL中,只有HEAP/MEMORY引擎表才能显式支持哈希索引(NDB也支持,但这个不常用),InnoDB引擎的自适应哈希索引(adaptive hash index)不在此列,因为这不是创建索引时可指定的 /**因为optimizer的存在**/
还需要注意到:HEAP/MEMORY引擎表在mysql实例重启后,数据会丢失。
通常,B+树索引结构适用于绝大多数场景,像下面这种场景用哈希索引才更有优势:
在HEAP表中,如果存储的数据重复度很低(也就是说基数很大),对该列数据以等值查询为主,没有范围查询、没有排序的时候,特别适合采用哈希索引
例如这种SQL:
SELECT … FROM t WHERE C1 = ?; — 仅等值查询
在大多数场景下,都会有范围查询、排序、分组等查询特征,用B+树索引就可以了。
1.6聚集和非聚集索引
简单概括:
- 聚集索引就是以主键创建的索引
- 非聚集索引就是以非主键创建的索引
区别:
- 聚集索引在叶子节点存储的是表中的数据
- 非聚集索引在叶子节点存储的是主键和索引列
- 使用非聚集索引查询出数据时,拿到叶子上的主键再去查到想要查找的数据。(拿到主键再查找这个过程叫做回表)
/* 两者的主要区别就是这个回表查找,导致非聚集索引的速度要低于聚集索引 **/
非聚集索引也叫做二级索引,不用纠结那么多名词,将其等价就行了~
非聚集索引在建立的时候也未必是单列的,可以多个列来创建索引。 /**只要创建了索引就会生成索引树,不管你使不适用它*//
- 此时就涉及到了哪个列会走索引,哪个列不走索引的问题了(最左匹配原则-->后面有说)
- 创建多个单列(非聚集)索引的时候,会生成多个索引树(所以过多创建索引会占用磁盘空间)
在创建多列索引中也涉及到了一种特殊的索引-->覆盖索引
- 我们前面知道了,如果不是聚集索引,叶子节点存储的是主键+列值
- 最终还是要“回表”,也就是要通过主键再查找一次。这样就会比较慢
- 覆盖索引就是对要查的列建立索引,因而就不需做回表操作! /**因为建立了索引,相应列的数据就在叶子结点上,查询就 不需要回表,而是在索引表里面找就可以*//
比如说:
- 现在我创建了索引
(username,age)
,在查询数据的时候:select username , age from user where username = 'Java3y' and age = 20
。 - 很明显地知道,我们上边的查询是走索引的,并且,要查询出的列在叶子节点都存在!所以,就不用回表了~但是如果要查询s
elect *from user where username = 'Java3y' and age = 20依然要回表查询。
- 所以,能使用覆盖索引就尽量使用吧~
1.7最佳左前缀法则(出现在多列索引中)
最佳左前缀法则:
- 索引可以简单如一个列
(a)
,也可以复杂如多个列(a, b, c, d)
,即联合索引。 - 如果是联合索引,那么key也由多个列组成,同时,索引只能用于查找key是否存在(相等),遇到范围查询
(>、<、between、like
左匹配)等就不能进一步匹配了,后续退化为线性查找。但如果是跨列相等,则只是当前列索引失效。 - 因此,列的排列顺序决定了可命中索引的列数。
例子:
如有索引(a, b, c, d)
,查询条件a = 1 and b = 2 and c > 3 and d = 4
,则会在每个节点依次命中a、b、c,无法命中d。(很简单:索引命中只能是相等的情况,不能是范围匹配),d变为线性查找,如果这时d列数据达到百万条,这时速度将大大打折扣。
1.8=、in自动优化顺序
不需要考虑=、in等的顺序,mysql会自动优化这些条件的顺序,以匹配尽可能多的索引列。
例子:
- 如有索引
(a, b, c, d)
,查询条件c > 3 and b = 2 and a = 1 and d < 4
与a = 1 and c > 3 and b = 2 and d < 4
等顺序都是可以的,MySQL会自动优化为a = 1 and b = 2 and c > 3 and d < 4
,依次命中a、b、c。
1.9索引总结
索引在数据库中是一个非常重要的知识点!上面谈的其实就是索引最基本的东西,要创建出好的索引要顾及到很多的方面:
- 1,最左前缀匹配原则。这是非常重要、非常重要、非常重要(重要的事情说三遍)的原则,MySQL会一直向右匹配直到遇到范围查询
(>,<,BETWEEN,LIKE)
就停止匹配。 - 3,尽量选择区分度高的列作为索引,区分度的公式是
COUNT(DISTINCT col) / COUNT(*)
。表示字段不重复的比率,比率越大我们扫描的记录数就越少。 - 4,索引列不能参与计算,尽量保持列“干净”。比如,
FROM_UNIXTIME(create_time) = '2016-06-06'
就不能使用索引,原因很简单,B+树中存储的都是数据表中的字段值,但是进行检索时,需要把所有元素都应用函数才能比较,显然这样的代价太大。所以语句要写成 :create_time = UNIX_TIMESTAMP('2016-06-06')
。 - 5,尽可能的扩展索引,不要新建立索引。比如表中已经有了a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
- 6,单个多列组合索引和多个单列索引的检索查询效果不同,因为在执行SQL时,MySQL只能使用一个索引,会从多个单列索引中选择一个限制最为严格的索引。