MySQL 基础篇(九):从 B+ 树到索引优化,全面理解 MySQL 索引

目录

本文内容概要

一、认识索引

[1.1 什么是索引](#1.1 什么是索引)

[1.2 为什么需要索引](#1.2 为什么需要索引)

[1.3 索引的优点和缺点](#1.3 索引的优点和缺点)

二、索引的底层实现原理

[2.1 常见的索引数据结构](#2.1 常见的索引数据结构)

[2.2 B+ 树的数据结构和特点](#2.2 B+ 树的数据结构和特点)

[2.3 为什么 InnoDB 主要采用 B+ 树索引](#2.3 为什么 InnoDB 主要采用 B+ 树索引)

[2.4 聚簇索引与二级索引](#2.4 聚簇索引与二级索引)

[2.5 什么是回表查询](#2.5 什么是回表查询)

三、索引的分类

[3.1 主键索引](#3.1 主键索引)

[3.2 唯一索引](#3.2 唯一索引)

[3.3 普通索引](#3.3 普通索引)

[3.4 联合索引](#3.4 联合索引)

[3.5 全文索引(了解)](#3.5 全文索引(了解))

四、索引的基本操作

[4.1 创建索引](#4.1 创建索引)

[4.2 查看索引](#4.2 查看索引)

[4.3 删除索引](#4.3 删除索引)

五、索引的使用规则

[5.1 最左前缀匹配原则](#5.1 最左前缀匹配原则)

[5.2 联合索引的范围查询](#5.2 联合索引的范围查询)

[5.3 覆盖索引](#5.3 覆盖索引)

[5.4 索引下推](#5.4 索引下推)

[5.5 索引与 ORDER BY 排序](#5.5 索引与 ORDER BY 排序)

六、索引失效的常见场景

[6.1 不满足最左前缀匹配原则](#6.1 不满足最左前缀匹配原则)

[6.2 对索引字段进行函数或表达式操作](#6.2 对索引字段进行函数或表达式操作)

[6.3 隐式类型转换](#6.3 隐式类型转换)

[6.4 LIKE 模糊匹配](#6.4 LIKE 模糊匹配)

[6.5 OR 条件与索引使用](#6.5 OR 条件与索引使用)

[6.6 优化器选择全表扫描](#6.6 优化器选择全表扫描)

七、索引分析与优化

[7.1 使用 EXPLAIN 分析 SQL 执行计划](#7.1 使用 EXPLAIN 分析 SQL 执行计划)

[7.2 如何判断 SQL 是否有效利用索引](#7.2 如何判断 SQL 是否有效利用索引)

[7.3 索引设计的基本原则](#7.3 索引设计的基本原则)


本文内容概要

本文主要围绕 MySQL 索引的基本概念、底层实现原理及实际使用规则展开介绍。

通过本文的学习,需要掌握索引的作用与常见分类,理解 InnoDB B+ 树索引的基本结构,明确聚簇索引、二级索引与回表查询之间的关系;掌握索引的创建、查看与删除操作,理解联合索引的最左前缀匹配原则、范围查询、覆盖索引和索引下推机制;了解常见的索引失效场景,并能够结合 EXPLAIN 执行计划,初步分析 SQL 的索引使用情况。

需要明确的是,索引的核心目标是降低数据访问成本,而不是让每一条 SQL 都必须使用索引。 在实际开发中,应结合业务查询需求、数据分布及维护开销,合理设计和使用索引。

一、认识索引

1.1 什么是索引

索引是数据库中用于提高数据检索效率的一种数据结构。其作用类似于数据的目录,能够帮助数据库快速定位符合查询条件的数据,而不再需要遍历整张数据表。

例如:当我们阅读一本书籍时,如果想要查找某个知识点,可以有两种方式:

  • 不使用目录:从第一页开始逐页查找,直到找到目标内容
  • 使用目录:根据目录中的关键字和页码,快速定位到目标内容所在的位置

数据库中的索引也具有类似的作用。

在 MySQL 中,索引由存储引擎负责实现和维护。不同存储引擎支持的索引结构可能不同,其中 InnoDB 存储引擎主要使用 B+ 树作为索引的数据结构。

1.2 为什么需要索引

假设存在一张 student 数据表,里面记录了 100 万条学生记录,现在需要查询年龄为 20 岁的学生信息。

  1. 没有建立索引:当 age 字段没有建立索引时,MySQL 需要采用全表遍历的方式,逐条读取记录并判断 age = 20 是否成立,随着数据量不断增加,需要遍历的记录数量不断增多,磁盘的 IO 次数随之增加,此时查询效率必然降低
  2. 建立索引:当 age 字段建立索引时,MySQL 可以利用索引快速定位年龄为 20 岁的记录,而不必遍历整张数据表,进而减小遍历次数和磁盘的 IO 次数,从而大大提高了查询效率 。(此处想要理解明白,就必须要理解一种数据结构:B+ 树)

因此,索引的核心作用是:通过合理的数据组织方式,减少查询过程中需要访问的数据量,从而提高数据检索效率。

1.3 索引的优点和缺点

索引的优点

  1. 提高查询效率:可以快速定位符合条件的记录,减少不必要的数据扫描
  2. 提高排序效率:对于满足特定条件的查询,可以利用索引的有序性,减少额外的排序消耗
  3. 提高分组效率:对于某些 GROUP BY 查询可以利用索引的有序性进行优化
  4. 保证数据唯一性:唯一索引能够约束索引字段的取值,避免出现不符合唯一性约束的记录

索引的缺点

  1. 占用额外的存储空间:除了表中的数据,还需要额外维护索引结构
  2. 增加数据维护成本:对数据进行 INSERT、UPDATE、DELETE 操作时,可能需要同步修改相关索引,从而带来额外开销
  3. 增加索引管理成本:索引数量过多,不仅浪费空间,还会增加数据库维护的复杂度

二、索引的底层实现原理

2.1 常见的索引数据结构

索引的本质是一种用于提高数据检索效率的数据结构。那么为什么 MySQL 的 InnoDB 存储引擎主要采用 B+ 树作为索引结构,而不是其他数据结构呢?

1. 哈希表

哈希表通过哈希函数将键值映射到对应的存储位置,从而实现快速的数据检索。

在理想情况下,哈希表的平均查询时间复杂度为 O(1)。

但是,哈希表存在一个明显的问题:无法利用键值的有序性进行范围查询。

例如:

复制代码
SELECT * FROM student WHERE age BETWEEN 18 AND 25;

哈希表无法直接根据键值顺序定位这个范围内的所有记录,因此不适合作为数据库通用索引的主要数据结构。

2. 二叉搜索树

二叉搜索树具有以下特点:

  • 左子树所有节点的值小于根节点。

  • 右子树所有节点的值大于根节点。

  • 左右子树同样满足二叉搜索树的性质。

由于二叉搜索树具有有序性,因此既能进行等值查询,也能支持范围查询。

但是,普通二叉搜索树可能退化成链表。

例如,当数据按照 1、2、3、4的顺序依次插入时,如果没有平衡机制,树可能向单侧不断延伸。

此时,查询时间复杂度将从理想情况下的 O(LogN) 退化为 O(N)。

3. 平衡二叉搜索树

平衡二叉搜索树通过维护树的平衡性,避免普通二叉搜索树退化成链表。

常见的平衡搜索树包括 AVL 树和红黑树。

虽然它们能够保证较好的查询时间复杂度,但是每个节点通常只有两个子节点。

对于数据库而言,如果数据量非常庞大,树的高度仍然可能较高。

而数据库的数据通常存储在磁盘中,访问数据时需要考虑磁盘 I/O 的开销。因此,降低树的高度、提高单次页面访问能够覆盖的数据量十分重要。

4. B 树

B 树是一种多路平衡搜索树。

与二叉搜索树不同,B 树的一个节点可以存储多个键值,同时拥有多个子节点。

因此,在存储相同数量的数据时,B 树通常能够保持更低的高度,从而减少查询过程中需要访问的节点数量。

5. B+ 树

B+ 树是在 B 树基础上形成的一种多路平衡搜索树,具有以下特点:

  • 非叶子节点主要负责存储索引键值和子节点指针。

  • 真正的索引记录集中存储在叶子节点。

  • 所有叶子节点处于相同的深度。

  • 叶子节点按照索引键值顺序组织,便于进行范围查询。

对于数据库而言,B+ 树不仅能够实现高效的等值查询,还能较好地支持范围查询与有序扫描。

总结:

数据结构 优点 缺点
哈希表 等值查询效率高 不支持基于键值顺序的范围查询
二叉搜索树 结构简单,支持有序查询 可能退化为链表
平衡二叉搜索树 查询性能稳定 分支数量少,树可能较高
B 树 多路搜索,树高度较低 非叶子节点也可以存储数据记录
B+ 树 树高度低,适合范围查询和有序扫描 需要维护有序的索引结构

2.2 B+ 树的数据结构和特点

B+ 树是一种多路平衡搜索树,它是 InnoDB 实现索引采用的核心数据结构

1.B+ 树的基本结构

B+ 树主要由三部分组成:

  • 根节点: 整棵树的入口,负责根据索引键值引导查询

  • 非叶子节点: 存储用于导航的键值和子节点指针

  • 叶子节点: 存储真正的索引记录

假设需要查找键值为 25 的记录:

  1. 首先访问根节点,根据键值 25 确定目标所在的子树。

  2. 沿着对应的子节点继续向下查找。

  3. 最终定位到包含 25 的叶子节点,并查找对应的索引记录。

由于 B+ 树是一种多路平衡搜索树,因此可以通过较少的节点访问完成查询。

2. B+ 树的主要特点

(1)非叶子节点主要用于索引导航

B+ 树的非叶子节点主要保存键值和子节点指针,而不是完整的数据行。

这样能够让一个节点容纳更多的索引键值,从而提高树的分支数量,降低树的高度。

(2)所有叶子节点位于同一层

B+ 树是一种平衡搜索树,所有叶子节点都处于相同深度。

因此,通过索引从根节点定位到叶子节点时,路径长度相对稳定。

(3)叶子节点之间具有顺序关联

B+ 树的叶子节点按照索引键值顺序排列。

在 InnoDB 中,叶子页面之间还通过双向链表建立联系。

因此,当数据库需要进行范围查询时,可以先定位范围起点,再沿着叶子页面的顺序访问后续记录。

例如:

复制代码
SELECT * FROM student
WHERE age BETWEEN 18 AND 25;

通过 age 字段上的 B+ 树索引,可以先定位到年龄为 18 的起始位置,再依次扫描满足范围条件的索引记录,而不必对每一个年龄值都重新从根节点查找。

(4)具有较低的树高度

B+ 树的一个节点可以拥有多个子节点,因此相比二叉搜索树,可以使用更少的层数组织大量数据。从而减少查询过程中需要访问的索引页面数量,大量地减少了磁盘的 I/O 次数。

2.3 为什么 InnoDB 主要采用 B+ 树索引

理解了 B+ 树的结构之后,我们再来分析 InnoDB 为什么主要选择 B+ 树。

1. 减少磁盘 I/O 开销

数据库中的数据往往非常庞大,不可能全部保存在内存中,因此需要频繁访问磁盘中的数据页面。

InnoDB 以页(Page)作为存储管理的基本单位之一,默认页大小为 16KB。

对于 B+ 树来说,一个非叶子页面可以存储多个索引键值与子页面指针。

这意味着每访问一个非叶子页面,就可以确定下一步应当进入哪一个子树。

由于一个页面能够容纳较多的分支信息,B+ 树通常具有较低的高度。

例如,一棵 B+ 树如果平均每个非叶子节点有 100 个子节点,那么在理想情况下:

  • 第一层可以指向约 100 个子节点。

  • 第二层可以覆盖约 10,000 个子节点。

  • 第三层可以覆盖约 1,000,000 个子节点。

树的高度越低意味着从根节点定位到目标叶子节点所需访问的页越少,意味着磁盘 I/O 次数越小。

2. 更适合范围查询

数据库不仅需要进行等值查询,还经常需要执行范围查询。

例如:

复制代码
SELECT * FROM student WHERE age >= 20;

由于 B+ 树具有有序性,并且叶子页面之间存在顺序关联,因此可以从满足条件的起始位置向后扫描。

相比之下,哈希表虽然适合等值查询,却无法直接利用键值顺序完成范围扫描。

3. 有利于减少额外排序

B+ 树中的索引记录按照索引键值有序组织。

因此,对于满足特定条件的 ORDER BY 查询,MySQL 可以利用索引的有序性完成扫描,而不必进行额外排序。

例如,当 age 字段建立了索引时:

复制代码
SELECT age FROM student ORDER BY age;

该查询就有机会直接利用索引的顺序返回结果。

综上所述,B+ 树通过多路平衡结构降低树的高度,同时利用索引键值的有序性支持等值查询、范围查询和有序扫描,因此非常适合数据库索引的应用场景。

2.4 聚簇索引与二级索引

在 InnoDB 存储引擎中,根据索引记录与实际行数据之间的存储关系,可以将普通 B+ 树索引分为聚簇索引和二级索引。

首先创建一张学生表:

sql 复制代码
CREATE TABLE student (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50),
    age INT,
    major VARCHAR(50),
    INDEX idx_age(age)
) ENGINE = InnoDB;

其中:

  • id 为主键。

  • id_age 是建立在 age 字段上的普通索引。

1. 聚簇索引

聚簇索引是将索引结构与实际行数据组织在一起的一种索引。

在 InnoDB 中,聚簇索引的叶子节点存储完整的行记录,因此通过聚簇索引定位到目标叶子记录后,就能够获取对应的行数据。

对于上面的学生表, id 是主键,因此 InnoDB 使用 id 构建聚簇索引。

其叶子节点可以简化理解为:

sql 复制代码
聚簇索引(按照 id 排序)

[id=1, name=张三, age=18, major=计算机]
                  ↕
[id=2, name=李四, age=20, major=软件工程]
                  ↕
[id=3, name=王五, age=21, major=计算机]

注:这里展示的是叶子记录按主键排序的逻辑关系,并不表示每条记录分别占用一个页面。

例如:

sql 复制代码
SELECT * FROM student WHERE id = 2;

InnoDB 可以通过聚簇索引定位到 id = 2 的叶子记录,从而获取完整的学生信息。

需要注意,每张 InnoDB 数据表都必须具有聚簇索引,其选择规则如下:

  1. 如果数据表定义了主键,则使用主键作为聚簇索引。

  2. 如果没有主键,则选择第一个所有索引列均为 NOT NULL 的唯一索引作为聚簇索引。

  3. 如果两者都不存在,InnoDB 会自动生成隐藏的聚簇索引 GEN_CLUST_INDEX ,使用内部生成的行 ID 组织记录。

聚簇索引不完全等同于主键索引,只是在定义了主键的情况下,两者通常对应同一棵索引树。

2. 二级索引

二级索引也称为辅助索引,是相对于聚簇索引而言的。

与聚簇索引不同,InnoDB 二级索引的叶子记录通常并不包含完整的行数据,而是包含:

  • 二级索引字段的值。

  • 对应行的主键值。

例如,前面创建的 idx_age 索引,其叶子记录可以简化理解为:

sql 复制代码
二级索引 idx_age(按照 age 排序)

[age=18, id=1]
        ↕
[age=20, id=2]
        ↕
[age=21, id=3]

可以发现,二级索引中只包含 age 和 id,并没有包含 name, major 等完整字段。

当使用二级索引查询时,MySQL 可以先通过 age 找到对应的主键值,再根据主键值到聚簇索引中查询完整行记录。这种查询方式,就是接下来需要介绍的回表查询。

3. 聚簇索引与二级索引的区别

对比维度 聚簇索引 二级索引
组织依据 聚簇键值 二级索引键值
叶子节点 存储完整行记录 存储索引字段和主键值
数量 每张 InnoDB 表只有一个 一张表可以有多个
获取完整行数据 可直接获取 可能需要回表
与主键的关系 有主键时采用主键 通过主键定位聚簇索引记录

2.5 什么是回表查询

回表查询是指:通过二级索引找到满足条件的主键值后,再使用主键值访问聚簇索引,以获取二级索引中不存在的字段。

继续使用前面的 student 数据表。

假设需要查询所有年龄为 20 岁的学生信息:

由于 age 字段已经建立了二级索引,假设 MySQL 优化器选择使用 idx_age,那么查询过程可以分为以下几个步骤。

第一步:查询二级索引

根据查询条件 age = 20,首先在 idx_age 对应的 B+ 树中查找符合条件的索引记录。

假设找到:

复制代码
[age=20, id=2]

此时,已经获得该学生对应的主键值 id = 2。

但是,由于查询语句使用了 SELECT *,还需要获取 name、major 等字段,而这些字段并不存在于 idx_age 索引记录中。

第二步:查询聚簇索引

根据刚刚获取的主键值 id = 2,再到聚簇索引中查找对应的完整行记录:

复制代码
[id=2, name=李四, age=20, major=软件工程]

上述过程就是一次典型的回表查询。

为什么回表可能降低查询效率?

因为一次二级索引查询可能需要访问两棵 B+ 树:

  1. 首先访问二级索引的 B+ 树,获取主键值。

  2. 然后访问聚簇索引的 B+ 树,获取完整的行数据。

如果二级索引匹配的记录数量非常多,那么就可能产生大量回表操作,从而增加页面访问和数据读取的成本。

需要注意,并不是每次回表都会产生磁盘 I/O。如果相关页面已经缓存在 InnoDB Buffer Pool 中,就可以直接从内存中获取数据。

如何避免回表查询?

一种常见方式就是使用覆盖索引。

所谓覆盖索引,是指查询所需要的字段都能够从某个索引中获取,而不必再次访问聚簇索引读取其他字段。

例如:

sql 复制代码
SELECT id, age FROM student WHERE age = 20;

对于前面的 idx_age 二级索引,其叶子记录已经包含:

  • age:二级索引字段。

  • id:对应记录的主键值。

直接从该索引获取查询所需的数据,而不需要执行回表查询。这种方式能够减少额外的数据页面访问,提高查询效率。

三、索引的分类

3.1 主键索引

主键索引是建立在主键字段上的索引,用于唯一标识数据表中的每一条记录。

主键索引具有以下特点:

  • 主键字段的值必须唯一,不允许重复。

  • 主键字段不允许为 NULL。

  • 一张数据表最多只能定义一个主键,但主键可以由多个字段组成。

  • 在 InnoDB 中,如果定义了主键,则使用主键作为聚簇索引。

sql 复制代码
CREATE TABLE student (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50),
    age INT
) ENGINE = InnoDB;

其中,id 字段被设置为主键,MySQL 会自动为其建立主键索引。

也可以使用 ALTER TABLE 为没有主键的数据表添加主键:

sql 复制代码
alter table student add primary key(id);

注意:该语句只适用于当前尚未定义主键,并且 id 字段中的数据符合主键约束的情况。

补充:在 InnoDB 中,通过主键索引查询时,可以直接定位到存储完整行记录的叶子节点。

3.2 唯一索引

唯一索引用于保证索引字段的值满足唯一性约束。

与主键索引不同,唯一索引允许字段为 NULL。在 MySQL 中,对于允许为 NULL 的唯一索引列,可以存在多条包含 NULL 的记录。

例如:

sql 复制代码
CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    email VARCHAR(100),
    UNIQUE KEY idx_email(email)
);

其中,email 字段建立了唯一索引。

如果已经存在以下数据:

sql 复制代码
insert into users values(1, 'abc@qq.com');

再次插入相同的邮箱:

sql 复制代码
insert into users values(2, 'abc@qq.com');

由于违反唯一性约束,MySQL 将拒绝插入。

除了约束数据唯一性,唯一索引还可以用于提高对应字段的查询效率。

3.3 普通索引

普通索引是最常见的一种索引,主要用于提高查询效率,不对字段的唯一性进行限制。

例如:

sql 复制代码
create index idx_age on student(age);

该语句为 student 数据表中的 age 字段建立了普通索引。

建立索引之后,下面的查询就有机会利用该索引:

sql 复制代码
select * from student where age = 20;

普通索引允许出现重复值,也允许出现 NULL 值。

补充:普通索引虽然能够提高查询效率,但并不意味着所有查询都会使用索引,具体还需要由 MySQL 优化器进行判断。

3.4 联合索引

联合索引也称为复合索引,是指将多个字段组合在一起建立的索引。

例如:

sql 复制代码
create index idx_major_age_name on student(major, age, name);

该语句建立了一个包含三个字段的联合索引:

sql 复制代码
(major, age, name)

对于联合索引,其内部索引记录并不是分别按照三个字段独立排序,而是遵循以下规则:

  1. 首先按照 major 字段排序。

  2. 如果 major 相同,则按照 age 字段排序。

  3. 如果 major 和 age 都相同,则按照 name 字段排序。

联合索引的有序性是由多个字段共同决定的。因此,联合索引的查询使用需要遵循一定规则,其中最重要的就是最左前缀匹配原则。关于联合索引的具体使用规则,将在第五章进一步讲解。

3.5 全文索引(了解)

全文索引主要用于对文本内容进行全文检索。

与普通 B+ 树索引不同,全文索引主要用于根据文本中的词项查找相关记录,而不是简单地按照字段值进行有序查询。

sql 复制代码
CREATE TABLE article (
    id BIGINT PRIMARY KEY,
    content TEXT,
    FULLTEXT INDEX idx_content(content)
) ENGINE = InnoDB;

随后可以通过 MATCH ... AGAINST 进行全文检索:

sql 复制代码
SELECT * FROM article
WHERE MATCH(content)
AGAINST('database' IN NATURAL LANGUAGE MODE);

补充:全文索引的检索效果受到分词方式、停用词以及相关配置的影响。对于中文文本,通常还需要考虑合适的分词器。

在当前阶段,只需要了解全文索引的基本用途即可,不需要深入研究全文搜索引擎的实现。

四、索引的基本操作

4.1 创建索引

MySQL 支持通过 CREATE INDEX 和 ALTER TABLE 等方式创建索引。

1. 创建普通索引

基本语法:

sql 复制代码
create index 索引名称 on 表名(字段名);

例如:

sql 复制代码
create index idx_age on student(age);

该语句在 age 字段上建立普通索引。

2. 创建唯一索引

基本语法:

sql 复制代码
create unique index 索引名称 on 表名(字段);

例如:

sql 复制代码
create unique index idx_email on users(email);

注意:建立唯一索引之前,应确保数据满足唯一性要求,否则创建索引可能失败。

3. 创建联合索引

基本语法:

sql 复制代码
create index 索引名称 on 表名(字段, 字段, ...);

例如:

sql 复制代码
create index idx_major_age_name on student(major, age, name);

联合索引中字段的排列顺序十分重要,因为索引记录会按照定义的字段顺序进行组织。

4. 使用 ALTER TABLE 创建索引

例如:

sql 复制代码
alter table student add index idx_age(age);

也可以添加联合索引:

sql 复制代码
alter table student add unique index idx_major_age_name(major, age, name);

注意:以上语句是不同创建方式的演示,不应对同一张表反复创建相同的索引。

4.2 查看索引

基本语法:

sql 复制代码
show index from 表名;

常见的返回字段如下:

字段 含义
Table 索引所属的数据表
Non_unique 是否允许重复,0 表示唯一
Key_name 索引名称
Seq_in_index 字段在索引中的顺序
Column_name 索引对应的字段
Collation 索引的排序方向
Cardinality 索引不同键值数量的估计值
Index_type 索引类型,如 BTREE

其中,需要重点关注:

  • Key_name:索引名称。

  • Seq_in_index:联合索引中字段的排列顺序。

  • Column_name:索引包含的字段。

  • Non_unique:是否为唯一索引。

4.3 删除索引

基本语法:

sql 复制代码
drop index 索引名称 on 表名;

例如:

sql 复制代码
drop index idx_age on student;

也可以使用:

sql 复制代码
alter table student drop index idx_age;

对于主键索引,删除主键的语法为:

sql 复制代码
alter table student frop primary key;

补充:InnoDB 中主键通常对应聚簇索引。删除主键可能涉及聚簇索引重建,而且在自增字段、外键或其他约束存在时还可能受到限制,因此不应随意修改。

五、索引的使用规则

索引虽然能够提高查询效率,但是索引并不是建立之后就一定能够充分发挥作用。尤其对于联合索引而言,索引字段的排列顺序、查询条件以及查询字段的选择,都会影响索引的使用效果。

本章重点介绍最左前缀匹配原则、联合索引的范围查询、覆盖索引、索引下推以及索引排序。

5.1 最左前缀匹配原则

最左前缀匹配原则是联合索引使用过程中最重要的规则之一。

假设存在以下联合索引:

复制代码
CREATE INDEX idx_major_age_name
ON student(major, age, name);

索引字段顺序为:

复制代码
(major, age, name)

由于联合索引首先按照 major 排序,在 major 相同时按照 age 排序,最后再按照 name 排序,因此索引的有序性具有明显的层次关系。

1. 满足最左前缀匹配

例如:

sql 复制代码
SELECT * FROM student
WHERE major = '计算机';

由于查询条件包含联合索引的第一个字段 major,因此可以利用索引定位对应的范围。

继续增加条件:

sql 复制代码
SELECT * FROM student
WHERE major = '计算机'
AND age = 20;

由于在 major 相同的情况下,索引记录继续按照 age 排序,因此可以进一步缩小查询范围。

如果三个字段都参与查询:

sql 复制代码
SELECT * FROM student
WHERE major = '计算机'
AND age = 20
AND name = '张三';

则可以利用三个字段共同确定更加精确的索引查找范围。

因此,对于 (major, age, name) 联合索引,典型的最左前缀包括:

复制代码
(major)

(major, age)

(major, age, name)

2. 不满足最左前缀匹配

例如:

sql 复制代码
SELECT * FROM student
WHERE age = 20;

对于该联合索引而言,age 并不是第一个字段。

由于不同 major 对应的记录都可能包含 age = 20,因此不能像查询 major 一样,直接按照传统最左前缀规则定位一个连续范围。

类似地:

sql 复制代码
SELECT * FROM student
WHERE age = 20
AND name = '张三';

也没有提供联合索引的首列条件。

不过,需要注意:不满足最左前缀规则,不代表数据库绝对不可能使用该索引。

例如,MySQL 8.0 引入的 Skip Scan 优化,在满足一定条件时可以利用缺少前导列条件的联合索引。此外,优化器也可能选择完整索引扫描或其他索引。

最左前缀原则主要用于理解联合索引的常规有序查找能力。

3. WHERE 条件顺序是否影响联合索引

例如:

sql 复制代码
SELECT * FROM student
WHERE age = 20
AND major = '计算机';

虽然 SQL 中的 age 条件出现在 major 前面,但这并不意味着违反最左前缀匹配原则。

MySQL 优化器可以分析并重新组织查询条件,使用符合要求的索引访问方式。

因此,最左前缀匹配原则关注的是索引字段的排列顺序,以及查询条件能否形成有效的索引查找范围,而不是 SQL 中 WHERE 条件的书写顺序。

5.2 联合索引的范围查询

联合索引不仅可以进行等值查询,还可以进行范围查询。

继续使用以下联合索引:

复制代码
(major, age, name)

例如:

sql 复制代码
SELECT * FROM student
WHERE major = '计算机'
AND age > 20;

由于 major 为等值条件,而 age 为范围条件,因此可以利用联合索引定位满足条件的记录范围。

那么,如果范围查询之后还有其他字段参与条件判断呢?

例如:

sql 复制代码
SELECT * FROM student
WHERE major = '计算机'
AND age > 20
AND name = '张三';

对于该查询:

  1. major = '计算机' 可以用于定位索引范围。

  2. age > 20 可以继续缩小范围。

  3. name = '张三' 通常不能在前面已经形成范围扫描后,继续作为单一连续范围的边界条件。

这是因为索引首先按照 major 排序,其次按照 age 排序,只有在前面字段值相同的情况下,后续字段的有序性才能进一步用于连续范围定位。

但是,范围条件之后的索引字段并不意味着完全失去作用。

对于 name = '张三',MySQL 仍然可能通过索引条件下推等方式,在读取完整行数据之前完成部分条件判断。

此外,>、>=、BETWEEN 等条件的具体范围优化行为存在差异,不能简单理解成"出现范围条件,后面所有字段的索引都失效"。

5.3 覆盖索引

覆盖索引是指查询所需要的字段全部能够通过某个索引获得,而不必再次访问聚簇索引读取其他字段。

例如,假设存在以下联合索引:

复制代码
idx_major_age_name(major, age, name)

执行查询:

复制代码
SELECT major, age, name
FROM student
WHERE major = '计算机'
AND age = 20;

由于查询需要的三个字段都包含在联合索引中,因此如果 MySQL 选择该索引,就可以直接通过二级索引获取完整的查询结果。

此时,不需要再根据主键值访问聚簇索引。

对于 InnoDB 而言,二级索引的叶子记录还包含主键值,因此以下查询同样具有覆盖索引的条件:

复制代码
SELECT id, major, age
FROM student
WHERE major = '计算机';

覆盖索引为什么可以提高查询效率

当查询需要的字段不在二级索引中时,数据库可能需要根据主键值继续访问聚簇索引,也就是回表查询。

而覆盖索引避免了这部分额外访问,从而减少数据读取开销。

例如:

复制代码
SELECT * FROM student
WHERE major = '计算机';

如果 student 表还有其他字段不包含在索引中,就可能需要进行回表查询。

因此,在实际开发中,应当尽量避免无意义地使用 SELECT *,而是根据业务需求明确查询所需字段。

但是,覆盖索引也不是越宽越好。联合索引包含的字段越多,其存储空间和维护成本通常也会增加。

5.4 索引下推

索引下推,简称 ICP,是 MySQL 提供的一种查询优化机制。

其核心思想是:将能够利用索引记录判断的部分查询条件交给存储引擎提前判断,从而减少不必要的完整行记录读取。

继续使用:

复制代码
idx_major_age_name(major, age, name)

假设执行以下查询:

复制代码
SELECT * FROM student
WHERE major = '计算机'
AND age > 20
AND name LIKE '%明';

由于 major 可以用于索引定位,age 可以用于范围扫描,因此数据库可以先在联合索引中定位符合前两个条件的候选记录。

对于 name LIKE '%明',由于存在前导通配符,通常无法利用该字段形成有效的 B+ 树范围查找。

但是,name 本身存在于联合索引记录中。

如果没有使用 ICP:

数据库可能需要对候选记录执行回表操作,获取完整行数据后,再判断 name LIKE '%明' 是否成立。

如果使用 ICP:

存储引擎可以先利用二级索引记录中的 name 字段判断条件。

对于不符合条件的索引记录,可以直接过滤,而不必继续访问聚簇索引。

例如:

复制代码
二级索引找到候选记录
          |
          v
检查 name LIKE '%明'
          |
     +----+----+
     |         |
   不满足     满足
     |         |
   过滤      回表查询
               |
               v
            返回结果

由此可见,ICP 并不是让所有查询都不需要回表,而是尽可能在回表之前过滤掉不符合条件的记录。

对于 InnoDB,ICP 主要用于符合条件的二级索引访问。

在 EXPLAIN 执行计划中,如果使用了索引条件下推,Extra 字段可能显示:

复制代码
Using index condition

需要注意:

  • Using index 通常表示查询可以从索引中获取所需数据。

  • Using index condition 表示使用了索引条件下推。

二者并不是同一个概念。

5.5 索引与 ORDER BY 排序

除了提高数据检索效率之外,B+ 树索引还可以利用索引键值的有序性,减少额外排序操作。

假设存在以下索引:

复制代码
idx_major_age_name(major, age, name)

执行:

复制代码
SELECT major, age, name
FROM student
WHERE major = '计算机'
ORDER BY age, name;

由于 major 为固定值,而索引在相同 major 下继续按照 age、name 排序,因此该查询有机会直接按照索引顺序返回数据。

但是,如果排序字段与索引顺序不匹配:

复制代码
SELECT major, age, name
FROM student
WHERE major = '计算机'
ORDER BY name;

由于相同 major 下首先按照 age 排序,因此不能仅凭该联合索引保证 name 的全局有序性。

此时,MySQL 可能需要进行额外排序。

如果执行计划中的 Extra 字段显示:

复制代码
Using filesort

通常表示 MySQL 使用了额外的排序操作。

需要注意,filesort 并不意味着一定会使用磁盘进行排序,排序操作也可能在内存中完成。

六、索引失效的常见场景

在实际开发过程中,即使已经为某个字段建立了索引,MySQL 也不一定会在查询时使用该索引。

很多情况下,我们会将这种现象称为索引失效。

但是,需要明确的是:所谓索引失效,并不是索引的数据结构被破坏,而是当前查询条件无法有效利用索引,或者优化器认为使用索引并不是最优方案。

接下来介绍几种常见场景。

6.1 不满足最左前缀匹配原则

假设存在联合索引:

复制代码
(major, age, name)

对于以下查询:

复制代码
SELECT * FROM student
WHERE age = 20;

由于没有提供联合索引的第一个字段 major,因此通常无法利用该联合索引进行传统的最左前缀定位。

同样:

复制代码
SELECT * FROM student
WHERE name = '张三';

也无法直接利用这个联合索引的首列顺序完成常规索引查找。

注意,这里讨论的是指定联合索引的常规范围查找能力。如果 age 或 name 还存在独立索引,或者优化器可以采用 Skip Scan 等其他访问方式,实际执行计划仍然可能使用索引。

6.2 对索引字段进行函数或表达式操作

当查询条件中对索引字段直接进行函数运算或者表达式计算时,可能导致原有索引无法直接用于范围定位。

例如,假设 age 字段已经建立普通索引:

复制代码
CREATE INDEX idx_age ON student(age);

执行:

复制代码
SELECT * FROM student
WHERE age + 1 = 21;

对于普通的 age B+ 树索引来说,索引记录保存的是 age 的值,而不是 age + 1 的计算结果。

因此,该表达式通常不利于直接利用原有索引进行范围查找。

可以改写为:

复制代码
SELECT * FROM student
WHERE age = 20;

同样,对于日期字段:

复制代码
SELECT * FROM orders
WHERE YEAR(create_time) = 2026;

如果 create_time 上只有普通 B+ 树索引,这种写法通常不利于直接进行索引范围定位。

可以考虑改写为:

复制代码
SELECT * FROM orders
WHERE create_time >= '2026-01-01'
AND create_time < '2027-01-01';

这样就将函数运算转换为字段本身的范围条件。

需要注意,MySQL 8.0.13 及之后的版本支持函数索引,因此不能认为所有函数操作都绝对无法使用索引。

6.3 隐式类型转换

隐式类型转换是指查询条件中的数据类型与字段类型不一致时,MySQL 自动进行类型转换。

例如,假设 name 为 VARCHAR 类型,并且建立了索引:

复制代码
CREATE INDEX idx_name ON student(name);

执行:

复制代码
SELECT * FROM student
WHERE name = 123;

其中,name 是字符串类型,而 123 是数值类型。

MySQL 可能按照其类型转换规则执行比较,使得原有字符串索引难以直接用于准确的等值查找。

正确的写法应当是:

复制代码
SELECT * FROM student
WHERE name = '123';

建议:查询条件中的数据类型应尽量与字段定义的数据类型保持一致。

这样不仅有利于索引使用,也能够避免隐式转换带来的意外比较结果。

但是,并不是所有隐式类型转换都会导致索引无法使用,需要结合具体字段类型和执行计划判断。

6.4 LIKE 模糊匹配

LIKE 是 MySQL 中常见的模糊匹配操作。

对于建立了索引的字符串字段,不同的匹配方式可能产生不同的索引使用效果。

1. 前缀匹配

复制代码
SELECT * FROM student
WHERE name LIKE '张%';

由于字符串以 张 开头,因此对于常见的字符串 B+ 树索引,可以利用前缀范围进行查找。

2. 前导通配符匹配

复制代码
SELECT * FROM student
WHERE name LIKE '%三';

或者:

复制代码
SELECT * FROM student
WHERE name LIKE '%张%';

由于查询无法确定字符串的起始部分,因此通常不能直接利用普通 B+ 树索引定位一个有效的前缀查找范围。

不过,数据库仍然可能选择扫描某个索引,特别是在覆盖索引等场景下。

因此,不能简单理解为只要 LIKE 前面出现 %,数据库就一定执行全表扫描。

6.5 OR 条件与索引使用

使用 OR 连接多个条件时,索引的使用也可能受到影响。

例如:

复制代码
SELECT * FROM student
WHERE age = 20
OR major LIKE '%工程';

假设 age 建立了索引,但是 major LIKE '%工程' 无法有效利用普通 B+ 树索引进行前缀定位。

由于查询需要返回满足任意一个条件的记录,优化器可能认为全表扫描更加合适。

但是,OR 并不是索引失效的充分条件。

例如:

复制代码
SELECT * FROM student
WHERE age = 20
OR name = '张三';

如果 age 和 name 均存在可利用的索引,MySQL 就有可能采用 index_merge 等访问方式,合并多个索引的查询结果。

所以,对于 OR 查询,需要分析每个条件的索引可用性以及优化器选择的执行方案。

6.6 优化器选择全表扫描

即使查询条件满足索引使用规则,MySQL 也不一定选择索引。

例如:

复制代码
SELECT * FROM student
WHERE age >= 0;

假设表中几乎所有记录都满足该条件。

如果使用 age 二级索引,数据库可能需要扫描大量二级索引记录,并且进行大量回表操作。

而直接扫描聚簇索引中的全部记录,可能具有更低的整体执行成本。

因此,MySQL 优化器可能主动选择全表扫描。

这种情况并不意味着索引出现了问题,而是优化器基于查询成本作出的选择。

影响索引选择的因素包括:

  • 查询条件能够过滤多少数据。

  • 需要返回多少条记录。

  • 是否需要回表。

  • 索引和数据页面的访问成本。

  • 统计信息与数据分布情况。

七、索引分析与优化

前面已经学习了索引的基本结构、使用规则以及常见的索引失效场景。

但是,在实际开发过程中,仅仅依靠理论分析并不能完全确定 MySQL 是否使用了某个索引。

因此,我们还需要掌握一种重要的 SQL 分析工具:EXPLAIN。

7.1 使用 EXPLAIN 分析 SQL 执行计划

EXPLAIN 是 MySQL 提供的执行计划分析工具,可以帮助我们了解优化器计划如何执行一条 SQL 语句。

基本语法:

复制代码
EXPLAIN SELECT ...;

例如:

复制代码
EXPLAIN
SELECT * FROM student
WHERE age = 20;

执行之后,MySQL 会返回对应的执行计划信息。

需要重点关注以下字段:

字段 作用
id 查询执行计划中的标识
select_type 查询的类型
table 当前访问的数据表
type 数据访问方式
possible_keys 优化器认为可能使用的索引
key 实际被优化器选中的索引
key_len 执行计划中使用的索引键长度
ref 与索引进行比较的字段或常量
rows 预计需要检查的记录数量
filtered 预计通过条件过滤的比例
Extra 其他执行信息

其中,最值得关注的字段是 type、key、rows 和 Extra。

1. type:数据访问方式

常见的访问类型如下:

type 含义
const 通过主键或唯一索引等值查找,至多匹配一行
eq_ref 连接查询中通过唯一键每次至多匹配一行
ref 使用索引进行非唯一等值查找
range 使用索引进行范围扫描
index 扫描整棵索引
ALL 全表扫描

通常情况下,能够利用索引范围定位少量记录的访问方式,相比扫描大量数据具有更高的效率。

但是,不能仅根据 type 判断 SQL 的性能。

例如,type = index 虽然表示使用了索引,但也可能意味着需要扫描整个索引。

2. key:实际选择的索引

key 字段表示优化器为当前访问路径选择的索引。

例如:

复制代码
key: idx_age

表示执行计划选择使用 idx_age。

如果显示:

复制代码
key: NULL

则说明当前访问没有选择具体索引作为访问路径。

还需要注意,possible_keys 表示可能用于查找的索引,并不代表这些索引一定会被使用。

3. rows:预计扫描的记录数量

rows 表示优化器预计该访问操作需要检查的记录数量。

通常情况下,扫描的记录越少,执行成本可能越低。

但是,该字段只是估计值,并不保证与实际执行数量完全一致。

4. Extra:额外执行信息

Extra 字段用于显示其他与执行计划有关的信息。

常见内容如下:

Extra 含义
Using where 需要对记录应用查询过滤条件
Using index 可以直接通过索引获取所需数据
Using index condition 使用索引条件下推
Using filesort 需要额外排序
Using temporary 使用临时表处理部分查询操作

其中:

复制代码
Using index

通常意味着查询可以直接利用索引中已有的数据,避免额外读取完整行记录。

而:

复制代码
Using index condition

表示使用了索引下推优化,两者需要加以区分。

7.2 如何判断 SQL 是否有效利用索引

接下来,通过几个简单示例分析索引的使用情况。

假设数据表已经建立以下索引:

复制代码
CREATE INDEX idx_major_age_name
ON student(major, age, name);

场景一:等值查询

复制代码
EXPLAIN
SELECT * FROM student
WHERE major = '计算机'
AND age = 20;

分析时,重点观察:

  1. key 是否选择 idx_major_age_name。

  2. type 是否为 ref 等访问类型。

  3. rows 预计需要访问多少记录。

  4. 是否存在回表等额外访问成本。

场景二:范围查询

复制代码
EXPLAIN
SELECT * FROM student
WHERE major = '计算机'
AND age BETWEEN 18 AND 22;

由于条件包含联合索引的前导列,并且对 age 指定了查询范围,因此优化器可能选择 range 访问方式。

场景三:覆盖索引

复制代码
EXPLAIN
SELECT major, age, name
FROM student
WHERE major = '计算机';

由于查询需要的字段都存在于联合索引中,因此可以利用覆盖索引减少额外的回表操作。

如果优化器选择该索引,传统执行计划的 Extra 字段可能显示:

复制代码
Using index

场景四:无法有效利用索引范围

复制代码
EXPLAIN
SELECT * FROM student
WHERE age + 1 = 21;

由于对索引字段执行了表达式运算,因此普通 age 索引通常无法直接用于等值范围定位。

可以与下面的执行计划进行对比:

复制代码
EXPLAIN
SELECT * FROM student
WHERE age = 20;

通过对比 key、type 和 rows 等字段,可以分析 SQL 写法对索引使用的影响。

需要注意,不同数据量、统计信息以及 MySQL 版本可能产生不同执行计划,因此不应提前认定每个示例一定出现某种结果。

补充:EXPLAIN ANALYZE

普通 EXPLAIN 主要显示优化器生成的执行计划及估计信息。

在支持该功能的 MySQL 版本中,还可以使用:

复制代码
EXPLAIN ANALYZE
SELECT * FROM student
WHERE age = 20;

与普通 EXPLAIN 不同,EXPLAIN ANALYZE 会实际执行查询,并提供实际耗时、实际返回行数和循环次数等信息。

因此,在分析查询性能时,可以通过 EXPLAIN ANALYZE 对比优化器的估计结果与实际执行情况。

由于该语句会真正执行查询,应注意查询本身的执行开销。

7.3 索引设计的基本原则

合理的索引设计是提高数据库查询性能的重要手段。

但是,索引并不是建立得越多越好。过多的索引不仅会占用存储空间,还会增加数据写入和维护成本。

因此,在实际开发过程中,通常需要遵循以下原则。

1. 优先为常用查询条件建立索引

例如:

复制代码
SELECT * FROM student
WHERE age = 20;

如果业务中经常根据 age 查询数据,并且查询具有足够的过滤能力,就可以考虑建立索引:

复制代码
CREATE INDEX idx_age ON student(age);

但是,如果某个字段几乎不参与查询,那么通常没有必要单独为其建立索引。

2. 考虑字段的区分度

字段区分度通常反映不同字段值对记录的区分能力。

例如:

  • 身份编号通常具有较高的区分度。

  • 性别等取值种类较少的字段通常具有较低的区分度。

可以使用以下 SQL 观察某个字段不同值的比例:

复制代码
SELECT
    COUNT(DISTINCT age) / COUNT(*) AS ratio
FROM student;

该比例可以作为简单的参考指标,但它并不是优化器成本判断的完整依据。

通常,过滤能力较强的字段更适合作为索引候选列。

不过,字段区分度只是考虑因素之一,最终还需要结合实际查询需求。

3. 合理设计联合索引

假设经常执行以下查询:

复制代码
SELECT * FROM student
WHERE major = '计算机'
AND age = 20;

可以考虑建立联合索引:

复制代码
CREATE INDEX idx_major_age
ON student(major, age);

相较于分别建立两个单列索引,联合索引可以在符合使用规则的情况下,根据多个字段共同定位记录。

设计联合索引时,应当综合考虑:

  • 哪些字段经常一起出现在查询条件中。

  • 哪些字段用于等值查询。

  • 哪些字段用于范围查询。

  • 是否需要支持 ORDER BY 和 GROUP BY。

  • 是否能够通过覆盖索引减少回表。

联合索引的字段顺序应根据常见查询模式确定,而不是机械地按照字段区分度进行排列。

4. 避免建立冗余索引

例如,已经建立:

复制代码
CREATE INDEX idx_major_age
ON student(major, age);

如果再次建立:

复制代码
CREATE INDEX idx_major
ON student(major);

由于联合索引已经以 major 为前导列,因此在许多查询场景下,可以直接利用联合索引完成对 major 的查找。

这种情况下,单独的 idx_major 就可能是冗余索引。

但是,单列索引通常更小,某些查询可能因此获得更低的扫描成本,所以删除前仍然需要结合实际工作负载进行验证。

5. 尽量利用覆盖索引

例如:

复制代码
SELECT major, age
FROM student
WHERE major = '计算机';

如果已经建立:

复制代码
(major, age)

就有机会直接通过该索引获取查询所需字段,从而避免额外回表。

在实际开发中,应当尽量根据业务需要明确查询字段,减少不必要的完整行读取。

6. 不要建立过多索引

索引虽然可以提高查询效率,但是每次执行:

复制代码
INSERT INTO ...
UPDATE ...
DELETE FROM ...

都有可能涉及相关索引结构的维护。

例如,当更新某个索引字段时,MySQL 可能需要同步修改对应的索引记录。

因此,索引数量越多,数据更新时产生的额外维护成本也可能越高。

对于写入频繁的数据表,更应该控制不必要的索引数量。

相关推荐
泡海椒1 小时前
JQuick-SQL SELECT 投影:字段、别名与表达式查询
数据库·sql
猫咪很可爱1 小时前
mysqldump 失败常见场景
数据库·sql·mysql
大明二代2 小时前
从零构建 Mastra RAG MVP:DeepSeek、SiliconFlow 与 PostgreSQL/pgvector 实战
javascript·数据库·人工智能
LMT流氓兔2 小时前
第四单元——第17课:让回答边生成、边显示
服务器·数据库·人工智能·python·oracle·langchain
1024奇点2 小时前
【仓颉语言入门 · 第二篇 第 8 课】
开发语言·数据库·ide·mysql·开源
其实防守也摸鱼2 小时前
Linux文本处理命令理论学习
linux·服务器·前端·数据库·sql·学习·安全
l1t2 小时前
DuckDB 各版本 tpch 10 查询时间对比
数据库
ggaofeng3 小时前
linux中的绿色版内存mysql
linux·运维·mysql
Nebula_g3 小时前
SpringBoot企业级实战项目—云禾外卖—SpringBoot常用注解解析
java·网络·数据库·spring boot·后端·spring·javase