Mysql索引详解

发布时间 2023-11-12 16:23:36作者: 风筝上的猫

 一、索引

1.1索引由来

如果数据量过大,没有索引就需要扫描全表挨个匹配 速度会非常慢,这时就该用到索引了。

通过索引表找到该行数据对应的物理地址然后访问相应的数据。

索引的出现其实就是为了提高数据查询的效率,就像书的目录一样。 事实上,索引是一种数据结构,用于帮助我们在大量数据快速定位到我们想要查找的数据。

但对于海量数据来说,它的目录也是很大的,不可能全部存储在内存中,因此索引往往是存储在磁盘上的文件中(可能存储在单独的索引文件中,也可能和数据一起存储在数据文件中)。

1.2索引查询示例

mysql 索引采用的是 B+ 树结构来构建,B+ 树是一颗变种的 B树,变种的主要目的是减少树的深度和大小,以减少 IO 的次数,提升效率。那 B树 可以理解为是一颗平衡多叉树,所以再这种数据结构下,查找的时间复杂度可以理解为 二分查找的时间复杂度O(logn)。

如果我们要加载id=55的数据,那么

1.把磁盘块1的数据由磁盘加载到内存,发生一次IO,在内存中用二分查找确定55在50-100,锁定磁盘块1的P2指针,确定了磁盘块3的位置。

2.把磁盘块3的数据由磁盘加载到内存,发生第二次IO,确定55在50-60之间,锁定磁盘块3的P指针,确定了磁盘块7的位置。

3.加载磁盘块7的数据到内存,发生第三次IO,在内存中做二分查找确定数据55,结束查询,总计三次IO。

真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。

1.3InnoDB引擎模型

在创建表的时候可以指定当前表的存储引擎,如果没有指定,默认的存储引擎为InnoDB。

到底该把数据存储在什么位置,是内存还是磁盘?怎么从表里读取数据,以及怎么把数据写入具体的表中,这都是 存储引擎 负责的事情。

MySQL 5.7及更新版中的默认InnoDB存储引擎。InnoDB是一个事务安全(与ACID兼容)的MySQL 存储引擎,它具有提交、回滚和崩溃恢复功能来保护用户数据。InnoDB行级锁(不升级为更粗粒度的锁)和Oracle风格的一致非锁读提高了多用户并发性。InnoDB将用户数据存储在聚集索引中,以减少基于主键的常见查询的I/O。为了保持数据完整性,InnoDB还支持外键引用完整性约束。

1.4局部性原理

空间局部性是说,如果当前数据是正在被使用的数据,那么与该数据空间地址临近的其他数据 在未来由更大的可能性被用到,因此可以优先加载到寄存器或主存中,从而提高效率。通俗的说就是,当磁盘上的一块数据被读取的时候,我们干脆多读一点,而不是用多少读多少。

InnoDB存储引擎将数据划分为若干个,以作为磁盘和内存之间交互的最小单位。InnoDB中页的大小默认为16KB。也就是默认情况下,一次最少从磁盘中读取16KB的数据到内存中,一次最少把内存中16KB的内容刷新到磁盘上。 对于InnoDB的存储引擎而言所有类型的数据都是以“页”的形式进行存储的。

非常非常重要的一点是,在一个数据页中,用户记录是按照主键由小到大的顺序串联而成的单向链表。

二、索引的数据结构

参考:

从根儿上理解索引 | 蝉沐风

 

2.1主键索引/聚簇索引

InnoDB 采用 B+ 树来实现索引,还有一点, 在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,也就是InnoDB会自动生成主键索引。并且其原始数据本身就是一颗 B+ 树,这个索引叫做主键索引,又叫聚簇索引

所以每一张 innoDB 的表数据,必须有且只有一个聚簇索引,聚簇索引的 B+ 树,最底层节点也就是叶子节点存储了我们完整的用户记录(就是我们插入表的所有数据),而且,这是用户记录在InnoDB引擎中的唯一存储方式。也就是所谓的“索引即数据,数据即索引”。

主键索引有两个特点:

  1. 按照主键的大小对用户记录和数据页进行排序,记录用单向链表连接,数据页使用双向链表连接;
  2. B+树的叶子节点保存了用户的完整记录。

2.2普通索引

InnoDB会为每个表自动创建主键索引,也就是一颗B+树,树的叶子节点包含了表中的所有数据,也就是InnoDB的一切操作都会在主键索引的B+树上展开。

对innerDB而言主键索引是一定存在的,但是我要以 name='张三' 为搜索条件怎么办?

按照之前的思想:再创建一颗B+树(叫做name索引)其中用户记录和数据页按照name字段进行排序,B+树的叶子节点保留完整的用户数据,这样就可以实现对 name 列的快速搜索了。

