Tavio's blog Tavio's blog
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Tavio Zhang

努力学习的小码喽
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • MySQL InnoDB
  • MySQL缓冲池与WAL机制
  • MySQL索引底层
    • 一、索引的核心价值:为何需要索引?
    • 二、B+树:MySQL索引的底层数据结构基石
      • 2.1 先懂B树:B+树的“前身”与不足
      • 2.2 B+树的核心设计:针对性优化与优势
      • 2.3 B+树的查询流程(以聚簇索引为例)
    • 三、MySQL索引的核心类型:聚簇索引与二级索引
      • 3.1 聚簇索引:索引即数据,数据即索引
      • 3.2 二级索引:叶子节点存储主键
      • 3.3 联合索引:有序的多列索引
    • 四、索引失效的本质:为何索引“失效”?
    • 五、常见索引失效场景与案例分析
      • 场景1:索引列上使用函数或运算
      • 场景2:模糊查询以“%”开头
      • 场景3:联合索引不满足最左匹配原则
      • 场景4:使用OR连接非索引列
      • 场景5:索引列与查询条件的数据类型不匹配
      • 场景6:查询条件是IS NULL/IS NOT NULL(视数据分布而定)
      • 场景7:数据量过小,优化器选择全表扫描
      • 场景8:统计信息过时,优化器判断失误
    • 六、索引优化实战建议
      • 1. 优先使用聚簇索引(主键索引)
      • 2. 合理设计联合索引:遵循“最左匹配+高频列在前”
      • 3. 利用覆盖索引,避免回表
      • 4. 避免索引列上的函数/运算/类型转换
      • 5. 控制索引数量,平衡查询与写入效率
      • 6. 定期维护索引与统计信息
    • 七、总结
  • MySQL事务与MVCC
  • MySQL锁机制
  • MySQL Binlog
  • MySQL崩溃恢复
  • MySQL查询优化
  • 《MySQL》笔记
Tavio
2022-07-20
目录

MySQL索引底层

# 深度解析MySQL索引底层:B+树设计精髓与索引失效本质

在MySQL数据库的性能优化中,索引无疑是最核心的手段之一。一句合适的索引设计,能让慢查询从“秒级”提升到“毫秒级”;反之,即便建了索引,也可能因“索引失效”导致全表扫描,性能断崖式下降。而这一切的根源,都要回归到索引的底层数据结构——B+树,以及MySQL优化器的执行逻辑。

# 一、索引的核心价值:为何需要索引?

在没有索引的情况下,MySQL查询数据时只能进行“全表扫描”——即从表的第一条记录开始,逐行匹配查询条件,直到找到目标数据。这种方式在数据量小时(如几千条)尚可接受,但当数据量达到百万、千万级时,全表扫描的时间成本会呈线性增长(O(n)复杂度),完全无法满足业务需求。

索引的核心作用,就是将“全表扫描”的线性查找,转化为“有序结构查找”的对数级查找(O(log n)复杂度),本质是通过预先生成的“有序数据结构”,快速定位数据在磁盘上的物理存储位置,减少磁盘I/O操作(数据库性能瓶颈的核心是磁盘I/O)。

举个通俗的例子:全表扫描如同在一本没有目录的字典里找“苹果”,需要逐页翻阅;而索引就像字典的目录,通过目录能直接定位到“苹果”所在的页码,大幅提升查找效率。

# 二、B+树:MySQL索引的底层数据结构基石

MySQL的索引底层默认使用B+树(B-tree的改进版),而非数组、链表、哈希表等结构。这并非偶然,而是B+树的设计完美适配了MySQL的磁盘存储特性(数据以“页”为单位存储,默认16KB,磁盘I/O按页进行)。要理解B+树的优势,我们先从B树的不足入手,再看B+树的优化设计。

# 2.1 先懂B树:B+树的“前身”与不足

B树(平衡多路搜索树)的核心结构特点:

  • 每个节点可以存储多个关键字(索引值)和对应的指针(指向子节点的磁盘地址);
  • 关键字按升序排列,子节点指针穿插在关键字之间,确保左子树的关键字均小于当前关键字,右子树的关键字均大于当前关键字;
  • 所有叶子节点在同一层,保证查询效率稳定(平衡树特性)。

