MySQL索引B+树数据结构原理以及分类了解只需这一篇(绝非标题党)

索引!可谓是重中之重。面试可谓是必问内容,当然了,平时开发对于SQL的优化也是很重要的

而当实践SQL优化不能盲目的操作,理论是支撑实践的基础

索引的意义

索引的本质就是一种排好序的数据结构。这个肯定都明白,自然而言就联想到字典中的目录

在详细讲解下面的索引数据前补充一句:

通过不断缩小想要获取数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序事件,也就是说,有了这种索引机制,总是用同一种方式来锁定数据

索引分类

1.普通索引 index:加速查找

2.唯一索引

​ 主键索引:primary key:加速查找+约束(不为空且唯一)
​ 唯一索引:unique:加速查找+约束(唯一)

3.联合索引

​ primary key(id,name):联合主键索引
​ unique(id,name):联合唯一索引
​ index(id,name):联合普通索引

4.全文索引 fulltext:用于搜索很长的一篇文章的时候,效果最好

5.空间索引 spatial:了解就好

索引数据结构分类

在数据库中,索引数据结构是很多分类的,而你印象最深刻的是不是就B+树索引呢?那是因为平时我们使用的基本都是MySQL,而根据存储引擎的不同,有些索引是不支持,只有heap、Memory引擎支持哈希索引,InnoDB引擎自适应哈希索引,下面我就来逐条讲解

Step1:Hash索引

Hash索引是比较常见的一种,单条查询效率很高,时间复杂度为1
但是!Hash索引不是最常用的数据库索引类型,尤其是我们常用Innodb引擎就不支持Hash索引

原因:Hash索引适合精确查找,但是范围查找不适合

因为存储引擎会为每一行数据计算一个hash码,hash码都比较小,并且不同键值行的hash码通常是不一样的,hash索引存储的就是hash码,hash码之间没有规律,且hash操作并不能保证顺序性,所以查找出值相近的两个数据,Hash值相差很远,被分到不同的桶中。这就是为什么hash索引只能进行全职匹配的查询,因为只有这样,hash码才能够匹配到数据

那什么情况使用哈希索引呢?

select … from user where user_id = ? 这种等查询非常适合hash索引

因为只需要经过一次算法即可找到相应的键值;前提是键值都是唯一的。如果键值不唯一就需要先找到该键值所在位置,然后再根据链表往后扫描,直到找到相应的数据

Step2:二叉树

常见的索引数据结构是树结构,先来详解最经典也是最开始的二叉树

二叉树特点:

  • 二叉树的时间复杂度为O(n)
  • 一个节点只能有两个子节点。不能超过2
  • 左子节点小于本节点,右子节点大于本节点
    在这里插入图片描述

但是在极端情况下可能出现链化的情况,既节点一直在某一边增加,如下图

在这里插入图片描述

二叉树中,有一种特殊的结构>>>平衡二叉树 平衡二叉树特点

  • 根节点会随着数据的改变而变更
  • 数据量越多,遍历次数越多,IO次数就越多,就越慢(磁盘的IO由树高决定)

Step3:B树(二三树)

了解二叉树之后,我们可以进一步详解什么是B树了,大概的样子如下图
在这里插入图片描述

从B树的结构图中可以看到每个节点中不仅包含数据的key值,还有data值

而每页的存储空间是有限的,如果data比较大,会导致每个节点的key存储比较少,当数据量比较大的时候,同样会导致B树很深,从而增加了磁盘IO的次数,进而影响查询效率

Step4:B+树

好了,说了上面这么多索引结构,都是为了正题而做铺垫的

B+树以下特点:
  1. 在 B+ 树中,所有数据记录节点都是按照键值的大小存放在同一层的叶子节点上,而非叶子结点只存储key的信息,这样可以大大减少每个节点的存储的key的数量,降低B+ 树的高度
  2. B+ 树叶子节点的关键字从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针。
  3. B+ 树的层级更少:相较于 B 树 B+ 每个非叶子节点存储的关键字数更多,树的层级更少所以查询数据更快
  4. B+ 树查询速度更稳定:B+ 所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定;
  5. B+ 树天然具备排序功能:B+ 树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。
  6. B+ 树全节点遍历更快:B+ 树遍历整棵树只需要遍历所有的叶子节点即可,,而不需要像 B 树一样需要对每一层进行遍历,这有利于数据库做全表扫描。