但是如此一来,表中数据就被完整记录了2次(主键索引的叶子节点和name索引的叶子节点),要是我们为其他字段再建立索引,磁盘空间可想而知。因此,我们得想个其他的办法。

我们已经知道根据主键查询用户记录是非常快的了,那我们可以想个办法根据name字段来迅速找到主键,然后再根据主键查找用户记录啊。这个办法同样离不开B+树。但和主键索引B+树稍有不同。

这棵B+树和聚簇索引的B+树有点区别:

  1. 叶子节点存放的不再是完整的用户记录,而是只记录name列和主键值;
  2. 数据页中存放的用户记录和目录项记录由原本的按照主键排序变为按照name列排序;
  3. 目录项记录除了存储索引列(name)和页号之外,还同时存储了主键值(防重复值)

有了这棵B+树,你就可以通过name列快速找到主键值了,查找的方式和根据主键值查找用户记录的方式完全一样,只不过前者查到的是主键值,后者查找到的是一条完整的用户记录罢了。

现在得到主键的id了,然后根据主键id到主键索引中查找到完整的用户记录,这个过程叫做回表。如果没有为name列设置唯一性约束,那就可能找到多个符合条件的主键id,多回几次表就可以了。

对name这种单个列添加的索引叫做普通索引,也叫二级索引或者辅助索引

(二级索引属于非聚簇索引)

2.3联合索引

假设我们为name列和phone列建立联合索引(注意描述顺序),自然也是创建一棵B+树,这棵B+树和之前又稍微有点不同:

  1. 叶子节点存放的是name列、phone列和主键值;
  2. 目录项记录除了存储索引列(name、phone)和页号之外,同时还存储了主键值
  3. 数据页中存放的用户记录和目录项记录由原本的按照主键排序变为按照name列排序,如果name列相同,就按照phone列排序,phone列相同就按主键排序。

还是和二级索引一样,利用B+树快速定位到数据页,然后页内快速定位到记录,找到记录中的主键id,再回表,如果找到多条符合条件的记录,就多回几次表

二级索引只有一个字段。如果将多个字段组合成一个索引,那么这种二级索引就被称为联合索引。

2.4最左匹配原则

参考:

