MySQL索引篇

2.索引

EXPLAIN-执行计划

索引分类

  • **按「数据结构」分类:**B+tree 索引、Hash 索引、Full-text 索引。

  • 按「物理存储」分类:

    • 聚簇索引(主键索引):主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里;

    • 二级索引(辅助索引):二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据。

    • 在查询时使用了二级索引,如果查询的数据能在二级索引里查询的到,那么就不需要回表,这个过程就是覆盖索引 。如果查询的数据不在二级索引里,就会先检索二级索引,找到对应的叶子节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表

  • 按「字段特性」分类:

    • **主键索引:**主键索引就是建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。

    • **唯一索引:**唯一索引建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。

    • **普通索引:**普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE。

    • 前缀索引: 前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为 char、 varchar、binary、varbinary 的列上。

  • 按「字段个数」分类:

    • **单列索引:**建立在单列上的索引称为单列索引,比如主键索引。

    • **联合索引:**建立在多列上的索引称为联合索引。

为什么选 B+Tree

磁盘读写的最小单位是扇区 ,扇区的大小只有 512B 大小,操作系统的最小读写单位是块(Block),操作系统一次会读写多个扇区, Linux 中的块大小为 4KB ,也就是一次磁盘 I/O 操作会直接读写 8 个扇区。(说明 IO 的性能损耗)

由于数据库的索引是保存到磁盘上的,因此当我们通过索引查找某行数据的时候,就需要先从磁盘读取索引 到内存,再通过索引从磁盘中找到某行数据对应的数据页,然后读入到内存,也就是说查询过程中会发生多次磁盘 I/O,而磁盘 I/O 次数越多,所消耗的时间也就越大,每走一层索引就需要一次 IO,所以索引结构要尽可能矮。

另外,MySQL 是支持范围查找的,所以索引的数据结构不仅要能高效地查询某一个记录,而且也要能高效地执行范围查找。

所以,要设计一个适合 MySQL 索引的数据结构,至少满足以下要求:

  • 能在尽可能少的磁盘的 I/O 操作中完成查询工作 -- 索引结构要矮;

  • 要能高效地查询某一个记录,也要能高效地执行范围查找 -- 行数据相连,快速范围查询;

B+Tree 存储千万级的数据只需要 3-4 层高度就可以满足,这意味着从千万级的表查询目标数据最多需要 3-4 次磁盘 I/O,所以 B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4 次。

  1. B+Tree vs B Tree

B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,所以 B+Tree 的单个节点的数据量更小,在相同的磁盘 I/O 次数下,就能查询更多的节点。数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O 次数会更少。

另外,B+Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的基于范围的顺序查找,而 B 树无法做到这一点。

B+ 树有大量的冗余节点,这样使得删除一个节点的时候,可以直接从叶子节点中删除,甚至可以不动非叶子节点,这样删除非常快。

B+ 树的插入也是一样,有冗余节点,插入可能存在节点的分裂(如果节点饱和),但是最多只涉及树的一条路径。而且 B+ 树会自动平衡,不需要像更多复杂的算法,类似红黑树的旋转操作等。

  • B+Tree vs 二叉树

二叉查找树的特点是一个节点的左子树的所有节点都小于这个节点,右子树的所有节点都大于这个节点,这样我们在查询数据时,不需要计算中间节点的位置了,只需将查找的数据与节点的数据进行比较。

当每次插入的元素都是二叉查找树中最大的元素,二叉查找树就会退化成了一条链表,查找数据的时间复杂度变成了 O(n)。

由于树是存储在磁盘中的,访问每个节点,都对应一次磁盘 I/O 操作(假设一个节点的大小「小于」操作系统的最小读写单位块的大小),也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数,所以树的高度越高,就会影响查询性能。

对于有 N 个叶子节点的 B+Tree,其搜索复杂度为O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。

在实际的应用当中, d 值是大于 100 的,这样就保证了,即使数据达到千万级别时,B+Tree 的高度依然维持在 3~4 层左右,也就是说一次数据查询操作只需要做 3~4 次的磁盘 I/O 操作就能查询到目标数据。

而二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 O(logN),这已经比 B+Tree 高出不少,因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多。

不管平衡二叉查找树还是红黑树,都会随着插入的元素增多,而导致树的高度变高,这就意味着磁盘 I/O 操作次数多,会影响整体数据查询的效率。

因此,当树的节点越多的时候,并且树的分叉数 M 越大的时候,M 叉树的高度会远小于二叉树的高度。

  • B+Tree vs Hash

Hash 在做等值查询的时候效率贼快,搜索复杂度为 O(1)。

但是 Hash 表不适合做范围查询,它更适合做等值的查询,这也是 B+Tree 索引要比 Hash 表索引有着更广泛的适用场景的原因。

回表查询

主键索引的 B+Tree 和二级索引的 B+Tree 区别如下:

  • 主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里;

  • 二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据。

先检二级索引中的 B+Tree 的索引值(商品 id,product_no),找到对应的叶子节点然后获取主键值然后再通过主键索引中的 B+Tree 树查询到对应的叶子节点然后获取整行数据 。这个过程叫**「回表」** ,也就是说要查两个 B+Tree 才能查到数据

