MySQL 索引超详解:从底层 B+ 树到面试高频考点

MySQL 索引超详解:从底层 B+ 树到面试高频考点

一篇搞懂 MySQL 索引的底层原理、数据结构、InnoDB 实现、最左匹配、索引失效场景,以及面试中必问的高频问题。

适合人群:备战后端面试、想搞懂索引为什么能加速的同学。


写在前面

在 MySQL 中,访问表数据只有两种方式:

|----------|---------------|----------------|
| 方式 | 原理 | 代价 |
| 顺序访问 | 全表扫描,从头到尾逐行匹配 | 数据量大时 O (n),极慢 |
| 索引访问 | 先查索引树,再定位到行记录 | 一般 O (log n),快 |

索引的本质,是一棵排好序的、存储了列值与行指针对应关系的数据结构。它以空间换时间,让数据库不用全表扫描就能定位到目标行。

可以把索引比作新华字典的音序表:要查 "库" 字,如果不用音序,得从字典第 400 页逐页翻;但如果先查音序表(10 多页),几秒钟就定位到了。索引就是这个 "音序表"。


一、索引到底用什么数据结构?

这是面试中最爱问的一道题:为什么 MySQL 索引用 B+ 树,而不是二叉树、红黑树、B 树、哈希?

1.1 为什么不用二叉搜索树 / 红黑树?

  • 二叉树高度 = log₂(n)。一张表哪怕只有 1000 万行,树高 ≈ 24 层。
  • 每查一层就要读一次磁盘 I/O(索引在磁盘上,不在内存)。
  • 树越高 = 磁盘 I/O 次数越多 = 越慢。
  • 红黑树只是限制了左右子树高度差,本质还是二叉,高度降不下来。

1.2 为什么不用哈希?

  • 哈希等值查询 O (1),极快。
  • 但哈希不支持范围查询、排序、模糊匹配、最左匹配。
  • Memory 引擎用哈希,InnoDB 内部虽然有 "自适应哈希索引",但那是数据库自动建的,DBA 控制不了。
  • 所以 InnoDB 主索引结构不选哈希。

1.3 为什么用 B+ 树,而不是 B 树?

B 树和 B+ 树都是多叉平衡查找树,目的是把树 "压矮",减少磁盘 I/O 次数。两者区别如下:

|-----------|----------------------|-----------------------------|
| 对比项 | B 树 | B+ 树 |
| 非叶子节点存数据吗 | 存(键值 + 数据 / 指针) | 不存,只存键值 + 子节点指针 |
| 叶子节点存什么 | 数据 | 数据(且叶子节点之间用双向链表相连) |
| 范围查询 | 要中序遍历、回溯父节点 | 直接沿叶子链表扫,极快 |
| 查询稳定性 | 数据可能在非叶子节点命中,查询次数不稳定 | 数据只在叶子节点,每次查询路径长度相同 |
| 单节点能存多少键 | 少(因为还要存数据) | 多(只存键,页大小固定时 fanout 更大) |

1.4 一组数字让你记住 B+ 树的强大

InnoDB 一个数据页默认 16KB。

  • B+ 树非叶子节点只存键值(比如 bigint 8 字节 + 页指针 6 字节 ≈ 14 字节),一个 16KB 的页能存约 1170 个键。
  • 假设树有 3 层:
    • 第 1 层 1 个节点 → 1170 个指针;
    • 第 2 层 1170 个节点 → 1170 × 1170 ≈ 137 万指针;
    • 第 3 层再 × 1170 ≈ 约 20 亿行。

也就是说:3 层 B+ 树就能索引 20 亿行数据,查任意一条记录最多 3 次磁盘 I/O。

这就是 MySQL 选择 B+ 树的根本原因。


二、InnoDB 索引的物理结构

2.1 页(Page)------ InnoDB 磁盘管理的最小单位

  • 默认大小 16KB。
  • 表数据是按 "页" 为单位读入内存的,不是按行读。
  • 一个页里放 N 行记录,行记录之间用单向链表连起来;页与页之间用双向链表连起来。

2.2 聚簇索引(Clustered Index)

InnoDB 的主键索引就是聚簇索引,叶子节点直接存整行数据。

复制代码