但B树存在两个致命缺陷,无法适配MySQL的高频查询场景:

  1. 查询效率不稳定:非叶子节点和叶子节点都存储数据,若查询的目标数据在非叶子节点,能快速命中;若在叶子节点,则需要遍历到最底层,查询次数差异较大;
  2. 范围查询低效:进行范围查询(如id>100 and id<200)时,需要遍历多个分支节点,找到起始值后,再回溯其他分支,操作复杂;
  3. 磁盘I/O次数多:非叶子节点存储数据占用空间,导致每个节点能存储的关键字数量减少,树的高度增加(比如百万级数据可能需要4-5层),而每一层的访问都需要一次磁盘I/O,I/O次数越多,查询越慢。

# 2.2 B+树的核心设计:针对性优化与优势

B+树在B树的基础上进行了三点关键优化,完美解决了B树的不足,成为MySQL索引的最优选择。其核心结构如下:

  • 非叶子节点仅存索引,不存数据:非叶子节点只存储“关键字(索引值)+ 子节点指针”,不存储具体的数据行。这样每个非叶子节点能容纳更多的关键字,树的高度大幅降低(百万级数据通常只需2-3层),减少磁盘I/O次数;
  • 所有数据集中在叶子节点:所有关键字对应的具体数据(或数据的磁盘地址)都存储在叶子节点,且叶子节点按关键字升序排列,保证查询效率稳定(无论查询哪个数据,都需要遍历到叶子节点,查询次数固定);
  • 叶子节点通过双向链表连接:所有叶子节点之间形成一个双向链表,相邻叶子节点可以快速访问。这使得范围查询时,只需找到起始叶子节点,然后通过链表遍历后续节点即可,效率极高。

补充:MySQL中B+树的“节点”对应磁盘上的“数据页”(16KB),非叶子节点的每个关键字+指针约占16字节(8字节关键字+8字节指针),因此一个非叶子节点可存储约16KB/16B=1024个关键字,对应1025个指针(子节点)。若树高为3,则总数据量可达10241024N(N为每个叶子节点存储的数据行数),足以支撑千万级数据的高效查询。

# 2.3 B+树的查询流程(以聚簇索引为例)

假设我们有一个user表,主键id为聚簇索引,B+树高度为3,查询SELECT * FROM user WHERE id=10086的流程如下:

  1. MySQL先访问B+树的根节点(磁盘I/O 1次),根节点存储关键字范围,通过二分查找确定id=10086对应的子节点指针;
  2. 根据子节点指针访问第二层节点(磁盘I/O 2次),再次通过二分查找确定目标叶子节点的指针;
  3. 访问叶子节点(磁盘I/O 3次),在叶子节点中找到id=10086对应的data域(存储完整数据行),返回结果。

整个过程仅需3次磁盘I/O,而全表扫描可能需要上百次甚至上千次,效率差距悬殊。

# 三、MySQL索引的核心类型:聚簇索引与二级索引

基于B+树,MySQL将索引分为两大类——聚簇索引(Clustered Index)和二级索引(Secondary Index,也叫辅助索引),两者的核心区别在于叶子节点存储的内容不同,这也是理解后续索引失效的关键。

# 3.1 聚簇索引:索引即数据,数据即索引

聚簇索引是MySQL的核心索引,其叶子节点直接存储“完整的数据行”,而非数据的地址。MySQL中,主键索引默认是聚簇索引;若表没有主键,则会选择唯一非空索引作为聚簇索引;若既没有主键也没有唯一非空索引,MySQL会自动生成一个隐藏的聚簇索引(基于行号的自增ID)。

聚簇索引的优势:

  • 查询效率最高:通过聚簇索引查询时,找到叶子节点即可获取完整数据,无需额外操作;
  • 范围查询高效:借助叶子节点的双向链表,范围查询(如id between 100 and 200)能快速遍历所有符合条件的数据行。

聚簇索引的不足:

  • 插入效率受影响:聚簇索引是有序的,插入新数据时需要找到对应的位置,若该位置的数据页已满,会触发“页分裂”(拆分数据页并重新排序),增加开销;
  • 不适合频繁更新的主键:主键更新会导致数据行在磁盘上的物理位置移动,同时所有二级索引都会失效(后续会讲),需要重新构建。

# 3.2 二级索引:叶子节点存储主键

二级索引是用户手动创建的索引(如CREATE INDEX idx_name ON user(name)),其叶子节点不存储完整数据行,而是存储“索引列的值 + 聚簇索引的关键字(即主键)”。