索引覆盖

不过,当查询的数据是能在二级索引的 B+Tree 的叶子节点里查询到,这时就不用再查主键索引查,比如下面这条查询语句:

这种在二级索引的 B+Tree 就能查询到结果的过程就叫作「覆盖索引」,也就是只需要查一个 B+Tree 就能找到数据。

最左匹配原则

联合索引的非叶子节点用两个字段的值作为 B+Tree 的 key 值。当在联合索引查询数据时,先按 product_no 字段比较,在 product_no 相同的情况下再按 name 字段比较。

也就是说,联合索引查询的 B+Tree 是先按 product_no 进行排序,然后在 product_no 相同的情况再按 name 字段排序。

因此,使用联合索引时,存在最左匹配原则 ,也就是按照最左优先的方式进行索引的匹配 。在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引会失效 ,这样就无法利用到索引快速查询的特性了。

利用索引的前提是索引里的 key 是有序的,name 只有在 product_no 相同的情况下才是有序,全局 name 是无序的。

联合索引范围查询

**并不是查询过程使用了联合索引查询,就代表联合索引中的所有字段都用到了联合索引进行索引查询,**也就是可能存在部分字段用到联合索引的 B+Tree,部分字段没有用到联合索引的 B+Tree 的情况。

这种特殊情况就发生在范围查询 。联合索引的最左匹配原则会一直向右匹配直到遇到「范围查询」就会停止匹配。也就是范围查询的字段可以用到联合索引 ,但是在范围查询字段的后面的字段无法用到联合索引

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

Q1: select * from t_table where a > 1 and b = 2,联合索引(a, b)哪一个字段用到了联合索引的 B+Tree?

由于联合索引(二级索引)是先按照 a 字段的值排序的,所以符合 a > 1 条件的二级索引记录肯定是相邻,于是在进行索引扫描的时候,可以定位到符合 a > 1 条件的第一条记录,然后沿着记录所在的链表向后扫描,直到某条记录不符合 a > 1 条件位置。所以 a 字段可以在联合索引的 B+Tree 中进行索引查询。

但是在符合 a > 1 条件的二级索引记录的范围里,b 字段的值是无序的。比如前面图的联合索引的 B+ Tree 里,下面这三条记录的 a 字段的值都符合 a > 1 查询条件,而 b 字段的值是无序的:

  • a 字段值为 5 的记录,该记录的 b 字段值为 8;

  • a 字段值为 6 的记录,该记录的 b 字段值为 10;

  • a 字段值为 7 的记录,该记录的 b 字段值为 5;

因此,我们不能根据查询条件 b = 2 来进一步减少需要扫描的记录数量(b 字段无法利用联合索引进行索引查询的意思)。

所以在执行 Q1 这条查询语句的时候,对应的扫描区间是 (2, + ∞),形成该扫描区间的边界条件是 a > 1,与 b = 2 无关。

因此,Q1 这条查询语句只有 a 字段用到了联合索引进行索引查询,而 b 字段并没有使用到联合索引。

Q2: select * from t_table where a >= 1 and b = 2,联合索引(a, b)哪一个字段用到了联合索引的 B+Tree?

虽然在符合 a>= 1 条件的二级索引记录的范围里,b 字段的值是「无序」的,但是对于符合 a = 1 的二级索引记录的范围里,b 字段的值是「有序」的

只有 a=1 and b=2 走了联合索引,然后后续的 a>1 其实还是不走联合索引。

对于 a>=1 AND b=2的两种不同查询方案

根据优化器的决策,存储引擎会有不同的执行方式:

方案一:a 字段的范围扫描 + b 字段作为过滤条件(较常见)

具体步骤

  1. 定位起始点 :存储引擎从根节点开始,在 B+Tree 中找到第一个满足a>=1的记录

  2. 顺序扫描 :沿着叶子节点的链表向后扫描所有满足a>=1的记录

  3. 条件过滤 :在扫描过程中,对每条记录检查b=2条件

    • 如果 Extra 字段显示"Using index condition",表示通过索引初步过滤

    • 如果显示"Using where",表示在存储引擎层或 Server 层进行过滤

  4. 回表操作:对于满足条件的记录,通过主键 ID 回表获取完整数据

b 索引的利用方式

  • b 字段不能作为索引查找条件,因为 a 字段的范围查询打断了最左前缀的连续性

  • b 字段只能作为过滤条件,在索引扫描过程中筛选数据

  • 这种过滤可能发生在:

    • 存储引擎层的索引下推(Index Condition Pushdown)

    • Server 层的 where 条件过滤

方案二:区分等值部分和范围部分(如果优化器识别出)

当优化器识别出 a=1 是等值条件时

  1. 等值部分(a=1)

    • 可以利用(a,b)联合索引进行精确查找

    • 先找到 a=1 的记录,然后在这些记录中精确匹配 b=2

    • 这时 b 字段可以完全利用索引进行查找

  2. 范围部分(a>1)

    • 从 a=1 的最后一条记录之后开始范围扫描

    • b 字段只能作为过滤条件