聚簇索引(按主键 id 组织)

┌─────────────────────────────┐

│ 非叶子节点:只存主键值 + 子页指针 │

└─────────────────────────────┘

↓

┌─────────────────────────────┐

│ 叶子节点:存【完整的行记录】 │

│ (id, name, age, all columns) │

└─────────────────────────────┘

特点:

  • 一张表只能有一个聚簇索引(因为数据只能按一种顺序物理存储)。
  • 如果没建主键,InnoDB 会:
    1. 优先选一个非空唯一索引当聚簇索引;
    1. 都没有?自动生成一个 6 字节的隐藏列 rowid 当聚簇索引。
  • 聚簇索引的叶子节点就是数据本身,所以通过主键查不需要回表。

2.3 二级索引 / 辅助索引(Secondary Index)

除主键外的索引都叫二级索引。叶子节点存的是 "索引列值 + 主键值",不是整行数据。

复制代码

二级索引(按 name 列组织)

┌─────────────────────────────┐

│ 非叶子节点:name 值 + 子页指针 │

└─────────────────────────────┘

↓

┌─────────────────────────────┐

│ 叶子节点:(name, 主键 id) │

└─────────────────────────────┘

2.4 回表(Bookmark Lookup)------ 高频面试题

如果你用二级索引 name 查 WHERE name = '张三':

  1. 先在 name 的 B+ 树叶子节点拿到 name='张三' 对应的主键 id = 100;
  1. 再拿着 id 回到主键聚簇索引的 B+ 树里查一遍,拿到完整行记录。

这个 "先查二级索引拿主键,再查主键索引拿全行" 的过程,就叫回表。

回表多了一次 B+ 树查找,代价不小。

2.5 覆盖索引(Covering Index)------ 解决回表

如果查询要的列,全部都在二级索引的叶子节点里,就不需要回表。

举例:

复制代码

-- 索引:idx_name_age(name, age)

SELECT name, age FROM user WHERE name = '张三';

二级索引叶子节点存的就是 (name, age, id),要查的 name、age 全在叶子节点里,直接返回,不用回表。这就叫覆盖索引。

用 EXPLAIN 看,Extra 列会显示 Using index:

复制代码

EXPLAIN SELECT name, age FROM user WHERE name = '张三';

-- Extra: Using index ✅ 覆盖索引

2.6 索引下推(Index Condition Pushdown, ICP)------ MySQL 5.6 引入

再举个联合索引 (name, age) 的例子:

复制代码

SELECT * FROM user WHERE name LIKE '张%' AND age = 20;

没有 ICP 时:

  1. 在 name 索引树里找到所有 name LIKE '张%' 的记录(可能有 1000 条);
  1. 对这 1000 条全部回表,拿到全行后再判断 age = 20。

有 ICP 时:

  1. 在 name 索引树里找 name LIKE '张%' 的同时,顺便用索引里已经存的 age 列先过滤一遍;
  1. 只对满足 age = 20 的记录回表。

效果:把回表次数从 1000 次降到 10 次。

EXPLAIN 里 Extra 显示 Using index condition。


三、索引的分类

3.1 普通索引(INDEX)

最基本的索引,没有任何约束:

复制代码

CREATE INDEX idx_name ON user(name);

3.2 唯一索引(UNIQUE)

索引列的值必须唯一,但允许 NULL:

复制代码

CREATE UNIQUE INDEX uk_phone ON user(phone);

3.3 主键索引(PRIMARY KEY)

聚簇索引,值唯一且不允许 NULL,一张表只能有一个:

复制代码

CREATE TABLE user (

id BIGINT PRIMARY KEY,

name VARCHAR(50)

);

3.4 联合索引(Composite Index)

对多个列一起建索引,遵循最左匹配原则(后面细讲):

复制代码

CREATE INDEX idx_name_age ON user(name, age);

3.5 全文索引(FULLTEXT)

用于大文本的分词检索:

复制代码

CREATE FULLTEXT INDEX ft_content ON article(content);

-- 使用:

SELECT * FROM article WHERE MATCH(content) AGAINST('数据库');

实际生产一般用 Elasticsearch,不用 MySQL 全文索引。

3.6 前缀索引

对长文本列的前 N 个字符建索引,节省空间:

复制代码

CREATE INDEX idx_email_prefix ON user(email(10));

注意:前缀索引不能用作覆盖索引,也不能用于 ORDER BY / GROUP BY。


四、联合索引与最左匹配原则(重中之重)

联合索引 (a, b, c) 在 B+ 树里是先按 a 排序,a 相同再按 b 排序,b 相同再按 c 排序。

就像字典:先按拼音首字母排序,首字母相同再按第二个字母排,以此类推。

4.1 哪些查询能命中联合索引?

假设有索引 idx_abc(a, b, c):

|----------------------------------|--------|------------------------------------------|
| 查询条件 | 能否命中索引 | 命中哪几列 |
| WHERE a = 1 | ✅ | a |
| WHERE a = 1 AND b = 2 | ✅ | a, b |
| WHERE a = 1 AND b = 2 AND c = 3 | ✅ | a, b, c |
| WHERE b = 2 | ❌ | 不走索引(没从最左列 a 开始) |
| WHERE b = 2 AND c = 3 | ❌ | 不走索引 |
| WHERE a = 1 AND c = 3 | ⚠️ | 只命中 a,c 用不上(中间断了 b) |
| WHERE a = 1 AND b > 2 AND c = 3 | ⚠️ | 命中 a, b;c 用不上(b 是范围查询后,后面的列无法再用索引定位) |
| WHERE a = 1 AND b LIKE '张%' | ✅ | 命中 a, b(前缀匹配) |
| WHERE a = 1 AND b LIKE '%张' | ⚠️ | 只命中 a(前置模糊匹配 b 用不上) |

4.2 关键规则总结

  1. 最左列必须出现,否则索引失效。
  1. 中间不能断,断了后面的列用不上。
  1. 遇到范围查询(>、<、BETWEEN、LIKE 'x%')之后的列,索引失效(但范围列本身能用)。
  1. LIKE 前导 % 失效,LIKE 'abc%' 可以,LIKE '%abc' 不行。
  1. MySQL 优化器会自动调整 WHERE 条件顺序,不用你手动按索引顺序写,它会自己找最左列。

五、索引失效的常见场景(面试必背清单)

以下这些写法会导致索引失效,导致全表扫描:

|-------------------|-----------------------------------------|--------------------------------------------------------------------|
| 场景 | 失效写法 | 正确写法 |
| 对索引列做函数运算 | WHERE DATE(create_time) = '2026-11-02' | WHERE create_time >= '2026-11-02' AND create_time < '2026-11-03' |
| 对索引列做算术运算 | WHERE age + 1 = 20 | WHERE age = 19 |
| 隐式类型转换 | 字段是 varchar,写 WHERE phone = 13800001111 | WHERE phone = '13800001111' |
| 使用 OR 且 OR 一侧无索引 | WHERE a = 1 OR b = 2(b 无索引) | 给 b 建索引,或改用 UNION |
| NOT / != / NOT IN | WHERE age != 20 | 尽量改范围查询 |
| 前导模糊查询 | WHERE name LIKE '%张' | WHERE name LIKE '张%',或用全文索引 |
| 联合索引违反最左匹配 | 见上文 | 按最左列写 |
| 优化器估算全表扫描更快 | 表数据量小 / 索引区分度太低 | 这是优化器主动放弃索引,不算 "失效",但要知道原因 |

⚠️ 特别注意隐式类型转换:比如 phone varchar 你写 WHERE phone = 13800001111,MySQL 会把 phone 转成数字再比较,相当于对索引列做了函数操作,索引直接失效。


六、索引的优缺点

优点

  1. 大幅减少扫描行数(降低 EXPLAIN 中的 rows)。
  1. 加速排序(ORDER BY)、分组(GROUP BY)、连接(JOIN)。
  1. 实现唯一性约束。

缺点

  1. 占空间:每个索引都是一棵 B+ 树,索引越多磁盘占用越大。
  1. 降低写性能:INSERT / UPDATE / DELETE 时,不仅要改数据页,还要同步维护所有相关索引树。
  1. 优化器选错索引的风险:索引多了,优化器可能选错,反而变慢。