二级索引的查询流程(回表机制): 以查询SELECT * FROM user WHERE name='张三'(name列有二级索引)为例:

  1. 先通过二级索引(idx_name)的B+树查找,找到name='张三'对应的主键值(如id=10086)——这一步叫“索引查找”;
  2. 再通过聚簇索引(主键索引)的B+树查找id=10086,在叶子节点获取完整的数据行——这一步叫“回表”。

补充:若查询的列都在二级索引中(如SELECT id, name FROM user WHERE name='张三'),则无需回表,直接从二级索引的叶子节点获取数据即可,这就是“覆盖索引”(Covering Index),是优化查询的重要手段。

# 3.3 联合索引:有序的多列索引

联合索引是多个列组合而成的二级索引(如CREATE INDEX idx_age_name ON user(age, name)),其B+树的关键字按“第一列→第二列→...→第N列”的顺序排序。联合索引的核心遵循“最左匹配原则”——即查询时必须从第一列开始匹配,否则无法利用索引。

例如,联合索引(age, name)的有效查询场景:

  • age=25(匹配第一列,有效);
  • age=25 and name='张三'(匹配第一列+第二列,有效);
  • age between 20 and 30 and name='张三'(范围匹配第一列,再匹配第二列,有效)。

无效场景:

  • name='张三'(未匹配第一列,无效);
  • age>25 and name='张三'(范围匹配第一列后,第二列的索引失效,仅第一列有效)。

# 四、索引失效的本质:为何索引“失效”?

很多开发者会遇到“建了索引但查询还是慢”的问题,这本质是“索引失效”——即MySQL的查询优化器判断“走索引的成本高于全表扫描”,因此放弃使用索引,转而进行全表扫描。

核心结论:索引失效的本质,是查询语句的执行计划无法利用B+树的有序性,或优化器认为走索引的效率低于全表扫描。具体来说,有两个核心原因:

  1. 破坏B+树的有序性:B+树的查询依赖于关键字的有序性,若查询条件对索引列进行了“无序化操作”(如函数、运算、类型转换),则优化器无法通过B+树的有序结构快速定位数据,只能放弃索引;
  2. 走索引的成本过高:即使查询条件适配索引,但如果符合条件的数据量过大(如占全表数据的30%以上),优化器会认为“走索引需要多次回表(二级索引)+ 磁盘I/O”,成本高于全表扫描,因此选择放弃索引。

补充:MySQL的查询优化器是基于“统计信息”(如索引的选择性、数据分布)来判断是否走索引的,若统计信息过时(如数据大量插入/删除后未更新统计信息),也可能导致优化器做出错误判断,出现索引失效。

# 五、常见索引失效场景与案例分析

结合上述本质原因,我们梳理出8种最常见的索引失效场景,每个场景都给出具体案例、失效原因和优化方案,让大家能直接落地避坑。

# 场景1:索引列上使用函数或运算

案例:user表有二级索引idx_create_time(create_time),查询“2024年1月1日之后创建的用户”:

-- 失效SQL
SELECT * FROM user WHERE DATE(create_time) > '2024-01-01';

-- 有效SQL
SELECT * FROM user WHERE create_time > STR_TO_DATE('2024-01-01', '%Y-%m-%d');
1
2
3
4
5

失效原因:在索引列create_time上使用了DATE()函数,破坏了B+树的有序性(索引列的原始值是有序的,但函数处理后的值无法保证有序),优化器无法通过索引快速定位数据,只能全表扫描。

优化思路:将函数操作从索引列上移到查询条件的右侧,让索引列保持原始的有序性,确保优化器能利用索引。

# 场景2:模糊查询以“%”开头

案例:user表有二级索引idx_name(name),查询“名字以‘三’结尾的用户”:

-- 失效SQL
SELECT * FROM user WHERE name LIKE '%三';

-- 有效SQL(以%结尾,有效)
SELECT * FROM user WHERE name LIKE '张%';

-- 有效SQL(覆盖索引,即使以%开头也有效)
SELECT id, name FROM user WHERE name LIKE '%三';
1
2
3
4
5
6
7
8

失效原因:模糊查询以“%”开头时,查询条件是“后缀匹配”,而B+树的索引是按前缀有序排列的,无法通过前缀快速定位后缀匹配的数据,因此索引失效。若以“%”结尾(前缀匹配),则能利用索引的有序性;若查询的列是覆盖索引(仅id和name),则无需回表,优化器会选择走索引(扫描所有叶子节点,但比全表扫描快)。

