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 会:
-
- 优先选一个非空唯一索引当聚簇索引;
-
- 都没有?自动生成一个 6 字节的隐藏列 rowid 当聚簇索引。
- 聚簇索引的叶子节点就是数据本身,所以通过主键查不需要回表。
2.3 二级索引 / 辅助索引(Secondary Index)
除主键外的索引都叫二级索引。叶子节点存的是 "索引列值 + 主键值",不是整行数据。
二级索引(按 name 列组织)
┌─────────────────────────────┐
│ 非叶子节点:name 值 + 子页指针 │
└─────────────────────────────┘
↓
┌─────────────────────────────┐
│ 叶子节点:(name, 主键 id) │
└─────────────────────────────┘
2.4 回表(Bookmark Lookup)------ 高频面试题
如果你用二级索引 name 查 WHERE name = '张三':
- 先在 name 的 B+ 树叶子节点拿到 name='张三' 对应的主键 id = 100;
- 再拿着 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 时:
- 在 name 索引树里找到所有 name LIKE '张%' 的记录(可能有 1000 条);
- 对这 1000 条全部回表,拿到全行后再判断 age = 20。
有 ICP 时:
- 在 name 索引树里找 name LIKE '张%' 的同时,顺便用索引里已经存的 age 列先过滤一遍;
- 只对满足 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 关键规则总结
- 最左列必须出现,否则索引失效。
- 中间不能断,断了后面的列用不上。
- 遇到范围查询(>、<、BETWEEN、LIKE 'x%')之后的列,索引失效(但范围列本身能用)。
- LIKE 前导 % 失效,LIKE 'abc%' 可以,LIKE '%abc' 不行。
- 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 转成数字再比较,相当于对索引列做了函数操作,索引直接失效。
六、索引的优缺点
优点
- 大幅减少扫描行数(降低 EXPLAIN 中的 rows)。
- 加速排序(ORDER BY)、分组(GROUP BY)、连接(JOIN)。
- 实现唯一性约束。
缺点
- 占空间:每个索引都是一棵 B+ 树,索引越多磁盘占用越大。
- 降低写性能:INSERT / UPDATE / DELETE 时,不仅要改数据页,还要同步维护所有相关索引树。
- 优化器选错索引的风险:索引多了,优化器可能选错,反而变慢。
七、索引设计原则(面试加分项)
- 优先给 WHERE、JOIN、ORDER BY、GROUP BY 里出现的列建索引。
- 区分度低的列不要单独建索引(比如性别,只有 0/1/2,建了也白建,优化器不会用)。
-
- 区分度 = 唯一值个数 / 总行数,越接近 1 越好。
- 优先建联合索引,少建单列索引,充分利用最左匹配和覆盖索引。
- 扩展索引,不要新建索引:已有 (a, b),要加 (a, b, c) 就把 (a,b) 扩成 (a,b,c),而不是新建一个。
- 索引列尽量用小数据类型:int 比 bigint 省空间,char (N) 比 varchar 定长,B+ 树一个页能放更多键,fanout 更大,树更矮。
- 字符串太长用前缀索引。
- 避免过度索引:一张表索引建议不超过 5~6 个。
- 主键建议用自增 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 验证每一条慢查询。
把这篇文章收藏起来,面试前过一遍,索引这一块基本就稳了。