【第四章】索引------B+树、回表、最左前缀,把查询提速
- [1. 索引是什么](#1. 索引是什么)
- [2. 为什么要用索引(以及索引的代价)](#2. 为什么要用索引(以及索引的代价))
- [3. 索引为什么不用 Hash](#3. 索引为什么不用 Hash)
- [4. 为什么是 B+ 树(不是二叉树、不是红黑树)](#4. 为什么是 B+ 树(不是二叉树、不是红黑树))
- [5. MySQL 的"页"(Page):16KB 的世界观](#5. MySQL 的“页”(Page):16KB 的世界观)
- [6. InnoDB 的两类核心索引:聚簇索引 & 二级索引](#6. InnoDB 的两类核心索引:聚簇索引 & 二级索引)
-
- [6.1 聚簇索引(Clustered Index)](#6.1 聚簇索引(Clustered Index))
- [6.2 二级索引(Secondary Index)](#6.2 二级索引(Secondary Index))
- [6.3 索引覆盖(Covering Index)](#6.3 索引覆盖(Covering Index))
- [7. 索引分类](#7. 索引分类)
-
- [7.1 主键索引(PRIMARY KEY)](#7.1 主键索引(PRIMARY KEY))
- [7.2 普通索引(INDEX / KEY)](#7.2 普通索引(INDEX / KEY))
- [7.3 唯一索引(UNIQUE)](#7.3 唯一索引(UNIQUE))
- [7.4 全文索引(FULLTEXT)](#7.4 全文索引(FULLTEXT))
- [8. 索引怎么用:自动创建、手动创建、查看、删除](#8. 索引怎么用:自动创建、手动创建、查看、删除)
-
- [8.1 哪些情况会自动创建索引](#8.1 哪些情况会自动创建索引)
- [8.2 手动创建索引(几种方式)](#8.2 手动创建索引(几种方式))
- [8.3 查看索引](#8.3 查看索引)
- [8.4 删除索引(别把自己绕进去)](#8.4 删除索引(别把自己绕进去))
- [9. 复合索引:最左前缀](#9. 复合索引:最左前缀)
- [10. EXPLAIN:你判断"到底走没走索引"的方式](#10. EXPLAIN:你判断“到底走没走索引”的方式)
- [11. 一些非常常见的"索引失效写法"](#11. 一些非常常见的“索引失效写法”)
-
- [11.1 对索引列做函数/运算](#11.1 对索引列做函数/运算)
- [11.2 LIKE 左边带 %](#11.2 LIKE 左边带 %)
- [11.3 复合索引跳过最左列](#11.3 复合索引跳过最左列)
- 本章小结
到这一步你会发现一个很真实的现象:SQL 写对不难,写快才难。
而在 MySQL 里,查询性能最绕不开的一块,就是索引。
这一章之前会遇到的问题":
- 索引到底是什么,为什么它能加速
- 为什么 MySQL 选 B+ 树,不选 Hash
- InnoDB 的聚簇索引 / 二级索引到底有什么区别(以及"回表"是什么)
- 索引怎么建、怎么查、怎么删,复合索引怎么用
- 常见写法为什么会让索引失效(你明明建了索引但还是全表扫)
剩下的问题就在这一章来解决。
1. 索引是什么
有个比喻我觉得特别贴:索引像书的目录。
- 你不看目录:找一段内容只能从第一页翻到最后一页(全表扫描)
- 你看目录:先定位大概位置,再直接翻过去(走索引)
在 MySQL 里更准确一点说,索引是一种数据结构,它把"列的值"按某种规则组织起来,并且能快速定位到对应的数据行。
你会发现一个关键点:索引的价值几乎都在"读" 。
因为插入/更新/删除时,索引也要跟着维护,索引越多,写入成本越高。
2. 为什么要用索引(以及索引的代价)
索引的目标只有一个:提升检索效率。
但它不是"白加速",它有明确代价:
- 占空间:索引本质也是一份数据结构文件
- 降低写入效率:INSERT/UPDATE/DELETE 都要改索引
- 选错会反噬:建了不合理的索引,反而可能让优化器走了更差的执行计划
所以索引不是"越多越好",而是"该建的建,不该建的别硬上"。
3. 索引为什么不用 Hash
核心原因很直接:Hash 不支持范围查找。
Hash 的优势是等值查询快(理想情况下接近 O(1)),但一旦你写:
sql
WHERE created_at BETWEEN ... AND ...
Hash 的结构就没法优雅地做范围扫描了。
而数据库里范围查询、排序、分组、分页太常见了,所以 MySQL 默认索引结构选择了 B+ 树这一套(InnoDB 的 BTREE 索引)。
4. 为什么是 B+ 树(不是二叉树、不是红黑树)
一个非常"数据库味"的点:IO 才是瓶颈。
内存里比较 100 次都不算啥,磁盘 IO 多来几次你就能看到慢查询了。所以索引结构的核心目标之一是:降低树高,减少 IO 次数。
- 二叉搜索树:最坏情况下退化成链表(O(N))
- AVL/红黑树:能平衡,但毕竟是二叉结构,树高仍然不够友好
- N 叉树:能有效降低树高
- B+ 树:更适合磁盘/页结构,叶子节点有序链表,范围查询更舒服

B+ 树的特点:
- 非叶子节点只做索引,不存真实数据
- 真实数据都在叶子节点(或者叶子节点指向真实数据)
- 叶子节点通过双向链表串起来,天然支持范围扫描
5. MySQL 的"页"(Page):16KB 的世界观
索引这块你只记住一句话就够用:页是内存与磁盘交互的最小单元,默认 16KB。
sql
SHOW VARIABLES LIKE 'innodb_page_size';
你会看到默认是 16384(16KB)。
为什么要按页读?
- 一次 IO 直接读一页,把邻近的数据也顺带读上来
- 局部性原理:你刚访问过的数据,接下来大概率还会访问附近的数据
这也是 B+ 树为什么在数据库里好用:它的节点大小可以和"页"很好地对齐。
6. InnoDB 的两类核心索引:聚簇索引 & 二级索引
6.1 聚簇索引(Clustered Index)
在 InnoDB 里:
- 有主键(PRIMARY KEY):主键就是聚簇索引
- 没主键:会找第一个
UNIQUE + NOT NULL的列当聚簇索引 - 还没有:InnoDB 会生成一个 6 字节的
ROW_ID来当聚簇索引
聚簇索引的直觉:叶子节点里存的就是整行数据。
6.2 二级索引(Secondary Index)
聚簇索引以外的索引,都是二级索引。二级索引的叶子节点里一般存的是:
- 索引列的值
- 以及对应行的主键值
当你通过二级索引定位到主键后,还要回到聚簇索引里把整行数据捞出来,这个过程就叫:回表。

6.3 索引覆盖(Covering Index)
如果你的查询只用到了某个二级索引里已经包含的列,那它就可以"不回表"直接返回结果,这种情况叫索引覆盖。
举个很实用的例子:
sql
CREATE TABLE IF NOT EXISTS t_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
sno VARCHAR(10) NOT NULL,
class_id BIGINT NOT NULL,
name VARCHAR(20) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_sno(sno),
KEY idx_sno_class(sno, class_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
走覆盖索引(只查索引里的列):
sql
SELECT sno, class_id
FROM t_user
WHERE sno = '100001';
容易回表(查询列里有 name,不在 idx_sno_class 的索引列里):
sql
SELECT sno, class_id, name
FROM t_user
WHERE sno = '100001';
覆盖索引不是魔法,它的本质就是:你要的列,索引里刚好都有。
7. 索引分类
7.1 主键索引(PRIMARY KEY)
你给表加主键,InnoDB 会用它当聚簇索引:
sql
CREATE TABLE t_pk (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20)
);
7.2 普通索引(INDEX / KEY)
就是最基础的索引,不保证唯一性:
sql
CREATE TABLE t_idx (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
sno VARCHAR(10),
KEY idx_sno(sno)
);
7.3 唯一索引(UNIQUE)
唯一约束会自动创建唯一索引:
sql
CREATE TABLE t_uk (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
sno VARCHAR(10) UNIQUE
);
7.4 全文索引(FULLTEXT)
用于文本检索(InnoDB/MyISAM 都支持,但适用场景更偏"文本搜索")。这一章先知道它存在就行,后面如果你做博客/文章检索再展开。
8. 索引怎么用:自动创建、手动创建、查看、删除
8.1 哪些情况会自动创建索引
三类特别典型:
- 主键(PRIMARY KEY)
- 唯一约束(UNIQUE)
- 外键(FOREIGN KEY)
直觉也很简单:这些约束要么需要快速判断"是否冲突",要么需要快速检查"引用是否存在",没有索引基本就跑不动。
8.2 手动创建索引(几种方式)
建表时直接指定:
sql
CREATE TABLE t_test_index6 (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
sno VARCHAR(10),
class_id BIGINT,
KEY idx_sno_class(sno, class_id)
);
建表后再加:
sql
ALTER TABLE t_test_index6 ADD INDEX idx_sno(sno);
ALTER TABLE t_test_index6 ADD UNIQUE uk_name(name);
或者单独建索引:
sql
CREATE INDEX idx_class_id ON t_test_index6(class_id);
8.3 查看索引
sql
SHOW INDEX FROM t_test_index6;
你看索引时我建议重点关注这几个字段:
Key_name:索引名Non_unique:0 表示唯一索引Seq_in_index:复合索引里字段顺序(很关键)Index_type:一般是 BTREE
8.4 删除索引(别把自己绕进去)
删除普通索引:
sql
ALTER TABLE t_test_index6 DROP INDEX idx_class_id;
删除主键索引(课件里也专门提醒了一个坑):
如果主键列还是自增列,直接删主键会报错,因为自增列必须是 key。
正确姿势是先去掉自增属性,再删主键:
sql
ALTER TABLE t_test_index6 MODIFY id BIGINT;
ALTER TABLE t_test_index6 DROP PRIMARY KEY;
9. 复合索引:最左前缀
复合索引的语法很简单,就是多个列用逗号隔开:
sql
CREATE INDEX idx_a_b ON t_user(sno, class_id);
真正难的是怎么用:复合索引满足"最左前缀"。
也就是说,对于 (sno, class_id):
- 能用索引:
WHERE sno = ? - 能用索引:
WHERE sno = ? AND class_id = ? - 很可能用不上:
WHERE class_id = ?

10. EXPLAIN:你判断"到底走没走索引"的方式
索引相关的问题,最怕两句话:
- "我明明建了索引,怎么还是慢?"
- "我看着像走了索引,但我也不确定。"
别猜,直接 EXPLAIN。
sql
EXPLAIN
SELECT *
FROM t_user
WHERE sno = '100001';
你重点看这几个字段就够用:
type:访问类型(越像 index/range/ref 越好,ALL 通常就是全表扫)possible_keys:可能会用的索引key:实际用的索引rows:预估扫描行数(越小越好)Extra:有没有Using index(常见于覆盖索引)
11. 一些非常常见的"索引失效写法"
11.1 对索引列做函数/运算
比如对时间列做函数:
sql
WHERE DATE(created_at) = '2026-09-04'
很多时候会让索引不太好用。更常见写法是改成范围:
sql
WHERE created_at >= '2026-09-04 00:00:00'
AND created_at < '2026-09-05 00:00:00'
11.2 LIKE 左边带 %
sql
WHERE name LIKE '%三'
这种写法很难利用普通 BTREE 索引(因为前缀都不确定了)。
11.3 复合索引跳过最左列
你建了 (sno, class_id),但你只按 class_id 查:
sql
WHERE class_id = 1
那就别指望它一定能走到这个复合索引。
本章小结
索引这章大概讲述了下面的内容:
- 索引是为了减少扫描范围,代价是空间和写入维护成本
- InnoDB 默认用 B+ 树,是为了减少 IO、支持范围查询、支持顺序遍历
- 二级索引可能要回表,覆盖索引能省掉回表;复合索引要遵守最左前缀
下一章我们还会为大家介绍数据库另一个更加核心的要点:事务。也是在数据库当中属于核心的章节,喜欢的朋友记得给个三连呦~