优化思路:避免以“%”开头的模糊查询;若必须使用,可考虑使用覆盖索引,或通过全文索引(FULLTEXT INDEX)替代(适用于字符串长度较长的场景)。

# 场景3:联合索引不满足最左匹配原则

案例:user表有联合索引idx_age_name(age, name),查询“名字为张三的用户”:

-- 失效SQL
SELECT * FROM user WHERE name='张三';

-- 有效SQL(匹配最左列age)
SELECT * FROM user WHERE age=25;

-- 有效SQL(匹配最左列+第二列)
SELECT * FROM user WHERE age=25 AND name='张三';

-- 有效SQL(最左列范围匹配,第二列仍有效)
SELECT * FROM user WHERE age>25 AND age<30 AND name='张三';

-- 失效SQL(最左列范围匹配后,第二列失效)
SELECT * FROM user WHERE age>25 AND name='张三';
1
2
3
4
5
6
7
8
9
10
11
12
13
14

失效原因:联合索引的B+树是按“age→name”的顺序排序的,查询时必须从最左列(age)开始匹配,否则无法利用索引的有序性。若最左列使用范围查询(>、<、between),则范围后的列无法利用索引(因为范围查询后的结果是无序的,第二列的索引失去意义)。

优化思路:创建联合索引时,将查询频率最高的列放在最左;查询时确保从最左列开始匹配;若需要单独查询name,可单独创建idx_name索引(权衡索引数量与查询效率)。

# 场景4:使用OR连接非索引列

案例:user表有索引idx_age(age),无idx_address(address),查询“年龄25或地址为北京的用户”:

-- 失效SQL
SELECT * FROM user WHERE age=25 OR address='北京';

-- 有效SQL(两个列都有索引)
SELECT * FROM user WHERE age=25 OR address='北京'; -- 前提:address有索引

-- 有效SQL(拆分为UNION)
SELECT * FROM user WHERE age=25
UNION
SELECT * FROM user WHERE address='北京';
1
2
3
4
5
6
7
8
9
10

失效原因:OR连接的两个条件中,若有一个列没有索引,优化器无法通过索引快速定位所有符合条件的数据(需要同时扫描索引和全表),因此会选择全表扫描。若两个列都有索引,优化器会分别扫描两个索引,再合并结果;若拆分为UNION,可分别利用索引和全表扫描,效率可能更高。

优化思路:避免用OR连接非索引列;若必须使用,可给非索引列添加索引,或拆分为UNION查询。

# 场景5:索引列与查询条件的数据类型不匹配

案例:user表有索引idx_phone(phone,varchar类型),查询“手机号为13800138000的用户”:

-- 失效SQL(查询条件是数字,索引列是字符串)
SELECT * FROM user WHERE phone=13800138000;

-- 有效SQL(查询条件是字符串,与索引列类型一致)
SELECT * FROM user WHERE phone='13800138000';
1
2
3
4
5

失效原因:查询条件的数据类型(数字)与索引列类型(字符串)不匹配时,MySQL会自动进行类型转换(将字符串转为数字),这相当于在索引列上进行了函数操作,破坏了B+树的有序性,导致索引失效。

优化思路:确保查询条件的数据类型与索引列类型完全一致,避免自动类型转换。

# 场景6:查询条件是IS NULL/IS NOT NULL(视数据分布而定)

案例:user表有索引idx_email(email),查询“邮箱为空/不为空的用户”:

-- 可能失效SQL
SELECT * FROM user WHERE email IS NULL;

-- 可能失效SQL
SELECT * FROM user WHERE email IS NOT NULL;
1
2
3
4
5

失效原因:B+树的索引会存储NULL值(NULL按最小排序),理论上IS NULL/IS NOT NULL可以走索引,但实际是否失效取决于数据分布:若NULL值占比极低(如1%),优化器会走索引;若NULL值占比极高(如50%以上),走索引需要扫描大量叶子节点,成本高于全表扫描,优化器会选择全表扫描。

优化思路:尽量避免查询大量NULL值的数据;若必须查询,可通过统计信息判断是否走索引,或强制使用索引(FORCE INDEX,但不推荐,可能适得其反)。

