读懂 MySQL 索引:从全表扫描到 B+Tree 快速定位
摘要
索引是数据库性能优化中最常见、也最容易被误解的工具。很多人认为"索引越多查询越快""LIMIT 1 就不需要索引""索引只是一张额外的排序表",这些理解都不完整。
本文以 MySQL InnoDB 为主,系统介绍索引为什么能提升查询效率、B+Tree 如何减少数据检查量、聚簇索引与二级索引的区别、什么是回表和覆盖索引,以及如何使用 EXPLAIN 验证索引是否真正生效。同时讨论普通索引、唯一索引、组合索引的选择,以及索引对写入、存储和执行计划的影响。
一、索引解决的是什么问题
数据库查询可以粗略分为两步:
- 找到符合条件的数据;
- 返回或处理这些数据。
索引主要优化的是第一步:帮助数据库快速定位数据,减少为了找到目标而检查无关记录的数量。
假设一张表有 100 万行,需要执行:
sql
SELECT id, email
FROM demo_user
WHERE email = 'user@example.com';
如果没有合适的索引,数据库可能只能逐行检查:
text
读取第 1 行,判断 email 是否匹配
读取第 2 行,判断 email 是否匹配
......
直到找到目标,或者检查完整张表
这种访问方式称为全表扫描。
如果 email 上有合适的索引,数据库可以先通过索引定位目标,再取得需要的数据,从而避免检查大量无关记录。
因此,索引优化的核心不是让数据库"计算得更快",而是让数据库少检查数据、少读取数据。
二、返回一行,不代表查询成本很低
下面两条 SQL 都只返回一行,但执行成本可能完全不同:
sql
SELECT id
FROM demo_user
WHERE email = 'user@example.com'
LIMIT 1;
sql
SELECT id
FROM demo_user
WHERE id = 10001
LIMIT 1;
如果 email 没有索引,第一条 SQL 中的 LIMIT 1 只能保证"找到一条后停止",不能保证数据库能快速找到第一条匹配记录。
如果匹配记录位于表的最后,或者根本不存在,数据库仍可能检查大量数据。
而第二条 SQL 使用主键查询,通常可以直接通过主键索引定位。
所以需要牢记:
LIMIT限制的是返回数量,不是数据库必须扫描的数量。
同样,查询结果只有一条,也不代表数据库只读取了一行。判断查询成本要看执行过程,而不是只看结果条数。
三、把索引理解成目录,但不要只把它理解成一张表
可以用查字典来理解索引。
没有目录时,只能一页一页翻;有拼音或部首目录时,可以先定位大致范围,再找到具体词条。
数据库索引也类似:
- 原表保存完整数据;
- 索引保存用于定位的数据;
- 查询先通过索引缩小范围,再访问目标记录。
但索引并不是简单复制出一张普通表。如果只是复制一份无序数据,查询时仍然从头扫描,并不会自然变快。
索引真正的价值在于:
- 使用适合查找的数据结构;
- 按索引键组织数据;
- 能快速排除大量无关范围;
- 通常只保存部分字段,读取内容更少。
在 InnoDB 中,常见索引使用 B+Tree 结构。
四、为什么是 B+Tree
1. 从有序查找开始理解
对于无序数据:
text
38、7、96、15、62、3、81、24
寻找 62 时,通常只能逐个检查。
对于有序数据:
text
3、7、15、24、38、62、81、96
可以先检查中间位置,根据大小关系排除一部分数据,再继续缩小范围。
核心思想是:
利用数据的顺序,不必检查每一个值。
2. B+Tree 是多层、多分支的查找目录
B+Tree 不是简单地把所有值排成一行,而是把查找过程组织成多层节点:
text
根节点
/ | \
内部节点 内部节点 内部节点
/ \ / \ / \
叶子节点 叶子节点 叶子节点 叶子节点
查询时:
- 从根节点开始判断目标可能位于哪个分支;
- 进入下一层继续缩小范围;
- 最终到达叶子节点;
- 找到索引键及其对应记录的位置。
每个节点可以容纳多个索引项,因此树的高度通常很低。即使表中有大量记录,也往往只需访问少量节点即可完成定位。
3. B+Tree 为什么适合数据库
B+Tree 的特点比较适合磁盘和页式存储:
- 单个节点可以包含大量索引项,降低树的高度;
- 非叶子节点主要负责导航;
- 叶子节点保存索引键及相关定位信息;
- 叶子节点有序并相互连接,方便范围扫描;
- 查询、插入和删除可以保持较好的平衡性。
它不仅适合:
sql
WHERE email = 'user@example.com'
也适合某些范围查询:
sql
WHERE created_at >= '2026-01-01'
AND created_at < '2026-02-01'
以及符合索引顺序的排序、前缀匹配等操作。
五、聚簇索引:InnoDB 如何组织表数据
InnoDB 表数据通常按照聚簇索引组织。一般情况下,主键就是聚簇索引。
假设表结构为:
sql
CREATE TABLE demo_user (
id BIGINT NOT NULL AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
name VARCHAR(100),
created_at DATETIME NOT NULL,
PRIMARY KEY (id)
) ENGINE = InnoDB;
主键查询:
sql
SELECT *
FROM demo_user
WHERE id = 10001;
可以通过聚簇索引直接定位到完整数据行。
如果表没有显式主键,InnoDB 会选择符合要求的唯一非空索引;如果也没有符合条件的索引,则生成隐藏的内部聚簇索引。因此,显式定义合适的主键通常是更清晰的做法。
六、二级索引:为什么叶子记录中包含主键
为 email 创建普通索引:
sql
CREATE INDEX idx_demo_user_email
ON demo_user(email);
可以粗略把二级索引理解为:
text
email → 主键 id
admin@example.com 10003
test@example.com 10008
user@example.com 10001
它不是完整复制原表,而是保存:
- 索引字段
email; - 对应记录的主键
id。
二级索引保存主键值,是为了在需要时定位聚簇索引中的完整数据行。
因此,虽然创建索引时只写了 email,二级索引中仍然会带有主键信息。
七、什么是回表
执行:
sql
SELECT id, email, name
FROM demo_user
WHERE email = 'user@example.com';
二级索引中有 email 和 id,但没有 name。数据库可能需要:
text
通过 email 二级索引找到主键 id
→ 根据 id 访问聚簇索引
→ 读取完整记录中的 name
根据二级索引取得主键,再回到聚簇索引读取其他字段的过程,通常称为"回表"。
少量回表通常是正常操作;如果需要回表的记录很多,额外访问成本就可能变得明显。
需要注意,回表并不等于索引失效,也不一定是问题。是否值得优化,应结合查询频率、返回行数和实际耗时判断。
八、什么是覆盖索引
执行:
sql
SELECT id, email
FROM demo_user
WHERE email = 'user@example.com';
当前查询只需要:
- 查询条件中的
email; - 返回字段
id。
而 idx_demo_user_email 中已经包含这两个值,因此数据库可以直接从索引返回结果,不需要回表。
这种"索引覆盖了查询所需全部字段"的情况称为覆盖索引。
EXPLAIN 中可能出现:
text
Extra = Using index
在 InnoDB 的执行计划中,这通常表示使用了覆盖索引,不需要访问聚簇索引中的完整数据行。
但不要为了减少回表,盲目把所有可能查询的字段都塞进索引。索引越宽,通常会:
- 占用更多存储空间;
- 降低单个索引页可容纳的条目数量;
- 增加写入和维护成本;
- 占用更多内存。
覆盖索引应该服务于明确的高频查询,而不是追求让所有查询都不回表。
九、普通索引和唯一索引有什么区别
1. 普通索引
sql
CREATE INDEX idx_demo_user_name
ON demo_user(name);
普通索引主要用于加速查询,允许多条记录拥有相同的索引值。
2. 唯一索引
sql
CREATE UNIQUE INDEX uk_demo_user_email
ON demo_user(email);
唯一索引除了可以用于查询定位,还要求索引值不能重复。
如果已经存在两条相同的 email,创建唯一索引会失败;创建成功后,插入或更新出重复值也会报错。
因此:
- 普通索引解决的是查询访问路径问题;
- 唯一索引同时解决查询定位和数据唯一性约束问题。
3. MySQL 中的 NULL
MySQL 的唯一索引通常允许多个 NULL,因为 NULL 表示未知,不按普通确定值处理重复。
但空字符串不同:
text
NULL ≠ ''
唯一索引中可以有多个 NULL,但通常不能有多条相同的空字符串。
4. 唯一索引一定比普通索引快很多吗
不一定。
如果普通索引匹配记录很少,两者性能可能非常接近。唯一索引能让优化器知道最多只有一条匹配记录,但真正的性能收益仍取决于数据分布和查询方式。
选择唯一索引的首要理由应该是业务或数据模型要求唯一,而不是单纯为了追求查询速度。
十、主键索引和普通索引已经够用,为什么还需要组合索引
组合索引是在多个字段上共同建立的索引,例如:
sql
CREATE INDEX idx_demo_user_status_created
ON demo_user(status, created_at);
可以粗略理解为先按 status 组织,再在同一状态中按 created_at 组织。
它适合以下查询:
sql
SELECT id
FROM demo_user
WHERE status = 1
AND created_at >= '2026-01-01';
组合索引不是多个单列索引的简单替代品,而是为了适应特定的多字段查询、排序和覆盖需求。
十一、组合索引的最左前缀原则
假设索引为:
text
(a, b, c)
通常可以有效利用索引进行定位的查询条件包括:
sql
WHERE a = 1
sql
WHERE a = 1 AND b = 2
sql
WHERE a = 1 AND b = 2 AND c = 3
但下面的条件通常不能直接利用该组合索引完成同样的高效定位:
sql
WHERE b = 2
sql
WHERE c = 3
sql
WHERE b = 2 AND c = 3
原因是组合索引先按照 a 组织。缺少最左字段时,后面的字段在全局范围内并不具备同样的有序性。
此外,范围条件也可能影响后续字段参与索引定位的程度。例如:
sql
WHERE a = 1
AND b > 10
AND c = 3;
c 仍可能用于过滤或覆盖,但不一定能继续像前面的等值条件一样缩小索引扫描范围。应通过实际执行计划判断,而不是只背规则。
十二、哪些写法可能导致索引不能充分发挥作用
1. 对索引字段进行计算或函数处理
例如:
sql
WHERE DATE(created_at) = '2026-01-01'
可以改写成范围条件:
sql
WHERE created_at >= '2026-01-01'
AND created_at < '2026-01-02'
具体是否能使用索引,还取决于字段类型、函数和优化器能力,但范围写法通常更容易利用普通 B+Tree 索引。
2. 使用前导通配符
sql
WHERE name LIKE '%tom'
普通 B+Tree 索引难以利用这种条件直接定位,因为开头字符不确定。
而:
sql
WHERE name LIKE 'tom%'
在合适条件下可以利用索引进行前缀范围查找。
3. 隐式类型转换
如果索引字段是字符串,却使用可能引发转换的比较方式,可能影响索引使用。例如字符串字段与数字常量比较时,需要关注实际类型转换规则。
应保持:
- 字段类型一致;
- 关联字段类型尽量一致;
- 字符集和排序规则兼容;
- 参数类型与字段类型匹配。
4. 索引字段选择性太低
如果某个字段只有极少数不同值,并且绝大多数记录都相同,那么即使使用索引,也可能仍需读取大量记录。
此时优化器甚至可能认为全表扫描成本更低。索引存在,不代表优化器一定使用。
十三、为什么 MySQL 有索引却不使用
索引只是为优化器提供一种可选访问路径,不是强制命令。
优化器会综合估算:
- 预计匹配行数;
- 索引扫描成本;
- 回表成本;
- 表的大小;
- 数据分布;
- 排序和分组需求;
- 缓冲与读取成本。
例如,一个条件预计匹配表中大部分记录:
text
通过索引找到大量主键
→ 再执行大量回表
其成本可能高于直接顺序读取整张表,因此优化器可能选择全表扫描。
这种选择不一定是错误。全表扫描也不天然等于慢,索引查询也不天然等于快,最终要看数据规模和实际成本。
十四、两个索引会比一个索引快吗
索引数量与查询速度不是正比关系。
假设有:
sql
PRIMARY KEY (id);
INDEX idx_email(email);
对于:
sql
SELECT *
FROM demo_user
WHERE email = 'user@example.com';
通常主要使用 email 索引,id 主键索引不会再为它叠加一次加速。
对于:
sql
SELECT *
FROM demo_user
WHERE id = 10001;
通常使用主键索引,email 索引没有作用。
MySQL 在某些场景下可以进行索引合并,但不代表所有查询都会同时使用多个索引,也不代表索引合并一定更快。
正确的理解是:
多个索引可以为不同查询提供更多优化选择,但不是同一个查询的性能简单相加。
十五、索引会立刻生效吗
当 CREATE INDEX 成功完成后,新发起的语句就可以根据优化器选择使用该索引。
通常不需要:
- 重启数据库;
- 重启应用;
- 修改已有 SQL;
- 手动刷新索引。
已经开始执行的语句不会中途改变执行计划。
索引存在不等于一定被使用,最终仍应通过 EXPLAIN 验证。
十六、数据变化时索引如何更新
索引由数据库自动维护,应用不需要单独更新索引内容。
插入数据
除了写入数据行,还需要在相关索引中插入索引项。
修改数据
如果修改了索引字段,数据库需要调整对应索引项的位置或内容。
删除数据
删除数据行时,相关索引项也需要删除。
事务一致性
数据和索引变更属于同一个事务:
- 提交后一起对外可见;
- 回滚时一起撤销;
- 正常情况下不会出现业务数据已修改、索引内容却停留在旧值的情况。
数据库不会每修改一条记录就把整个索引重新排序,而是修改相关节点,必要时进行局部页分裂或合并。
十七、索引的成本是什么
索引通过额外存储和维护成本换取查询效率。
主要成本包括:
- 磁盘空间:索引需要保存索引键和定位信息;
- 内存占用:热点索引页也会占用缓冲池;
- 写入开销:插入、更新、删除时需要维护相关索引;
- 页分裂与碎片:某些写入模式可能带来额外维护成本;
- 优化器选择成本:过多、重复或相似索引会增加管理复杂度。
因此索引不是越多越好。
重复或低价值索引可能几乎没有查询收益,却持续增加每次写入的成本。
十八、使用 EXPLAIN 验证索引是否有效
创建索引后,应检查执行计划,而不是只看索引是否存在。
sql
EXPLAIN
SELECT id, email
FROM demo_user
WHERE email = 'user@example.com';
重点字段如下。
1. type
表示访问类型。常见值包括:
const:通过主键或唯一索引等值定位常量记录;ref:通过非唯一索引等值查找;range:索引范围扫描;index:扫描索引;ALL:全表扫描。
这些值需要结合查询和表规模理解,不能脱离场景机械排名。
2. possible_keys
优化器认为可能使用的索引。
3. key
实际选择的索引。
possible_keys 中存在某个索引,但 key 为 NULL,说明它最终没有被选择。
4. rows
优化器预计需要检查的行数。这是估算值,不是精确结果行数。
如果创建索引后 rows 从几十万降到很小,通常说明访问路径得到明显改善。
5. Extra
常见信息包括:
Using index:通常表示覆盖索引;Using where:服务器层还需要进行条件过滤;Using filesort:需要额外排序,不一定表示使用磁盘文件;Using temporary:需要临时表,不一定就意味着查询不可接受。
这些提示需要结合完整执行计划分析,不能看到某个关键词就直接下结论。
十九、EXPLAIN ANALYZE 有什么区别
EXPLAIN 主要展示优化器预估的执行计划,不一定真正执行查询。
在支持该功能的 MySQL 版本中,可以使用:
sql
EXPLAIN ANALYZE
SELECT id, email
FROM demo_user
WHERE email = 'user@example.com';
它会实际执行语句,并给出实际执行时间、实际行数及循环次数等信息,有助于发现:
- 估算行数与实际行数差异很大;
- 某个节点实际耗时远高于预期;
- 优化器选择了不合适的访问路径。
对于 SELECT,它也会真实执行查询,因此大查询仍可能消耗数据库资源,应在测试环境或低峰期谨慎使用。
二十、一个实用的索引优化流程
第一步:确认慢在数据库
通过日志、监控或分段计时确认耗时发生在 SQL 执行,而不是网络传输、结果处理或文件生成。
第二步:取得真实 SQL 和参数
同一条 SQL 使用不同参数时,匹配行数和执行计划可能不同。尽量分析真实慢请求,而不是随意构造条件。
第三步:查看数据规模和分布
关注:
- 表总量;
- 条件匹配量;
- 索引字段不同值数量;
- 是否存在极端数据倾斜。
第四步:检查现有索引
sql
SHOW INDEX FROM demo_user;
避免创建重复索引,并确认字段顺序是否符合查询需求。
第五步:执行 EXPLAIN
确认当前访问类型、实际使用索引和预计扫描行数。
第六步:一次只修改一个关键变量
例如只新增一个目标索引,然后重新检查执行计划和耗时。不要同时修改 SQL、超时、缓存和多个索引,否则难以判断真正有效的变更。
第七步:验证写入影响
索引优化不仅要看查询,还要评估相关表的插入、更新和删除频率。
第八步:进行完整回归
确认:
- 查询结果不变;
- 实际耗时改善;
- 执行计划稳定;
- 写入功能正常;
- 没有其他重要 SQL 因计划变化而退化。
二十一、常见误区总结
误区一:索引越多,查询越快
事实是:合适的索引才有价值,多余索引会增加写入和维护成本。
误区二:有 LIMIT 1 就不需要索引
事实是:LIMIT 只限制返回数量,不保证快速定位第一条匹配记录。
误区三:结果只有一条,查询就一定很轻量
事实是:数据库可能为了找到这一条记录检查了大量数据。
误区四:创建了索引,就一定会使用
事实是:优化器会根据成本选择访问路径,需要通过 EXPLAIN 验证。
误区五:全表扫描一定是错误的
事实是:小表或高匹配比例查询中,全表扫描可能是合理选择。
误区六:唯一索引只是为了提高速度
事实是:唯一索引首先是一种数据完整性约束,其次才是访问路径。
误区七:二级索引只保存显式指定的字段
事实是:InnoDB 二级索引还包含主键值,用于定位聚簇索引记录。
误区八:Using index 表示"使用了任意索引"
事实是:在 Extra 中,它通常表示覆盖索引;是否实际使用索引主要看 key 字段。
误区九:为了覆盖查询,应该把很多字段都加入索引
事实是:过宽索引会增加存储和维护成本,应针对明确的高价值查询设计。
误区十:索引能够解决所有慢查询
事实是:索引主要优化数据定位,无法自动解决超大结果集、锁等待、网络传输、复杂计算或应用处理缓慢。
二十二、结语
索引的本质,是用额外的存储空间和写入维护成本,为数据库建立更高效的数据定位路径。
理解索引时,最重要的不是记住多少条规则,而是抓住三个问题:
- 数据库为了找到目标数据,需要检查多少无关记录?
- 当前索引能否帮助它快速缩小范围?
- 索引带来的查询收益,是否值得它产生的存储和写入成本?
在实际优化中,应始终坚持:
text
真实 SQL 和参数
→ 数据规模与分布
→ 现有索引
→ EXPLAIN 执行计划
→ 单变量优化
→ 实际耗时与业务回归
索引不是魔法,也不是数量竞赛。真正有效的索引优化,是让正确的查询在正确的数据上使用正确的访问路径。