执行过程

  1. 精确查找阶段:使用索引找到所有 a=1 且 b=2 的记录

  2. 范围扫描阶段:从 a>1 的位置开始顺序扫描

  3. 组合结果:将两个阶段的结果合并

Q3: SELECT * FROM t_table WHERE a BETWEEN 2 AND 8 AND b = 2,联合索引(a, b)哪一个字段用到了联合索引的 B+Tree?[2,8]

这条查询语句 a 和 b 字段都用到了联合索引进行索引查询。

Q4: SELECT * FROM t_user WHERE name like 'j%' and age = 22,联合索引(name, age)哪一个字段用到了联合索引的 B+Tree?

先按照 name 字段的值排序的,所以前缀为 'j' 的 name 字段的二级索引记录都是相邻的, 于是在进行索引扫描的时候,可以定位到符合前缀为 'j' 的 name 字段的第一条记录,然后沿着记录所在的链表向后扫描,直到某条记录的 name 前缀不为 'j' 为止。

虽然在符合前缀为 'j' 的 name 字段的二级索引记录的范围里,age 字段的值是「无序」的,但是对于符合 name = j的二级索引记录的范围里,age 字段的值是「有序」的。

于是,在确定需要扫描的二级索引的范围时,当二级索引记录的 name 字段值为 'j' 时,可以通过 age = 22 条件减少需要扫描的二级索引记录范围 (age 字段可以利用联合索引进行索引查询的意思)。也就是说,从符合 name = 'j' and age = 22 条件的第一条记录时开始扫描,而不需要从第一个 name 为 j 的记录开始扫描 。

这条查询语句 a 和 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), 可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

联合索引进行排序

这里出一个题目,针对针对下面这条 SQL,你怎么通过索引来提高查询效率呢?

给 status 和 create_time 列建立一个联合索引 ,因为这样可以避免 MySQL 数据库发生文件排序

因为在查询时,如果只用到 status 的索引,但是这条语句还要对 create_time 排序 ,这时就要用文件排序 filesort ,也就是在 SQL 执行计划中,Extra 列会出现 Using filesort

所以,要利用索引的有序性,在 status 和 create_time 列建立联合索引,这样根据 status 筛选后的数据就是按照 create_time 排好序的避免在文件排序,提高了查询效率。

是否创建索引

索引最大的好处是提高查询速度,但是索引也是有缺点的,比如:

  • 需要占用物理空间,数量越大,占用空间越大;

  • 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增大;

  • 会降低表的增删改的效率,因为每次增删改索引,B+ 树为了维护索引有序性,都需要进行动态维护。

什么时候适用索引?

  • 字段有唯一性限制的,比如商品编码;

  • 经常用于 WHERE 查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。

  • 经常用于 GROUP BYORDER BY 的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的。

什么时候不需要创建索引?

  • WHERE 条件,GROUP BYORDER BY 里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。

  • 字段中存在大量重复数据 ,不需要创建索引,比如性别字段,只有男女,如果数据库表中,男女的记录分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。

  • 表数据太少的时候,不需要创建索引;

  • 经常更新的字段不用创建索引 ,比如不要对电商项目的用户余额建立索引 ,因为索引字段频繁修改,由于要维护 B+Tree 的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的。

索引失效

  • 对索引使用左或者左右模糊匹配

  • **对索引使用函数:**索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了。

  • **对索引进行表达式计算:**索引保存的是索引字段的原始值,而不是 id + 1 表达式计算后的值,所以无法走索引,只能通过把索引字段的取值都取出来,然后依次进行表达式的计算来进行条件判断,因此采用的就是全表扫描的方式。将 id + 1 = 10 变成 id = 10 - 1,也是不行的。

  • **对索引进行隐式类型转换:**MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。

  • 联合索引非最左匹配原则

  • **WHERE 子句中的 OR:**OR 的含义就是两个只要满足一个即可,因此只有一个条件列是索引列是没有意义的,只要有条件列不是索引列,就会进行全表扫描。( age = 18 or id = 1)

相关推荐
oradh4 小时前
Oracle数据库表存储基本概述
数据库·oracle·oracle基础·oracle入门·oracle表存储
为什么不问问神奇的海螺呢丶4 小时前
Oracle Golden Gate 19c 微服务版 (19.1.0.0.4) 静默安装
数据库·微服务·oracle
NineData4 小时前
使用NineData实现MySQL异地多活场景
运维·数据库·mysql
xinhuanjieyi4 小时前
php setplayersjson实现类型转换和文件锁定机制应对高并发
android·开发语言·php
森叶4 小时前
逻辑仲裁者:实现多事件关联匹配与事务原子化后执行逻辑的技术方案
数据库·oracle
Navicat中国4 小时前
北京理工大学推荐 Navicat | 高校教育行业应用案例
数据库·navicat·高校·教育版
533_4 小时前
[vxe-table] 表头:点击出现输入框
android·java·javascript
邹阿涛涛涛涛涛涛4 小时前
Jetpack Compose Modifier 深度解析:从链式调用到 Modifier.Node
android
素玥5 小时前
实训7 json文件数据用python导入数据库
数据库·python·json