上图!看不懂没关系 下面一一讲解

在这里插入图片描述

上面数据页就是存放数据的地方,且数据页之间是通过双向链表进行连接的,下面逐步分析

主键目录

将上图的数据页拿出来细分,就形成了下面的主键目录
在这里插入图片描述

我们知道 MySQL 在存储数据的时候是以数据页为最小单位的,且数据在数据页中的存储是连续的,数据页中的数据是按照主键排序的(没有主键是由 MySQL自己维护的 ROW_ID 来排序的),数据页和数据页之间是通过双向链表来关联的,数据与数据之间是通过单向链表来关联的。

也就是说有一个在每个数据页中,他必然就有一个最小的主键,然后每个数据页的页号和最小的主键会组成一个主键目录(就像上图中的左边部分,最终叫做索引页),假设现在要查找主键为 2 的数据,通过二分查找法最后确定下主键为 2 的记录在数据页 1 中,此时就会定位到数据页 1 接着再去定位主键为 2 的记录,我们先知道大致的流程,细节先不要深究,先从宏观看结构原理,再到微观看实现原理。

刚刚上面是说的其实可以理解为是主键索引,主键索引也是最简单的最基础的索引。这个时候大家应该知道为什么你建立了主键查询就能变快了吧?

索引页

那假设有很多数据页呢?那是不是对应的主键目录会不会也很大呢?

那假设有1000万记录、5000万记录呢?就算用二分法查询,其效率也依旧很低的,所以为了解决这种情况Mysql又设计出了新的存储结构-索引页。看下图(主键目录不再存储数据页,而是变为存储索引页)将里面的记录拆分成不同的索引页中
在这里插入图片描述

索引页中记录的是每页数据页的页号和该数据页中最小的主键的记录,也就是说最小主键和数据页号不是单纯的维护在主键目录中了,而是演变成了索引页,索引页和数据页类似,一张不够存就分裂到下一张。

假如现在要查找 id=20 的这条记录,咦?那我应该到哪个索引页中查找该条记录呢?所以这个时候肯定是需要去维护索引页的。

没错,MySQL 也是这么设计的,也就是说 MySQL 同时也设计出了用于维护索引页的数据结构,其实也还叫索引页,只不过他们是在不同的层级,类似下面这样子的:
在这里插入图片描述

也就是说维护索引页的索引页是在真正存储记录和数据页的索引页的上一层,现在如果你想查找 id=20 的这条记录,那就是从最上层的索引页开始查找,通过二分法查找,很快就能够定位到 id=20 s这条记录是在索引页 2 上,然后到就索引页 2 上面查找,接着就是和之前一样了(注意,索引页中的记录也是通过单向链表连接的),根据各个最小的主键能够定位到 id=20 是在数据页5上,假设数据页5是这样子的 。所以B+树中的索引定位就是这个样子的
在这里插入图片描述

索引页的分层

好,既然你已经知道到索引页太多会往上一层扩散,那现在假设上一层的索引页记录也太多了,那该怎么办?很简单,继续分裂,再往上一层继续,不废话,我来画图帮助大家理解
在这里插入图片描述

我们来模拟一个查找的过程,假设你要查找 37 这条记录,说实话我根本不知道这条记录在哪里。好,现在我们就来模拟 MySQL 的查找过程,首先从最顶层的索引页开始查找,因为 id=37,因此定位到了索引页16,然后到索引页 16 中继续查找,此时同样能够定位到 id=37 在索引页 3 中,然后继续查找,最终能够定位到数据实在数据页 8 中,假设数据页 8 是这样子的
在这里插入图片描述

上个完整的图

在这里插入图片描述