# 场景7:数据量过小,优化器选择全表扫描

案例:user表有索引idx_age(age),但表中仅100条数据,查询“年龄25的用户”:

-- 可能失效SQL
SELECT * FROM user WHERE age=25;
1
2

失效原因:当表中数据量过小时,全表扫描的成本(遍历100条数据)可能低于走索引的成本(3次磁盘I/O+回表),因此优化器会主动放弃索引,选择全表扫描。这是优化器的“智能判断”,并非索引本身的问题。

优化思路:无需优化,数据量小时全表扫描效率更高;若需要测试索引是否有效,可通过EXPLAIN查看执行计划,或强制使用索引(FORCE INDEX(idx_age))。

# 场景8:统计信息过时,优化器判断失误

案例:user表有索引idx_age(age),近期批量插入了100万条数据(age=25的占比从1%变为50%),查询“年龄25的用户”:

-- 可能失效SQL
SELECT * FROM user WHERE age=25;
1
2

失效原因:MySQL的优化器依赖“统计信息”(如索引的选择性、数据分布)来判断是否走索引。若数据大量插入/删除后,统计信息未及时更新,优化器会基于旧的统计信息(如age=25占比1%)判断走索引有效,但实际数据占比50%,走索引的成本高于全表扫描,导致索引失效。

优化思路:手动更新统计信息(ANALYZE TABLE user;),让优化器获取最新的数据分析结果;MySQL 8.0默认会自动更新统计信息,可通过SHOW VARIABLES LIKE 'innodb_stats_auto_recalc';查看配置。

# 六、索引优化实战建议

结合前面的底层原理和失效场景,给出6条可直接落地的索引优化建议,帮助大家避坑并提升查询效率:

# 1. 优先使用聚簇索引(主键索引)

聚簇索引的查询效率最高,尽量基于主键进行查询;设计主键时,选择自增ID(INT/BIGINT),避免使用字符串或频繁更新的字段(减少页分裂和二级索引失效的成本)。

# 2. 合理设计联合索引:遵循“最左匹配+高频列在前”

创建联合索引时,将查询频率最高的列放在最左;避免创建冗余的联合索引(如已有(idx_age, name),则无需再创建(idx_age));联合索引的列数不宜过多(一般3-4列即可,列数过多会增加索引维护成本)。

# 3. 利用覆盖索引,避免回表

查询时,尽量只查询需要的列,若查询的列都在索引中(覆盖索引),则无需回表,大幅提升效率。例如,查询SELECT id, name FROM user WHERE name='张三'(idx_name是二级索引,包含id和name),无需回表。

# 4. 避免索引列上的函数/运算/类型转换

确保查询条件中的索引列是原始值,不进行任何函数操作(如DATE()、UPPER())、运算(如age+1=26)或类型转换(字符串 vs 数字),避免破坏B+树的有序性。

# 5. 控制索引数量,平衡查询与写入效率

索引能提升查询效率,但会降低插入/更新/删除的效率(每次写入都需要维护索引的B+树)。一般来说,一张表的索引数量不宜超过5个,优先保留高频查询的索引。

# 6. 定期维护索引与统计信息

定期查看慢查询日志(slow_query_log),找出索引失效的查询并优化;定期更新统计信息(ANALYZE TABLE);对于长期未使用的索引(通过sys.schema_unused_indexes查看),及时删除,减少维护成本。

# 七、总结

MySQL索引的底层是B+树,其设计精髓在于“非叶子节点存索引、叶子节点存数据+双向链表”,完美适配磁盘I/O特性,实现高效的单点查询和范围查询。索引失效的本质是“查询语句破坏了B+树的有序性,或优化器判断走索引成本高于全表扫描”。

要想用好索引,需记住三点核心:一是理解聚簇索引与二级索引的区别,二是避开常见的索引失效场景,三是结合业务场景合理设计索引(如联合索引的最左匹配、覆盖索引的利用)。只有从底层原理出发,才能真正掌握索引优化的精髓,而不是死记硬背失效场景。

编辑 (opens new window)
#MySQL索引底层
上次更新: 2026/01/21, 19:29:14
MySQL缓冲池与WAL机制
MySQL事务与MVCC

← MySQL缓冲池与WAL机制 MySQL事务与MVCC→

最近更新
01
订单超时取消
01-21
02
双 Token 登录
01-21
03
长短链接跳转
01-21
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式