索引是 MySQL 优化查询性能的核心手段,其设计与使用直接决定数据库的响应效率,本质是一种帮助数据库高效查询,检索数据的数据结构。
本文将围绕 InnoDB 引擎,结合时下高频问题,按照基础概念→底层原理→核心机制→设计优化→问题排查→引擎对比的逻辑,系统梳理索引知识,兼顾理论与实践。
一、基础概念
1. 索引的定义和核心作用
- 定义: 索引是存储在磁盘上的与数据表关联的数据结构,通过提前排序 + 快速查找算法,减少查询时的磁盘 I/O 次数,显著加快检索速度。
- 核心作用:
- 加快 SELECT 查询速度(如单表查询、联表查询,排序/分组查询);
- 约束数据完整性和唯一性(主键索引,唯一索引);
- 优化排序/分组(避免全表排序,利用索引有序性)。
- 代价:
- 空间代价:索引需要额外存储(通常占数据表大小的10%~30%);
- 时间代价:写操作(INSERT/UPDATE/DELETE)需要同步维护索引,降低写效率。
2. 索引的分类
1. 按物理存储方式分类
聚簇索引(Clustered Index):
- 索引和数据存储在一起,索引的叶子节点就是数据行(InnoDB核心);
- 每张表仅一个聚簇索引(默认是主键索引,无主键时选择唯一非空索引,否则自动生成隐藏主键);
- 查询效率最高(可以直接从叶子节点拿到数据,无需回表查询);
非聚簇索引(Non-clustered Index):
- 索引和数据分离,索引的叶子节点就是索引和主键值,查询数据一般还需要回表查询(覆盖索引除外);
- 每张表可有多个非聚簇索引(普通索引,联合索引等);
聚簇索引vs 非聚簇索引
| 特性 | 聚簇索引(主键索引) | 非聚簇索引(普通索引) |
|---|---|---|
| 数据存储位置 | 叶子节点存储完整表数据 | 叶子节点存储主键值(而非完整数据) |
| 数量限制 | 一张表仅能有 1 个(InnoDB 强制) | 一张表可创建多个 |
| 查询效率 | 无需回表,查询速度最快 | 需通过主键回表查询完整数据(除非覆盖索引) |
| 索引键 | 必须是主键(无主键时自动选唯一非空列,否则生成隐藏主键) | 可是任意字段或字段组合 |
2. 按功能逻辑分类
主键索引( PRIMARY KEY ):
聚簇索引,唯一且非空,加速主键查询;
唯一索引( UNIQUE ):
非聚簇索引,值唯一(允许为NULL,最多一个),避免重复数据;
普通索引( INDEX ):
非聚簇索引,无约束,仅加速查询;
联合索引( INDEX(col1,col2,…) ):
多列组合的非聚簇索引,遵循最左前缀原则;
前缀索引( INDEX(col(N)) ):
对字符串了列的前 N 个字符建索引,节省空间;
空间索引( SPATIAL ):
针对「空间数据」设计的专用索引,核心作用是高效处理空间关系查询(如 “附近的商家”“某区域内的 POI”),
二、底层原理
InnoDB 选择 B+ 树作为核心索引结构,并非偶然,而是 B+ 树的设计完美匹配了 InnoDB 的核心诉求:适配磁盘存储特性、支持高频查询场景、平衡查询与维护效率、兼容事务与并发控制。
1. 层高极低,大幅减少磁盘 I/O 次数
B+ 树是多路平衡查找树,且有一个关键设计,非叶子节点只存储索引键 + 子节点指针,不存储完整数据行,单个非叶子节点可以存储更多的索引键,树的层高被极低压低,千万级的数据层高仅仅只有3-4层。
2. 叶子节点有序连续:
所有叶子节点按索引键排序,通过双向链表连接,高效的支持范围查询(between),排序(order by),分页(limit)。
聚簇索引叶子节点存储完整数据行,非聚集索引叶子节点存储索引键 + 主键值,回表逻辑统一,保证数据完整性。
3. 节点大小与磁盘页对齐,最大化利用磁盘预读
InnoDB的页大小与 B+ 树的节点大小强制对齐,每个节点就是一个磁盘页。
当读取一个 B+ 树节点时,操作系统会自动预读后续连续的几个磁盘页,而 B+ 树的节点是连续有序的,预读的节点恰好很可能是后续查询用到的数据,大幅减少 I/O 次数。
4. 读写效率稳定,维护成本低
B+ 树是平衡树,插入、删除数据时,会通过旋转、分裂、合并节点保持树的平衡,避免出现倾斜。此外,数据行存储在叶子节点,非叶子节点仅存索引键,读写时对非叶子节点的修改更少,进一步减少维护开销。
5. 兼容事务和并发控制
B+ 树天然适配锁机制(行锁,间隙锁):
- 叶子节点连续有序,便于实现间隙锁;
- 非叶子节点只存储索引键,锁的粒度实现的更细,比如行锁。
6. 为什么不选其他数据结构(反向验证)
- 不选 B 树:非叶子节点存数据,预读效率低
- 不选哈希表:仅支持等值查询,不支持范围 / 排序
- 不选红黑树(二叉树):层级太深,磁盘 I/O 爆炸
- 不选跳表:磁盘上维护成本高
三、核心机制
1. 聚簇索引vs非聚簇索引:查询差异
| 场景 | 聚簇索引(主键) | 非聚簇索引(普通索引) |
|---|---|---|
| 单值查询 | 1 次B+ 树遍历,无需回表 | 2 次B+ 树遍历,先查询非聚簇索引,再回表查询聚簇索引 |
| 范围查询 | 叶子节点链表直接遍历 | 先查非聚簇索引拿到主键列表,再批量回表(MRR 优化) |
| 排序查询 | 直接利用叶子节点有序性 | 若排序列是索引列,无需额外排序;否则需 filesort |
2. 联合索引:最左前缀原则
联合索引(也叫复合索引、组合索引)是指在数据库表的多个列上共同创建的索引,而非仅针对单个列。其底层依然是 B+ 树结构,但排序逻辑是 “按索引列的定义顺序逐层排序”—— 先按第一列排序,第一列值相同的行再按第二列排序,以此类推。核心价值是高效支持 “多列组合过滤、排序、分组” 的查询场景。
联合索引的查询效率完全依赖 最左前缀匹配:即查询条件必须从索引的第一列(最左列)开始,依次匹配后续列,才能高效利用索引。若跳过前面的列,索引将无法被有效使用(除非触发索引跳跃扫描,但也有严格条件)。列顺序不影响 “前缀匹配”。
以 idx(a,b,c) 为例,不同查询条件的匹配情况:
| 查询条件(WHERE 子句) | 是否命中联合索引 | 命中的索引部分 | 说明 |
|---|---|---|---|
| a=10 | 是 | a | 匹配最左第一列,触发索引扫描 |
| a=10 AND b=20 | 是 | a→b | 匹配前两列,索引扫描范围进一步缩小 |
| b=20 AND a=10 | 是 | a→b | 匹配前两列,索引扫描范围进一步缩小 |
| a=10 AND b=20 AND c=30 | 是 | a→b→c | 完全匹配所有列,扫描范围最小(效率最高) |
| b=20 AND c=30 | 否(低版本) | 无 | 跳过最左列 a,无法匹配索引排序逻辑 |
| a=10 AND c=30 | 部分命中 | a | 仅匹配第一列 a,c 列条件无法利用索引 |
| a>10 AND b=20 | 部分命中 | a | a 是范围查询,后续列 b 无法利用索引 |
3. 回表查询
当查询使用非聚簇索引时,若查询的字段超出索引包含的列(即索引无法覆盖所有查询字段),数据库需先通过非聚簇索引找到主键值,再通过聚簇索引(主键索引)查询完整数据,这个 “通过主键查完整数据” 的过程就是回表。
示例:
-- 表结构:id(主键)、a、b
-- 索引:idx_a(a)
select id, b from test_table where a = 10;
- 第一步:通过 idx_a 找到 a=10 对应的主键值(如 1001);
- 第二步:通过主键 1001 查聚簇索引,获取 b 字段,完成回表。
那如何减少回表呢,核心思路:让查询字段被索引覆盖,避免触发回表。
4. 索引优化机制
1. 覆盖索引(Covering Index)
所查询的字段(select 列、where 条件列、order by 列)都包含在索引列中,无需回表,直接从索引获取所有所需数据。
优势:避免回表,减少 IO,查询效率大幅提升(尤其适用于频繁查询的高频字段组合)
示例:
-- 表结构:id(主键)、a、b
-- 索引:idx_a_b(a, b)
select id, b from test_table where a = 10 and b = 18;
因 b 在索引中,属于索引覆盖,数据可以从索引获取,无需回表。
2. 索引下推(ICP, Index Condition Pushdwon)
MySQL5.6+ 特性,在使用非聚簇索引查询时,将部分where条件(索引列)下推到存储引擎层,在索引扫描阶段直接过滤不符合条件的数据行,减少回表次数和数据传输量。
无 ICP 时,存储引擎仅返回符合 “最左前缀原则” 的索引项,由 MySQL 服务器层过滤其他条件;
有 ICP 时,存储引擎在索引扫描时直接过滤 “索引列相关条件”,仅返回符合所有条件的索引项,再视情况回表查询。
适用场景:非聚簇索引的范围查询(like、between)、多条件过滤,且条件列均在索引中。
示例:
-- 表结构:id(主键)、a、b、c
-- 索引:idx_a_b(a, b)
select id, a, c from test_table where a =10 and b > 18;
- 无 ICP:存储引擎返回所有 a =10 的索引项(可能包含 b ≤ 18 的数据),服务器层过滤 b > 18,再回表查询c;
- 有 ICP:存储引擎在索引扫描时,直接过滤 a =10 and b > 18,仅返回符合条件的索引项,减少回表次数。
3. 索引合并(Index Merge)
索引合并是 MySQL 针对单表查询的一种优化策略,当查询条件涉及多个独立索引(非联合索引)时,优化器会将多个索引的查询结果合并(交集/并集),而非仅使用一个索引或全表扫描。
本质是索引筛选 + 集合运算 + 回表取数的三步流程,利用索引的高效筛选能力和内存中集合运算的快速性,替代低效的全表扫描或单个索引扫描。
无需创建复杂的联合索引,就能适配多列任意组合的过滤条件,减少全表扫描的概率;但它并非 “万能优化”,是否触发取决于优化器的成本评估,且效率受索引筛选性、合并数量、回表开销的影响。
示例:
-- 表结构:id(主键)、a、b、c
-- 索引:idx_a(a),idx_b(b)
select id, a, b from test_table where a = 10 and b > 18;
- 无索引合并:优化器仅选择其中一个索引(如 idx_a),查询出 a = 10 的数据后,再过滤 b > 18 的记录;
- 有索引合并:优化器同时使用 idx_a 和 idx_b,分别查询 a = 10 和 b > 18 的数据,然后取两者的交集(条件是 and),直接得到结果。
支持的合并类型
- 交集合并(AND):条件用 and 连接,需满足所有条件(如 a=1 and b=2),EXPLAIN 中 Extra 显示 Using intersect(idx_a,idx_b);
- 并集合并(OR):条件用 or 连接,满足任一条件(如 a=1 or b=2);EXPLAIN 中 Extra 显示 Using union(idx_a,idx_b)
- 排序合并(Sort-Union):当 or 连接的索引列有序时,合并后无需额外排序。(a=1 OR b>2),EXPLAIN 中 Extra 显示 Using sort_union(idx_a,idx_b)
4. 前缀索引
前缀索引是对字符串字段(CHAR、VARCHAR、TEXT)的前 N 个字符建立索引,而非整个字段。
其核心目的是:在保证索引区分度足够的前提下,大幅减少索引占用的磁盘空间,提升索引扫描和维护效率。
主要用于字符串列长度较长时,字符串前N个字符有足够的区分度。
5. 索引跳跃扫描(Index Skip Scan)
MySQL 8.0+ 新增特性,针对联合索引(复合索引)的优化技术,当前导列(最左列)区分度极低(如性别,仅 2 个值)时,优化器可跳过前导列,直接扫描后续列的索引,实现 “非最左前缀” 的索引匹配。
其本质是将联合索引的前缀列视为 “分组标识”,把整个联合索引拆分成多个 “虚拟的单列索引”(按前缀列不同值分组),再在每个虚拟索引中扫描非前缀列的条件,最终合并结果。
示例:
联合索引 idx(a,b) 的结构(有序):
(a=1,b=10) → (a=1,b=20) → (a=1,b=30) → (a=2,b=15) → (a=2,b=20) → (a=3,b=5) → (a=3,b=20)
查询:SELECT * FROM t WHERE b=20;
索引跳跃扫描流程:
1. 提取 a 的不同值:{1,2,3}(3 个分组);
2. 分组 1(a=1):扫描 b 列,快速找到 (a=1,b=20) → 收集 ID;
3. 分组 2(a=2):扫描 b 列,快速找到 (a=2,b=20) → 收集 ID;
4. 分组 3(a=3):扫描 b 列,快速找到 (a=3,b=20) → 收集 ID;
5. 合并 3 个分组的 ID → 回表取完整数据 → 返回结果。
6. 多范围读取(MRR, Multi-Range Read)
优化非聚簇索引的回表逻辑,将分散的主键按顺序排序后批量回表,减少磁盘随机 I/O;
适用于范围查询(如 WHERE a BETWEEN 1 AND 100)或 IN 查询(WHERE a IN (1,3,5));
将随机 I/O 转为顺序 I/O,提升回表效率(EXPLAIN 中 Extra 显示 Using MRR)。
7. 自适应哈希索引(AHI, Adaptive Hash Index)
InnoDB 自动为高频访问的索引页建立哈希索引,将 B + 树的 O (log n) 查找优化为 O (1);
自动维护(无需手动配置),仅对等值查询有效,不占用额外磁盘空间(存储在内存中)。
四、索引设计与优化
1. 设计索引的原则
索引设计需平衡 “查询效率” 和 “写入 / 存储成本”,核心原则如下:
- 优先针对高频查询字段建立索引
针对 where、order by、group by、join on 中的字段建索引,优先覆盖高频查询场景。 - 联合索引遵循最左前缀原则和字段顺序,
- 高频查询字段放左侧(如 where a=? and b=?,建 idx_a_b 而非 idx_b_a);
- 区分度高的字段放左侧(如手机号、ID 放前,性别、状态放后);
- 排序 / 分组字段放右侧(如 where a=? order by b,建 idx_a_b)。
- 控制索引数量
索引越多,写入(insert/update/delete)速度越慢(需维护索引树),且占用更多存储空间,建议单表索引数量≤5 个。 - 避免冗余索引
冗余索引会增加维护成本,如已建 idx_a_b,无需再建 idx_a(idx_a_b 可复用前缀)。 - 长字符串使用前缀索引
避免对完整长字符串建索引,节省空间。 - 避免过度使用覆盖索引
索引覆盖虽好,但联合索引列过多会导致索引体积增大,反而降低查询效率,按需选择覆盖列。
2. 用了索引还是很慢?可能的原因
- 索引失效
- 索引列参与函数操作(如 WHERE DATE(create_time) = ‘2025-11-08’);
- 索引列发生隐式转换(如 WHERE id = ‘100’,id 是 INT 类型,字符串转数字);
- 模糊查询以 % 开头(如 WHERE a LIKE ‘%张三’);
- 使用 OR 连接非索引列(如 WHERE a = 1 OR b = 2,b 无索引);
- 联合索引不满足最左前缀原则;
- 优化器判断全表扫描更快(如小表、索引选择性极低,如性别列)。
- …
- 索引设计不合理
- 索引区分度低:如对 gender(男/女)、status(0/1)等低基数列建索引;
- 索引冗余/过多:示例:建了 idx(a,b) 又建 idx(a)(冗余),或单表建了 5+ 个索引;
- 未用覆盖索引,导致大量回表:如非聚集索引 idx(name),查询 SELECT * FROM user WHERE name=‘张三’;
- 查询语句不合理
- SELECT * 导致不必要的回表;
- 连接(join)/子查询效率低;
- LIMIT 偏移量过大
- 数据特征导致开销大
- 数据倾斜严重;
- 大字段 / 大表问题;
- 表频繁插入 / 删除,导致索引碎片过多。
五、问题排查
1. 排查工具
(1)EXPLAIN(执行计划分析)
- 查看 SQL 是否命中索引、索引类型、查询方式等;
- 关键字段解读,
- type:索引使用类型(从优到差:const > eq_ref > ref > range > index > ALL);
- key:实际使用的索引(NULL 表示未命中索引);
- rows:预计扫描的行数(越小越好);
- Extra:额外信息(Using index:覆盖索引;Using filesort:需额外排序;Using temporary:需临时表;Using where:过滤条件)。
(2)慢查询日志(Slow Query Log)
- 作用:记录执行时间超过 long_query_time(默认 10 秒,可改为 1 秒)的 SQL;
- 配置:
SET GLOBAL slow_query_log = ON; -- 开启
SET GLOBAL long_query_time = 1; -- 阈值 1 秒
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log'; -- 日志路径
- 分析工具:mysqldumpslow(自带)、pt-query-digest(Percona Toolkit,更强大)。
(3)sys schema 视图(快速定位问题)
- sys.schema_unused_indexes:查看未使用的索引(可删除);
- sys.schema_redundant_indexes:查看冗余索引(可删除);
- sys.statements_with_full_table_scans:查看全表扫描的 SQL。
(4)SHOW PROFILE
作用:查看 SQL 执行的详细过程(CPU、I/O、锁等待等);
示例:
SET profiling = ON; -- 开启
SELECT * FROM t WHERE a = 'jh'; -- 执行 SQL
SHOW PROFILE FOR QUERY 1; -- 查看第 1 条 SQL 的执行详情
2. 常见问题及排查流程
问题 1:查询慢(全表扫描)
排查步骤:
- 用 EXPLAIN 查看 type 是否为 ALL,key 是否为 NULL;
- 检查是否存在索引失效原因(函数操作、隐式转换等);
- 若未建索引,按设计原则添加索引;
- 若索引失效,优化 SQL(如避免函数操作)。
问题 2:索引存在但未命中
排查步骤:
- 用 EXPLAIN 确认 key 为 NULL;
- 检查 SQL 是否违反最左前缀原则、是否有隐式转换等;
- 检查索引区分度是否过低(如性别列),优化器选择全表扫描;
解决方案:
调整 SQL 写法、重建索引(如前缀索引长度)、强制使用索引(FORCE INDEX,谨慎使用)。
问题 3:写操作慢(INSERT/UPDATE/DELETE)
排查步骤:
- 用 sys.schema_unused_indexes 查看是否有冗余 / 未使用索引;
- 检查是否有大索引(如联合索引列过多、前缀索引过长);
解决方案:
删除冗余索引、优化索引结构(如缩短前缀长度)、批量执行写操作(避免单条频繁写入)。
问题 4:排序/分组慢
排查步骤:
- 用 EXPLAIN 查看 Extra 是否有 Using filesort/Using temporary;
- 检查排序 / 分组列是否与索引列一致;
解决方案:
调整索引结构(如联合索引包含排序列)、避免 SELECT *(用覆盖索引)。
六、引擎对比
MySQL 中索引的实现依赖存储引擎,核心对比 InnoDB(默认)和 MyISAM(已过时):
| 对比维度 | InnoDB | MyISAM |
|---|---|---|
| 索引类型 | 聚簇索引 + 非聚簇索引 | 仅非聚簇索引(所有索引叶子节点存物理地址) |
| 主键索引 | 聚簇索引,必须存在(无则自动生成) | 普通非聚簇索引,与其他索引无区别 |
| 索引存储 | 聚簇索引:叶子节点存数据;非聚簇索引:叶子节点存主键 | 所有索引:叶子节点存数据物理地址 |
| 回表操作 | 非聚簇索引需回表(覆盖索引除外) | 无需回表(直接通过物理地址定位数据) |
| 事务支持 | 支持 ACID 事务、MVCC | 不支持事务 |
| 锁粒度 | 行级锁(并发性能好) | 表级锁(并发性能差) |
| 索引效率 | 读:聚簇索引查询快;写:因事务 / 行锁略慢 | 读:非聚簇索引无需回表,简单查询快;写:表锁导致慢 |
| 全文索引 | 5.6+ 支持,支持中文(需分词器) | 早期支持,仅支持英文,无中文分词 |
| 崩溃恢复 | 支持(有 redo/undo log) | 不支持(崩溃后需修复表) |
| 适用场景 | 互联网业务(高并发、需事务、读写混合) | 只读业务(如博客、静态数据)、小表 |
其他引擎
Memory:内存引擎,索引为哈希索引(默认)或 B + 树索引,适合临时表、高频访问的小表(数据易丢失);
CSV:无索引,仅适合存储 CSV 格式数据(如数据导入导出)。
总结
MySQL 索引的核心是以空间换时间,优化的关键在于:
理解 B + 树的底层逻辑,掌握聚簇索引与非聚簇索引的查询差异;
按高区分度、高频查询、最左前缀原则设计索引,避免冗余和失效;
用 EXPLAIN、慢查询日志等工具定位问题,针对性优化 SQL 或索引;
首选默认引擎InnoDB,仅特殊场景用其他合适引擎。
MySQL索引原理与实战优化

被折叠的 条评论
为什么被折叠?