这个时候机智的你是不是已经发现了什么小秘密?他是不是很像一颗二叉树?实际上这就是一颗 B+ 树的结构,这也是数据在磁盘中真正存储的物理结构。B+树的特性是什么呢?B+树,也是二叉搜索树的一种,但是他的数据仅仅存储在叶子节点(在这里就是数据页),像这种索引页+数据页组成的组成的B+树就是聚簇索引(这句话很重要)。

聚簇索引是Mysql基于主键索引结构创建的
非主键索引

但是现在问题又来了,既然这里强调的是主键索引,那我们平时开发中除了主键索引其他的索引也用的不少,这时候该怎么办?假设你现在 对name、age建立索引。现在回顾下主键索引,是不是在插入数据的时候基于主键的顺序去维护一个 B+ 树的?

而实际上非主键索引其原理是一样的,MySQL 都是去维护一颗 B+ 树,说白了,你建立多少个索引,MySQL 就会帮你维护多少的B+树(这下是不是也突然想明白了为什么索引不能建立太多了?以前就知道不能建立太多索引,因为索引也会占用空间,实际上这就是根本原因)

假如现在真的对 name+age 建立索引,那此时是存放的呢?此时 MySQL 根据 name+age 维护一个单独的 B+ 树结构,数据依旧是存放在数据页中的,只不过是原来数据中的每条记录写的是 id=xx,现在写的是name=xx,age=xx,id=xx,不管怎么样,主键肯定会存放的,先来张图压压惊
在这里插入图片描述

在插入数据的时候,MySQL 首先会根据 name 进行排序,如果 name 一样,就根据联合索引中的 age 去排序,如果还一样,那么就会根据 主键 字段去排序。插入的原理就是这样子的。

此时每个数据页中的记录存放的实际是索引字段和主键字段,而其他字段是不存的(为什么不存放?一样的数据到处存放很浪费空间的,也没必要,所以才会有下面的索引优化),至于查找,原理和过程跟聚簇索引一样,这里就不再赘述,但是,下面说的内容却是至关重要的:假设现在执行这样的SQL:

select name from student where name = '张三'

那么此时查询是完美的,使用到了索引且不需要回表

回表

是这样子的,现在要根据 name 查找到该条记录,且查询的字段(即 select 后面的查询字段)也仅仅有 name(只要是在 name,age,id 这三个字段中都可以)这个时候是能够直接获取到最终的记录的

换句话说,因为联合索引中的记录也仅仅有 name,age,id,所以在查询的如果也仅仅查询这三个字段,那么在该B+树中就能够查询到想要的结果了。

那现在假设查询的 SQL 是这样子的(我们假设 student 中还有除了name,age,id 其他的字段 )

select * from student where name = '张三'

那这下子就完蛋了,因为你现在虽然根据 name 很快的定位到了该条记录,但是因为 name+age 不是聚簇索引,此时的 B+ 树的数据页中存放的仅仅是自己关联的索引和主键索引字段,并不会存其他的字段,所以这个时候其他的属性值是获取不到的,这时候该怎么办?

这种情况下,MySQL 就需要进行回表查询了。此时 MySQL 就会根据定位到的某条记录中的 id 再次进行聚簇索引查找,也就是说会根据 id 去维护 id 的那么 B+ 树中查找。因为聚簇索引中数据页记录的是一条记录的完整的记录,这个过程就叫回表

再强调下回表的含义:根据非主键索引查询到的结果并没有查找的字段值,此时就需要再次根据主键从聚簇索引的根节点开始查找,这样再次查找到的记录才是完成的。

最后,让我一起看下 MySQL 对于非主键索引的维护过程:

对于非主键索引(一般都是联合索引),在维护 B+ 树的时候,会根据联合索引的字段依次去判断,假设联合索引为:name + address + age,那么 MySQL 在维护该索引的 B+ 树的时候,首先会根据 name 进行排序,name 相同的话会根据第二个 address 排序,如果 address 也一样,那么就会根据 age 去排序,如果 age 也一样,那么就会根据主键字段值去排序,且对于非主键索引,MySQL 在维护 B+ 树的时候,仅仅是维护索引字段和主键字段。