全网都在说一个错误的结论

 

  联合索引的最左匹配原则,在遇到范围查询(如>、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段后面的字段无法用到联合索引。注意,对于>=、、BETWEEN、like前缀匹配的范围查询,并不会停止匹配。
 

三、索引的代价

索引可以非常有效地提升查询效率,既然这么好,我给每个字段都创建一个索引行不行? ----答案是不行,过度使用索引,我们在空间和时间上都会付出相应的代价。

3.1空间代价

索引就是一棵B+数,每创建一个索引都需要创建一棵B+树,每一棵B+树的节点都是一个数据页,每一个数据页默认会占用16KB的磁盘空间,每一棵B+树又会包含许许多多的数据页。所以,大量创建索引,你的磁盘空间会被迅速消耗。

3.2时间代价

空间上的代价你可以使用“钞能力”来解决,但时间上的代价我们可能就束手无策了。

链表的维护

我以主键索引为例举个例子,主键索引的B+树的每一个节点内的记录都是按照主键值由小到大的顺序,采用单向链表的方式进行连接的。如下图所示:

如果我现在要删除主键id为1的记录,会破坏3个数据页内的记录排序,需要对这3个数据页内的记录进行重排列,插入和修改操作也是同理。

注:这里给大家提一嘴,其实删除操作并不会立即进行数据页内记录的重排列,而是会给被删除的记录打上一个删除的标识,等到合适的时候,再把记录从链表中移除,但是总归需要涉及到排序的维护,势必要消耗性能。

假如这张表有12个字段,我们为这张表的12个字段都设置了索引,我们删除1条记录,需要涉及到12棵B+树的N个数据页内记录的排序维护。

更糟糕的是,你增删改记录的时候,还可能会触发数据页的回收和分裂。还是以上图为例,假如我删除了id为13的记录,那么数据页124就没有存在的必要了,会被InnoDB存储引擎回收;我插入一条id为12的记录,如果数据页32的空间不足以存储该记录,InnoDB又需要进行页面分裂。我们不需要知道页面回收和页面分裂的细节,但是能够想象到这个操作会有多复杂。

如果每个字段都创建索引,所有这些索引的维护操作带来的性能损耗,

查询计划

执行查询语句之前,MySQL查询优化器会基于cost成本对一条查询语句进行优化,并生成一个执行计划。如果创建的索引太多,优化器会计算每个索引的搜索成本,导致在分析过程中耗时太多,最终影响查询语句的执行效率。

3.3回表的代价

1、额外磁盘IO:如果回表的目标数据页恰好在内存中的话效果倒也不会太差,但如果不在,则需要额外的磁盘IO操作,增加响应时间,特别是大型表上;

2、增加CPU开销:回表过程需要消耗额外的 CPU 资源,因为数据库引擎需要执行额外的操作来定位并返回所需的数据行。

3、增加网络传输开销:如果数据库引擎和数据存储不在同一台服务器上,回表可能导致额外的网络传输开销,因为需要将数据从存储服务器传输到查询引擎所在的服务器。

4、增加锁的竞争:回表操作可能导致额外的锁的竞争,尤其是在高并发的环境中。如果多个查询需要回表,并试图访问相同的数据行,可能会引发锁的争夺,影响系统的并发性能。

所以:

  1. 能不回表就不回;
  2. 必须回表就减少回表的次数。

 

优化回表策略:

3.3.1索引覆盖

想一下,如果非聚簇索引的叶子节点上有你想要的所有数据,是不是就不需要回表了呢?比如我为 name 和 phone 字段创建了一个联合索引,如果我们恰好只想搜索 name、phone、主键字段:

select id,name,phone from t_user where name='张三';

可以直接从叶子节点获取所有数据,根本不需要回表操作。

我们把索引中已经包含了所有需要读取的列数据的查询方式称为覆盖索引(或索引覆盖)。

 

重要概念:

3.3.2索引下推

还是拿name和phone的联合索引为例,我们要查询所有name为「蝉沐风」,并且手机尾号为6606的记录,查询SQL如下:

SELECT * FROM user_innodb WHERE name = "蝉沐风" AND phone LIKE "%6606";

由于联合索引的叶子节点的记录是先按照name字段排序,name字段相同的情况下再按照phone字段排序,因此把%加在phone字段前面的时候,是无法利用索引的顺序性来进行快速比较的,也就是说这条查询语句中只有name字段可以使用索引进行快速比较和过滤。正常情况下查询过程是这个样子的:

  1. InnoDB使用联合索引查出所有name为蝉沐风的二级索引数据,得到3个主键值:3485,78921,423476;
  2. 拿到主键索引进行回表,到聚簇索引中拿到这三条完整的用户记录;
  3. InnoDB把这3条完整的用户记录返回给MySQL的Server层,在Server层过滤出尾号为6606的用户。

如下面两幅图所示,第一幅图表示InnoDB通过3次回表拿到3条完整的用户记录,交给Server层;第二幅图表示Server层经过phone LIKE "%6606"条件的过滤之后找到符合搜索条件的记录,返给客户端。

值得我们关注的是,索引的使用是在存储引擎中进行的,而数据记录的比较是在Server层中进行的。

现在我们把上述搜索考虑地极端一点,假如数据表中10万条记录都符合name='蝉沐风'的条件,而只有1条符合phone LIKE "%6606"条件,这就意味着,InnoDB需要将99999条无效的记录传输给Server层让其自己筛选,更严重的是,这99999条数据都是通过回表搜索出来的啊!关于回表的代价你已经知道了。

  现在引入索引下推。准确来说,应该叫做索引条件下推(Index Condition Pushdown,ICP),就是过滤的动作由下层的存储引擎层通过使用索引来完成,而不需要上推到Server层进行处理。ICP是在MySQL5.6之后完善的功能。

索引下推(Index Condition Pushdown) 是 MySQL 5.6 版本中提供的一项索引优化功能,可以在非聚簇索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数。

再回顾一下,我们第一步已经通过name = "蝉沐风"在联合索引的叶子节点中找到了符合条件的3条记录,而且phone字段也恰好在联合索引的叶子节点的记录中。这个时候可以直接在联合索引的叶子节点中进行遍历,筛选出尾号为6606的记录,找到主键值为78921的记录,最后只需要进行1次回表操作即可找到符合全部条件的1条记录,返回给Server层。

很明显,使用ICP的方式能有效减少回表的次数。

另外,ICP是默认开启的,对于二级索引,只要能把条件甩给下面的存储引擎,存储引擎就会进行过滤,不需要我们干预。

四、索引类型

1、普通索引:MySQL中基本索引类型,没有什么限制,允许在定义索引的列中插入重复值和空值,纯粹为了查询数据更快一 点。

2、唯一索引:索引列中的值必须是唯一的,但是允许为空值。

3、主键索引:是一种特殊的唯一索引,不允许有空值。(主键约束,就是一个主键索引)。

4、联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。

5、全文索引:对文本的内容进行分词,进行搜索。目前只有 CHAR、VARCHAR、TEXT列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。

  • 聚簇索引(聚集索引):索引结构和数据一起存放的索引,InnoDB 中的主键索引就属于聚簇索引。

 

本文参考:

https://www.chanmufeng.com/posts/storage/MySQL/

https://mp.weixin.qq.com/s/8qemhRg5MgXs1So5YCv0fQ

https://javaguide.cn/database/mysql/mysql-index.html