七、索引设计原则(面试加分项)

  1. 优先给 WHERE、JOIN、ORDER BY、GROUP BY 里出现的列建索引。
  1. 区分度低的列不要单独建索引(比如性别,只有 0/1/2,建了也白建,优化器不会用)。
    • 区分度 = 唯一值个数 / 总行数,越接近 1 越好。
  1. 优先建联合索引,少建单列索引,充分利用最左匹配和覆盖索引。
  1. 扩展索引,不要新建索引:已有 (a, b),要加 (a, b, c) 就把 (a,b) 扩成 (a,b,c),而不是新建一个。
  1. 索引列尽量用小数据类型:int 比 bigint 省空间,char (N) 比 varchar 定长,B+ 树一个页能放更多键,fanout 更大,树更矮。
  1. 字符串太长用前缀索引。
  1. 避免过度索引:一张表索引建议不超过 5~6 个。
  1. 主键建议用自增 id:
    • 自增主键是顺序插入,B+ 树页不会频繁分裂;
    • 如果用 UUID 当主键,插入是随机的,会导致页分裂、碎片、索引膨胀。

八、如何分析索引有没有生效?

用 EXPLAIN:

复制代码

EXPLAIN SELECT * FROM user WHERE name = '张三';

重点看这几列:

|-----------|---------------------------------------------------------------------------------------------------|
| 列 | 关注什么 |
| type | 访问类型。system > const > eq_ref > ref > range > index > ALL。出现 ALL 就是全表扫描,危险。 |
| key | 实际用到的索引。NULL 表示没走索引。 |
| rows | 预估要扫描的行数,越小越好。 |
| Extra | Using index(覆盖索引)、Using index condition(索引下推)、Using filesort(额外排序,要优化)、Using temporary(用临时表,要优化)。 |


九、面试高频速答题

Q1:索引为什么用 B+ 树?

多叉矮树减少磁盘 I/O;非叶子节点不存数据,fanout 更大;叶子节点双向链表支持范围查询;查询路径长度稳定。

Q2:聚簇索引和二级索引的区别?

聚簇索引叶子节点存整行数据,一张表只有一个(主键);二级索引叶子节点存索引列 + 主键值,查全行需要回表。

Q3:什么是回表?怎么避免?

二级索引查到主键后再去主键索引查一次就是回表。用覆盖索引(要查的列都在二级索引叶子节点里)避免。

Q4:最左匹配原则是什么?

联合索引 (a,b,c),查询必须从最左列 a 开始,且中间不能断,遇到范围查询后后面的列失效。

Q5:索引失效有哪些场景?

函数 / 运算操作索引列、隐式类型转换、前导模糊查询、OR 无索引、违反最左匹配、NOT/!=、优化器放弃。


写在最后

索引是 MySQL 性能优化里性价比最高的手段,但它不是银弹:建多了拖慢写入,建少了拖慢查询。真正的高手会根据业务 SQL 去设计联合索引、利用覆盖索引和索引下推,并用 EXPLAIN 验证每一条慢查询。

把这篇文章收藏起来,面试前过一遍,索引这一块基本就稳了。

相关推荐
联犀14 分钟前
纯 Go 的 CAD 开源库 go-cad:直接解析 DWG/DXF,现在也能导出 PDF
开发语言·golang·pdf
骑着蜗牛撵大象32714 分钟前
MySQL 8.0 迁移后 SQL 变慢:执行计划里 3 类被忽略的成本差异
android·数据库·sql·mysql·优化器·执行计划
这个昵称不容易23 分钟前
SpringBoot 读书笔记共享平台部署实战:97 个 Java 文件 / 113 个接口 / 10 张表
java·springboot·毕设
zjxtxdy1 小时前
C++面向对象程序开发-类与对象
开发语言·c++
金銀銅鐵3 小时前
一个 Java 文件实现《孫子兵法·始計篇》中英对照阅读器
java·后端·ai编程
DevOpenClub3 小时前
高考教育信息问答 API:会话参数、资料引用与流式响应
java·数据库·高考
qq_406981033 小时前
c++拷贝构造函数
开发语言·c++
bro_Java6663 小时前
《排序的底层逻辑:比较、交换与分治》
java·算法·排序算法
爱喝雪碧的可乐3 小时前
MATLAB
开发语言·算法·matlab