覆盖索引

意思就是该sql命中了索引,但未覆盖索引。像我上面说的,它还是会回表操作

select * from student where id = 123

利用id=123到索引的数据结构中定位到id在硬盘的位置,或者说数据页中的位置
但select的字段为*,除了id以外还需要其它字段,这就意味着,通过索引结构取到id还不够
还需要利用该id再去找到该id所在行其它字段值,回表操作是需要时间的,如果是下面这样

select id from student where id = 123

这条就是覆盖索引,命中索引,且从索引的数据结构直接取到了id在硬盘的地址,速度很快

索引合并

组合索引能做的事情,都可以用索引合并去解决 但索引合并能解决的组合索引未必 比如

先创建一个组合索引

create index ne on s1(name,age);组合索引

其实我们可以单独为name和age创建索引

组合索引可以命中

select * from s1 where name = ‘123’

select * from s1 where name = ‘123’ and age = 21

组合索引不可命中

select * from s1 where age = 21; 因为索引比配必须按照从左到右匹配规则

索引合并可以命中

select * from s1 where name = ‘123’

select * from s1 where age = 21

select * from s1 where name = ‘123’ and age = 21

乍一看感觉索引合并好像更好,可以命中更多的情况,但还是分情况的 如果是 name = ‘123’ and age =21。那么组合索引的效率要高于索引合并,而且每创建一个索引都需要去单独的B+树去维护,都是占用空间的

索引匹配规范

1.最左前缀匹配原则,非常重要的原则。上面的组合索引提到了,如果不遵循是不走索引的

create index ne on s1(name,age)

2.mysql会向右一直匹配到 直到遇到范围查询(>、<、between、like)就会停止匹配

比如a = 1 and b = 2 and c > 3 and d = 4,如果建立索引(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a.b.d的顺序可以任意调整

3.=和in可以乱序

比如a = 1and b = 2 and c = 3 建立(a,b,c)索引可以任意调整,Mysql的查询优化器会帮你优化索引可以识别的形式

4.尽量选择分区度高的字段作为索引

区分度的公式是count (distinct col) / count(*)。也就是取值离散大的字段

表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录

5.索引列不能参与计算,保持列“干净”

比如from_unixtime(create_time) = ‘2021-03-05’

就不能使用索引,原因很简单,B+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用到函数才能比较,显然成本太大了。所以语句可以改成create_time = unixtime(‘2021-03-05’)

索引无法命中的情况

1:like '%xx’

select * from student where name like '%cn'; 

2.使用函数

上面已经说到过了 会将字段值放到函数里比较

3.or

当or条件中有未建立的索引列才会失效!!!

4.类型不一致

如果列是字符串类型,传入条件必须用引号引起来!

select * from student where name = 999

5.普通索引的不等于不会走索引

select * from student where name != ‘张三’

如果是主键索引,则还是会走索引

select * from student where id !=123

如果主键或索引是整数倍,则还是会走索引

select * from student where id > 123

select * from student where age > 123

6.order by 排序条件为索引,则select字段必须也是索引字段,否则无法命中

当索引排序的时候,select查询的字段不是索引,则不走索引

**如果对主键排序,则还是走索引

select * from student order by id desc;

7.组合索引最左前缀

如果组合索引:(name,age)

name and age :走索引

name :走索引

age :不走索引

8.count(1)或count(列)代替count(*)在mysql中没有差别

9.避免使用select * 因为可能会回表操作放弃使用索引

10.表的字段顺序固定长度的字段优先

11.尽量使用短索引

12.使用连接(join)代替子查询

13.连表时注意条件类型需要一致

好了,以上就是对索引结构和索引分类,以及使用中可能遇到问题的整合,后期还会持续更新内容,如果你感觉还不错,何不收藏点个赞呢 对吧!

猜你喜欢

转载自blog.csdn.net/q736317048/article/details/114415571
今日推荐