MySQL(十二):聚簇索引与索引优化

目录

[一、回顾 B+ 树索引](#一、回顾 B+ 树索引)

[1. B+ 树索引体系回顾](#1. B+ 树索引体系回顾)

二、聚簇索引与非聚簇索引

[1. 聚簇索引](#1. 聚簇索引)

[2. 非聚簇索引](#2. 非聚簇索引)

[3. 查找流程与回表查询](#3. 查找流程与回表查询)

[三、MyISAM 与 InnoDB 索引实现](#三、MyISAM 与 InnoDB 索引实现)

[1. MyISAM 的 B+ 树实现](#1. MyISAM 的 B+ 树实现)

[2. InnoDB 的 B+ 树实现](#2. InnoDB 的 B+ 树实现)

四、索引操作

[1. 查看索引](#1. 查看索引)

[2. 创建主键索引](#2. 创建主键索引)

[3. 创建唯一索引](#3. 创建唯一索引)

[4. 创建普通索引](#4. 创建普通索引)

[5. 创建全文索引](#5. 创建全文索引)

[6. 删除索引](#6. 删除索引)

[五、Explain 执行计划](#五、Explain 执行计划)

[1. Explain 的定义与基本使用](#1. Explain 的定义与基本使用)

[2. 控制字段解剖](#2. 控制字段解剖)

[3. 判定是否命中索引](#3. 判定是否命中索引)

六、联合索引与最左匹配原则

[1. 联合索引的底层 B+ 树结构](#1. 联合索引的底层 B+ 树结构)

[2. 最左匹配原则](#2. 最左匹配原则)

[七、 覆盖索引](#七、 覆盖索引)

[1. 什么是覆盖索引](#1. 什么是覆盖索引)

八、索引创建原则

[1. 哪些字段适合建立索引](#1. 哪些字段适合建立索引)

[2. 哪些字段不适合建立索引](#2. 哪些字段不适合建立索引)

总结


一、回顾 B+ 树索引

在理解了 B+ 树的底层物理 page 结构与多级目录路由机制后,我们已经掌握了数据库高效检索的理论。然而,在实际的生产工程中,仅仅了解 B+ 树的抽象模型是不够的

不同的存储引擎(如 InnoDB 与 MyISAM)对 B+ 树的物理实现有着本质的差异;而在复杂的业务场景下,如何设计多列索引、如何避免索引失效、如何通过执行计划分析慢查询,才是决定系统性能的关键


1. B+ 树索引体系回顾

为了承上启下,我们首先归纳 B+ 树索引能够将检索效率提升数万倍的底层核心物理逻辑:

  1. 全局有序性:无论是叶子节点的数据行,还是非叶子节点的目录项,在页面内部通过逻辑链表、在页面之间通过双向链表,均严格维持主键键值的升序排列。这奠定了二分查找的基础

  2. 非叶子节点扇出度:索引页不存放业务行数据,只存放密集的目录项(主键 + 页号),使得单个 16KB 页面可映射上千个子页面

  3. 低矮拓扑结构:高扇出度直接将千万级数据的 B+ 树高度压制在 3 层左右。这意味着定位任意随机记录,其物理磁盘 I/O 次数被严格锁定在 2~3 次以内,实现了 O(log N) 的对数级时间复杂度

二、聚簇索引与非聚簇索引

在 MySQL 中,根据叶子节点存储内容的物理组织形式 ,索引被划分为聚簇索引(Clustered Index)非聚簇索引(Secondary Index,亦称二级索引或辅助索引)


1. 聚簇索引

定义与特性

聚簇索引并不是一种单独的索引类型,而是一种数据存储方式 。它的核心特征是:B+ 树的叶子节点完整存放了整张表的实际行记录数据 。也就是说,索引结构与物理数据文件是交织在一起的

在 InnoDB 存储引擎中,聚簇索引的构建遵循以下原则:

  • 如果表定义了主键,InnoDB 会自动选择该主键作为聚簇索引

  • 如果未定义主键,InnoDB 会在表中检索第一个定义的 UNIQUE 且 NOT NULL 的列作为聚簇索引

  • 如果以上条件均不满足,InnoDB 会在底层自动生成一个 6 字节的隐式自增列来构建聚簇索引

**架构原则:**正因为数据行只能按照一种顺序存储在磁盘上,所以一张 MySQL 表有且只能有一个聚簇索引


2. 非聚簇索引

定义与特性

当我们在业务中对非主键列建立索引时,生成的全都是二级索引。其物理特征与聚簇索引截然不同:二级索引的叶子节点并不包含实际的数据行,而是仅包含当前索引列的键值以及对应的聚簇索引键值(即主键值值)


3. 查找流程与回表查询

基于上述物理拓扑,我们拆解两条不同查询语句的底层执行路径:

路径 A:基于主键的查询

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

执行逻辑:引擎直接检索主键聚簇索引的 B+ 树,当下探到叶子节点时,直接在该物理页内获取到整行数据。整个过程只需遍历一棵树

路径 B:基于二级索引的查询

sql 复制代码
SELECT * FROM user WHERE name = '悟空';

执行逻辑

  1. 引擎首先扫描 name 列对应的二级索引 B+ 树,在其叶子节点中查找到 name = '悟空' 的记录,并提取出其关联的主键值 id = 2

  2. 拿到主键 id = 2 后,引擎必须转而跳转到主键聚簇索引的 B+ 树,重新执行一次自上而下的下探检索

  3. 在聚簇索引的叶子节点中抓取到完整的物理行记录并返回

什么是回表查询(Lookup)

上述路径 B 中,先通过二级索引找到主键值,再用主键值去聚簇索引中检索完整行记录的过程,在数据库工程中被称为回表查询

回表查询意味着系统需要先后遍历两棵独立的 B+ 树,这会引发额外的页面加载与磁盘随机 I/O 开销,是导致二级索引查询性能劣化的主要诱因

三、MyISAM 与 InnoDB 索引实现

不同的存储引擎对上述索引模型的实现有着本质差别。我们重点对比 MySQL 最核心的两种引擎:InnoDBMyISAM


1. MyISAM 的 B+ 树实现

MyISAM 引擎采用的是非聚簇索引架构,其物理文件和索引文件是完全分离的:

  • .MYD 文件:专门存放数据行,数据按照插入顺序追加写入,行与行之间属于堆表关系

  • .MYI 文件:专门存放索引树结构

在 MyISAM 中,主键索引与二级索引在结构上没有任何本质区别 。它们的叶子节点内部存储的都不是完整的行数据,而是指向 .MYD 文件中该行记录的物理文件偏移量指针。 即使通过主键查询,MyISAM 也需要先在 .MYI 文件中找到指针,再到 .MYD 文件中读取数据,其物理本质全部属于非聚簇架构


2. InnoDB 的 B+ 树实现

InnoDB 引擎采用的是索引组织表架构,数据文件 .ibd 本身就是一棵巨大的聚簇索引树

为什么 InnoDB 坚持选择聚簇索引?

相比 MyISAM 的指针回表,InnoDB 选择聚簇索引具备压倒性的优势:

  1. 主键查询性能极致化:在企业级高并发读写中,基于主键的单表/多表关联查询占比极高。聚簇索引使主键查询免去了读取物理数据文件的二次指针寻址,数据局部性极强

  2. 辅助缓存效率:由于数据与主键紧凑排列在 16KB 页内,当该页被加载进 Buffer Pool 后,大量相邻行记录同时常驻内存。这使得范围查询能够触发高效的内存连续顺序访问,彻底规避了机械磁盘的随机寻道

两种存储引擎索引对比

InnoDB 存储引擎 MyISAM 存储引擎
主架构类型 索引组织表 堆表(Heap Table)
物理文件构成 .ibd(结构、索引与数据一体化) .frm(表结构)、.MYD(数据)、.MYI(索引)
主键叶子节点内容 存储完整的物理业务行记录 存储物理文件的偏移量指针
二级索引叶子节点内容 存储索引键值 + 关联的主键值 存储索引键值 + 物理文件的偏移量指针
主键查询性能 极高(一步到位,无需二次物理寻址) 较高(需解析索引,再根据指针跨文件读取)
物理行移动(如页分裂)的影响 二级索引无需修改(因为绑定的是主键值) 所有索引均需重写指针(因为物理偏移量改变)

四、索引操作

在理解了物理存储差异后,我们需要在数据库定义语言(DDL)层面熟练掌握索引的操作方法。在实际开发中,正确的创建和删除规范能够最大程度减少对线上生产业务的冲击


1. 查看索引

在对数据表进行结构调整或查询优化前,必须首先探查其现有的索引拓扑结构

sql 复制代码
SHOW INDEX FROM table_name;

输出解析

执行该命令后,结果集会输出以下关键控制字段:

  • Table:当前查询的数据表名称

  • Non_unique:若为 0,代表该索引是唯一索引或主键索引;若为 1,代表该索引是允许键值重复的普通二级索引

  • Key_name:索引的名称。主键索引固定显示为 PRIMARY

  • Column_name:当前索引项所绑定的数据列名称

  • Seq_in_index:该列在索引中的序列号。在单列索引中固定为 1;在多列联合索引中,按照定义的先后顺序从 1 开始递增

  • Cardinality基数。估计该索引列中包含多少个不同(Unique)的值。该数值通常由存储引擎通过采样算法计算得出。基数越高,代表该列的数据区分度越好,MySQL 优化器也越倾向于选择该索引


2. 创建主键索引

主键索引在物理层直接决定了 InnoDB 聚簇索引的组织结构

方式一:在创建表时直接指定

sql 复制代码
CREATE TABLE sys_user (
    user_id BIGINT AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL,
    PRIMARY KEY (user_id) -- 显式声明主键
) ENGINE=InnoDB;

方式二:通过修改表结构追加

sql 复制代码
ALTER TABLE sys_user ADD PRIMARY KEY (user_id);

注意:对于已经存放有海量数据的表,执行 ALTER TABLE 会触发全表重组与数据页分裂,导致长时间锁表,应绝对避免在业务高峰期操作


3. 创建唯一索引

唯一索引(Unique Index)要求索引列的所有值必须唯一,但允许存在 NULL 值,且一个表中可以存在多个 NULL 值

语法格式

sql 复制代码
-- 方式一:直接创建
CREATE UNIQUE INDEX idx_uniq_email ON sys_user (email);

-- 方式二:修改表结构追加
ALTER TABLE sys_user ADD UNIQUE INDEX idx_uniq_email (email);

4. 创建普通索引

普通二级索引没有任何唯一性约束,其唯一使命是加速对指定列的条件检索

语法格式

sql 复制代码
-- 方式一:直接创建
CREATE INDEX idx_username ON sys_user (username);

-- 方式二:修改表结构追加
ALTER TABLE sys_user ADD INDEX idx_username (username);

5. 创建全文索引

全文索引主要用于解决形如 LIKE '%keyword%' 这种前后双百分号模糊查询导致的索引失效问题。它的底层不再是标准的 B+ 树,而是倒排索引结构

语法格式

sql 复制代码
-- 修改表结构追加全文索引(假定对文章内容字段建立)
ALTER TABLE sys_article ADD FULLTEXT INDEX idx_full_content (content) 
    WITH PARSER ngram;

注意:在涉及中文全文检索时,建议附加 WITH PARSER ngram 插件,以便 MySQL 能够正确地对中文字符串进行分词切片


6. 删除索引

当某个索引不再被业务查询命中(成为冗余索引),或者需要对其进行结构重建时,应予以删除以释放磁盘空间并降低数据变更时的索引维护开销

语法格式

sql 复制代码
-- 方式一:标准的 DROP INDEX 语法
DROP INDEX idx_username ON sys_user;

-- 方式二:通过 ALTER TABLE 语法删除
ALTER TABLE sys_user DROP INDEX idx_username;

-- 方式三:专门用于删除主键索引(无需指定索引名)
ALTER TABLE sys_user DROP PRIMARY KEY;

五、Explain 执行计划

在实际开发中,编写出 SQL 语句只是第一步。由于 MySQL 内部存在一个复杂的组件------优化器,它会根据各索引列的基数统计信息,自主计算并决定最终采用何种路径去检索数据

为了观察优化器的物理选择,避免盲目猜测性能,必须借助 MySQL 提供的官方诊断工具------EXPLAIN(执行计划)


1. Explain 的定义与基本使用

什么是 Explain

Explain 是用于分析查询语句执行效率的关键字。通过在 SELECT、DELETE、INSERT 或 UPDATE 语句的最前端附加 EXPLAIN,MySQL 不会真正运行该业务语句,而是会直接输出该语句在底层的执行方案拓扑图

基本语法

sql 复制代码
EXPLAIN SELECT * FROM sys_user WHERE user_id = 1;

2. 控制字段解剖

执行 EXPLAIN 后,MySQL 会返回一张包含多个字段的结构矩阵。在索引优化领域,以下 4 个字段具有决定性的诊断价值

type 字段(访问类型)

type 字段直接反应了 MySQL 是以何种物理手段在表中查找数据的。它是判定查询性能好坏的最核心指标。其取值从优到劣的经典排序如下:

type 值 物理含义与底层行为 性能评级
system 表中只有一行记录(系统表特例)。通常不需要触发磁盘 I/O 极致
const 通过主键索引或唯一索引进行等值匹配。由于键值唯一,优化器直接将其转化为常量处理,查找仅需下探一次 B+ 树 优秀
eq_ref 在进行多表连接(JOIN)时,驱动表的关联字段正好是被驱动表的主键或唯一索引,对于驱动表的每一行,被驱动表只有一行匹配 优秀
ref 通过非唯一性的二级索引进行等值匹配。可能会匹配到多条满足条件的有序记录 良好
range 检索二级索引树的某个指定范围。常见于使用了 >, <, BETWEEN, IN, LIKE 的查询 合格
index 全索引扫描。MySQL 遍历了整棵二级索引树,虽然避免了读取数据文件,但依然属于 O (N) 遍历(此处 N 为索引树的节点总数) 较差
ALL 全表扫描。不触发任何索引,直接从磁盘首字节开始遍历 .ibd 数据文件。时间复杂度为 O (N) 灾难

注意:在生产环境中,线上业务的 SQL 查询其 type 字段至少应达到 range 级别,在高并发核心链路上则必须争取达到 ref 或 const 级别。如果出现 ALL 或 index,通常意味着系统存在重大性能隐患

key 字段与 possible_keys 字段

  • possible_keys:显示在此次查询中,有哪些索引可能被选用。它反映的是候选索引池

  • key :显示 MySQL 优化器在经过成本计算后,最终实际决定使用的索引名称。若该字段为 NULL,则代表本次查询没有命中任何索引

rows 字段

  • 含义 :MySQL 根据统计信息预估出来的、为了找到目标数据所必须读取并检查的行数

  • 调优:rows 的数值越小,意味着执行引擎需要扫描的物理 Page 越少,磁盘 I/O 损耗也就越低。该字段是衡量 SQL 优化前后效果最直观的量化尺

Extra 字段(附加状态信息)

Extra 字段包含了不适合在其他列中展示、但对判定性能至关重要的追加内部状态。以下三类是最常见的输出结果:

  • Using index极优 。表明查询所需要的全部字段,直接在一棵二级索引树上就能够全部获取,完全不需要执行回表查询

  • Using where:表明 MySQL 在从存储引擎层获取到数据后,还需要在 Server 二次过滤

  • Using filesort高危。表明 MySQL 无法利用索引自带的有序性完成 ORDER BY 的排序操作,必须在内存或磁盘中启用专用的排序缓冲区进行二次物理排序

  • Using temporary高危。表明 MySQL 在处理 GROUP BY 或 DISTINCT 时,由于无法利用索引分组,不得不创建一张内部的临时表来辅助去重聚合,极其消耗内存与 CPU


3. 判定是否命中索引

在实际调优时,不能孤立地看某个字段,而是应当遵循以下逻辑来断定索引的健康状态:

六、联合索引与最左匹配原则

在实际业务开发中,复杂的查询往往需要依赖多个筛选条件(例如:WHERE age = 18 AND status = 1)。如果为每一个列单独建立单列索引,MySQL 优化器通常只能选择其中一个区分度最高的索引,而另一个条件则不得不依赖 Server 层的二次过滤,性能提升有限

为了应对多条件组合检索,数据库引入了联合索引 。联合索引并不是简单地将多个单列索引叠加,它在底层有着严密的复合排序逻辑,并直接推导出了关系型数据库中至关重要的最左匹配原则


1. 联合索引的底层 B+ 树结构

联合索引在底层的物理本质依然是一棵 B+ 树,其核心差异在于非叶子节点的目录项和叶子节点的数据项,其键值是由多个列的值复合而成的元组

假设我们对一张表建立了一个联合索引:INDEX idx_age_status (age, status)。 底层 B+ 树在构建时,会遵循全局严格的字典序(Lexicographical Order)进行排列:

  1. 首列全局有序:所有记录首先严格按照 age 列的值从小到大进行全局排序

  2. 次列局部有序:只有当 age 列的值完全相同时,内部记录才会按照 status 列的值从小到大进行局部排序

  3. 次列全局无序:如果脱离了 age 列的依赖,孤立地观察整棵树中的 status 列,其数值表现为:1, 0, 2, 1, 0,在全局维度是彻底无序的


2. 最左匹配原则

基于联合索引 "首列全局有序,次列局部有序" 的物理特性,MySQL 优化器在解析 SQL 时会严格遵循最左匹配原则:查询条件必须从索引的最左列开始,并且不能跳过中间的列

案例分析(以联合索引 (a, b, c) 为例)

查询语句示例 索引命中 底层行为
WHERE a = 1 AND b = 2 AND c = 3 全额命中 完美契合字典序。引擎利用 a 快速收敛范围,再利用 b 和 c 在局部有序空间内进行对数级下探
WHERE b = 2 AND c = 3 完全失效 未命中索引。由于查询跳过了最左列 a,而在全局视角下 b 和 c 是杂乱无序的,B+ 树无法利用二分查找定位,被迫退化为全表扫描
WHERE a = 1 AND c = 3 部分命中 仅 a 列命中索引。引擎可以通过 a = 1 快速定位到目标页面,但由于中间断开了 b 列,在 a = 1 的局部空间内 c 是无序的,因此 c 条件无法用于树形下探,只能由 Server 层进行逐行过滤
WHERE a > 1 AND b = 2 部分命中 仅 a 列命中范围检索。一旦最左列 a 触发了范围查询(如 >, <, BETWEEN),其右侧的所有列(如 b)将彻底丧失索引定位能力。因为在 a> 1 的巨大跨度下,多个不同 a 值所夹杂的 b 值在全局上是无序的

七、 覆盖索引

我们已经清楚了 "回表查询" 的概念:当通过二级索引查找到主键值后,需要二次折返到聚簇索引中抓取整行数据。回表操作会引发大量的磁盘随机 I/O,是拖慢查询效率的核心瓶颈

为了斩断回表的链路,数据库设计者提出了覆盖索引(Covering Index)的设计思想


1. 什么是覆盖索引

覆盖索引并不是一种新的物理索引结构,而是一种利用现有二级索引树实现零回表的查询应用模式

如果一个查询语句中,SELECT 子句所需要获取的全部列,全部包含在当前所使用的二级索引树中 ,那么执行引擎在完成二级索引的下探后,就已经拿到了所需的全部业务数据。此时,系统会直接将结果集返回给客户端,不再触发任何回表操作

sql 复制代码
[ 二级索引树 (username) ] 
     |
     v 下探到叶子节点
 [ 键值: "张三" | 主键ID: 99 ]  ===> (由于只需获取 username 和 id) ===> 立即返回结果 (零回表)

八、索引创建原则

索引虽然能够大幅度提升检索速度,但它在物理上是有代价的:

  • 存储代价:索引本身需要占用磁盘空间(.ibd 文件会成倍膨胀)

  • 计算代价:每当对表执行 INSERT、UPDATE、DELETE 时,InnoDB 为了维持 B+ 树的全局有序性,必须实时执行复杂的页分裂、页合并以及链表指针重组,这会直接拉低写性能

因此,索引的设计必须克制且精准,遵循以下工业级创建原则


1. 哪些字段适合建立索引

  1. 高频出现在 WHERE 条件、JOIN 关联字段、ORDER BY 或 GROUP BY 中的列

  2. 列的基数极高、数据区分度好的字段

    • 区分度计算公式:区分度 = COUNT(DISTINCT column) / COUNT(*)

    • 区分度越接近 1(例如手机号、身份证号、唯一准考证号),索引的过滤效率越高;如果区分度极低(例如性别列,无论数据量多大都只有 "男/女" 两个值),建立索引不仅无法提速,反而会加剧系统回表负担

  3. 长字符串列建议采用前缀索引

    • 如果对一个 VARCHAR(255) 的长文本建立全额索引,会导致二级索引的目录项体积过大,降低 16KB 页面的扇出度,推高树高。建议仅提取前缀字符构建索引:ALTER TABLE table ADD INDEX idx_title (title(20))

2. 哪些字段不适合建立索引

  1. 数据频繁变更、读写比严重的字段(例如账户余额、验证码时效计数器)。高频的 B+ 树物理重组会导致严重的磁盘写入瓶颈

  2. 参与无序追加或高重复度的字段(如上述的性别、状态机标志位)

  3. 不要在非空字符比例极低的字段上盲目建索引

总结

综上所述,我们从索引的使用方式出发,深入学习了聚簇索引、非聚簇索引、联合索引以及覆盖索引等核心概念,并结合 EXPLAIN 执行计划分析了索引对查询效率的影响

通过本篇内容可以发现,索引本质上是一种以空间换时间的策略。合理地设计索引,不仅能够显著降低查询的数据访问量,还能够避免全表扫描,从而大幅提升数据库的查询性能。但与此同时,索引并非越多越好,不合理的索引设计同样会增加维护成本,影响数据的插入、更新与删除效率

至此,我们已经理解了 MySQL 为什么能够实现高效查询。不过,仅仅查询得快还不足以支撑一个真正的数据库系统。在多用户同时访问数据库的场景下,还需要保证数据操作的正确性、一致性以及可靠性

因此,在下一篇中,我们将正式进入 MySQL 事务专题,深入理解事务的本质、ACID 特性以及数据库如何保证并发环境下数据的一致性