SheepDog1998

博客园 首页 新随笔 联系 订阅 管理

读懂 MySQL 索引:从全表扫描到 B+Tree 快速定位

摘要

索引是数据库性能优化中最常见、也最容易被误解的工具。很多人认为“索引越多查询越快”“LIMIT 1 就不需要索引”“索引只是一张额外的排序表”,这些理解都不完整。

本文以 MySQL InnoDB 为主,系统介绍索引为什么能提升查询效率、B+Tree 如何减少数据检查量、聚簇索引与二级索引的区别、什么是回表和覆盖索引,以及如何使用 EXPLAIN 验证索引是否真正生效。同时讨论普通索引、唯一索引、组合索引的选择,以及索引对写入、存储和执行计划的影响。

一、索引解决的是什么问题

数据库查询可以粗略分为两步:

  1. 找到符合条件的数据;
  2. 返回或处理这些数据。

索引主要优化的是第一步:帮助数据库快速定位数据,减少为了找到目标而检查无关记录的数量。

假设一张表有 100 万行,需要执行:

SELECT id, email
FROM demo_user
WHERE email = 'user@example.com';

如果没有合适的索引,数据库可能只能逐行检查:

读取第 1 行,判断 email 是否匹配
读取第 2 行,判断 email 是否匹配
……
直到找到目标,或者检查完整张表

这种访问方式称为全表扫描。

如果 email 上有合适的索引,数据库可以先通过索引定位目标,再取得需要的数据,从而避免检查大量无关记录。

因此,索引优化的核心不是让数据库“计算得更快”,而是让数据库少检查数据、少读取数据。

二、返回一行,不代表查询成本很低

下面两条 SQL 都只返回一行,但执行成本可能完全不同:

SELECT id
FROM demo_user
WHERE email = 'user@example.com'
LIMIT 1;
SELECT id
FROM demo_user
WHERE id = 10001
LIMIT 1;

如果 email 没有索引,第一条 SQL 中的 LIMIT 1 只能保证“找到一条后停止”,不能保证数据库能快速找到第一条匹配记录。

如果匹配记录位于表的最后,或者根本不存在,数据库仍可能检查大量数据。

而第二条 SQL 使用主键查询,通常可以直接通过主键索引定位。

所以需要牢记:

LIMIT 限制的是返回数量,不是数据库必须扫描的数量。

同样,查询结果只有一条,也不代表数据库只读取了一行。判断查询成本要看执行过程,而不是只看结果条数。

三、把索引理解成目录,但不要只把它理解成一张表

可以用查字典来理解索引。

没有目录时,只能一页一页翻;有拼音或部首目录时,可以先定位大致范围,再找到具体词条。

数据库索引也类似:

  • 原表保存完整数据;
  • 索引保存用于定位的数据;
  • 查询先通过索引缩小范围,再访问目标记录。

但索引并不是简单复制出一张普通表。如果只是复制一份无序数据,查询时仍然从头扫描,并不会自然变快。

索引真正的价值在于:

  1. 使用适合查找的数据结构;
  2. 按索引键组织数据;
  3. 能快速排除大量无关范围;
  4. 通常只保存部分字段,读取内容更少。

在 InnoDB 中,常见索引使用 B+Tree 结构。

四、为什么是 B+Tree

1. 从有序查找开始理解

对于无序数据:

38、7、96、15、62、3、81、24

寻找 62 时,通常只能逐个检查。

对于有序数据:

3、7、15、24、38、62、81、96

可以先检查中间位置,根据大小关系排除一部分数据,再继续缩小范围。

核心思想是:

利用数据的顺序,不必检查每一个值。

2. B+Tree 是多层、多分支的查找目录

B+Tree 不是简单地把所有值排成一行,而是把查找过程组织成多层节点:

                根节点
          /        |        \
      内部节点   内部节点   内部节点
       /  \       /  \       /  \
    叶子节点  叶子节点  叶子节点  叶子节点

查询时:

  1. 从根节点开始判断目标可能位于哪个分支;
  2. 进入下一层继续缩小范围;
  3. 最终到达叶子节点;
  4. 找到索引键及其对应记录的位置。

每个节点可以容纳多个索引项,因此树的高度通常很低。即使表中有大量记录,也往往只需访问少量节点即可完成定位。

3. B+Tree 为什么适合数据库

B+Tree 的特点比较适合磁盘和页式存储:

  • 单个节点可以包含大量索引项,降低树的高度;
  • 非叶子节点主要负责导航;
  • 叶子节点保存索引键及相关定位信息;
  • 叶子节点有序并相互连接,方便范围扫描;
  • 查询、插入和删除可以保持较好的平衡性。

它不仅适合:

WHERE email = 'user@example.com'

也适合某些范围查询:

WHERE created_at >= '2026-01-01'
  AND created_at <  '2026-02-01'

以及符合索引顺序的排序、前缀匹配等操作。

五、聚簇索引:InnoDB 如何组织表数据

InnoDB 表数据通常按照聚簇索引组织。一般情况下,主键就是聚簇索引。

假设表结构为:

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;

主键查询:

SELECT *
FROM demo_user
WHERE id = 10001;

可以通过聚簇索引直接定位到完整数据行。

如果表没有显式主键,InnoDB 会选择符合要求的唯一非空索引;如果也没有符合条件的索引,则生成隐藏的内部聚簇索引。因此,显式定义合适的主键通常是更清晰的做法。

六、二级索引:为什么叶子记录中包含主键

为 email 创建普通索引:

CREATE INDEX idx_demo_user_email
ON demo_user(email);

可以粗略把二级索引理解为:

email                → 主键 id
admin@example.com         10003
test@example.com          10008
user@example.com          10001

它不是完整复制原表,而是保存:

  • 索引字段 email;
  • 对应记录的主键 id。

二级索引保存主键值,是为了在需要时定位聚簇索引中的完整数据行。

因此,虽然创建索引时只写了 email,二级索引中仍然会带有主键信息。

七、什么是回表

执行:

SELECT id, email, name
FROM demo_user
WHERE email = 'user@example.com';

二级索引中有 email 和 id,但没有 name。数据库可能需要:

通过 email 二级索引找到主键 id
→ 根据 id 访问聚簇索引
→ 读取完整记录中的 name

根据二级索引取得主键,再回到聚簇索引读取其他字段的过程,通常称为“回表”。

少量回表通常是正常操作;如果需要回表的记录很多,额外访问成本就可能变得明显。

需要注意,回表并不等于索引失效,也不一定是问题。是否值得优化,应结合查询频率、返回行数和实际耗时判断。

八、什么是覆盖索引

执行:

SELECT id, email
FROM demo_user
WHERE email = 'user@example.com';

当前查询只需要:

  • 查询条件中的 email;
  • 返回字段 id。

而 idx_demo_user_email 中已经包含这两个值,因此数据库可以直接从索引返回结果,不需要回表。

这种“索引覆盖了查询所需全部字段”的情况称为覆盖索引。

EXPLAIN 中可能出现:

Extra = Using index

在 InnoDB 的执行计划中,这通常表示使用了覆盖索引,不需要访问聚簇索引中的完整数据行。

但不要为了减少回表,盲目把所有可能查询的字段都塞进索引。索引越宽,通常会:

  • 占用更多存储空间;
  • 降低单个索引页可容纳的条目数量;
  • 增加写入和维护成本;
  • 占用更多内存。

覆盖索引应该服务于明确的高频查询,而不是追求让所有查询都不回表。

九、普通索引和唯一索引有什么区别

1. 普通索引

CREATE INDEX idx_demo_user_name
ON demo_user(name);

普通索引主要用于加速查询,允许多条记录拥有相同的索引值。

2. 唯一索引

CREATE UNIQUE INDEX uk_demo_user_email
ON demo_user(email);

唯一索引除了可以用于查询定位,还要求索引值不能重复。

如果已经存在两条相同的 email,创建唯一索引会失败;创建成功后,插入或更新出重复值也会报错。

因此:

  • 普通索引解决的是查询访问路径问题;
  • 唯一索引同时解决查询定位和数据唯一性约束问题。

3. MySQL 中的 NULL

MySQL 的唯一索引通常允许多个 NULL,因为 NULL 表示未知,不按普通确定值处理重复。

但空字符串不同:

NULL ≠ ''

唯一索引中可以有多个 NULL,但通常不能有多条相同的空字符串。

4. 唯一索引一定比普通索引快很多吗

不一定。

如果普通索引匹配记录很少,两者性能可能非常接近。唯一索引能让优化器知道最多只有一条匹配记录,但真正的性能收益仍取决于数据分布和查询方式。

选择唯一索引的首要理由应该是业务或数据模型要求唯一,而不是单纯为了追求查询速度。

十、主键索引和普通索引已经够用,为什么还需要组合索引

组合索引是在多个字段上共同建立的索引,例如:

CREATE INDEX idx_demo_user_status_created
ON demo_user(status, created_at);

可以粗略理解为先按 status 组织,再在同一状态中按 created_at 组织。

它适合以下查询:

SELECT id
FROM demo_user
WHERE status = 1
  AND created_at >= '2026-01-01';

组合索引不是多个单列索引的简单替代品,而是为了适应特定的多字段查询、排序和覆盖需求。

十一、组合索引的最左前缀原则

假设索引为:

(a, b, c)

通常可以有效利用索引进行定位的查询条件包括:

WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3

但下面的条件通常不能直接利用该组合索引完成同样的高效定位:

WHERE b = 2
WHERE c = 3
WHERE b = 2 AND c = 3

原因是组合索引先按照 a 组织。缺少最左字段时,后面的字段在全局范围内并不具备同样的有序性。

此外,范围条件也可能影响后续字段参与索引定位的程度。例如:

WHERE a = 1
  AND b > 10
  AND c = 3;

c 仍可能用于过滤或覆盖,但不一定能继续像前面的等值条件一样缩小索引扫描范围。应通过实际执行计划判断,而不是只背规则。

十二、哪些写法可能导致索引不能充分发挥作用

1. 对索引字段进行计算或函数处理

例如:

WHERE DATE(created_at) = '2026-01-01'

可以改写成范围条件:

WHERE created_at >= '2026-01-01'
  AND created_at <  '2026-01-02'

具体是否能使用索引,还取决于字段类型、函数和优化器能力,但范围写法通常更容易利用普通 B+Tree 索引。

2. 使用前导通配符

WHERE name LIKE '%tom'

普通 B+Tree 索引难以利用这种条件直接定位,因为开头字符不确定。

而:

WHERE name LIKE 'tom%'

在合适条件下可以利用索引进行前缀范围查找。

3. 隐式类型转换

如果索引字段是字符串,却使用可能引发转换的比较方式,可能影响索引使用。例如字符串字段与数字常量比较时,需要关注实际类型转换规则。

应保持:

  • 字段类型一致;
  • 关联字段类型尽量一致;
  • 字符集和排序规则兼容;
  • 参数类型与字段类型匹配。

4. 索引字段选择性太低

如果某个字段只有极少数不同值,并且绝大多数记录都相同,那么即使使用索引,也可能仍需读取大量记录。

此时优化器甚至可能认为全表扫描成本更低。索引存在,不代表优化器一定使用。

十三、为什么 MySQL 有索引却不使用

索引只是为优化器提供一种可选访问路径,不是强制命令。

优化器会综合估算:

  • 预计匹配行数;
  • 索引扫描成本;
  • 回表成本;
  • 表的大小;
  • 数据分布;
  • 排序和分组需求;
  • 缓冲与读取成本。

例如,一个条件预计匹配表中大部分记录:

通过索引找到大量主键
→ 再执行大量回表

其成本可能高于直接顺序读取整张表,因此优化器可能选择全表扫描。

这种选择不一定是错误。全表扫描也不天然等于慢,索引查询也不天然等于快,最终要看数据规模和实际成本。

十四、两个索引会比一个索引快吗

索引数量与查询速度不是正比关系。

假设有:

PRIMARY KEY (id);
INDEX idx_email(email);

对于:

SELECT *
FROM demo_user
WHERE email = 'user@example.com';

通常主要使用 email 索引,id 主键索引不会再为它叠加一次加速。

对于:

SELECT *
FROM demo_user
WHERE id = 10001;

通常使用主键索引,email 索引没有作用。

MySQL 在某些场景下可以进行索引合并,但不代表所有查询都会同时使用多个索引,也不代表索引合并一定更快。

正确的理解是:

多个索引可以为不同查询提供更多优化选择,但不是同一个查询的性能简单相加。

十五、索引会立刻生效吗

当 CREATE INDEX 成功完成后,新发起的语句就可以根据优化器选择使用该索引。

通常不需要:

  • 重启数据库;
  • 重启应用;
  • 修改已有 SQL;
  • 手动刷新索引。

已经开始执行的语句不会中途改变执行计划。

索引存在不等于一定被使用,最终仍应通过 EXPLAIN 验证。

十六、数据变化时索引如何更新

索引由数据库自动维护,应用不需要单独更新索引内容。

插入数据

除了写入数据行,还需要在相关索引中插入索引项。

修改数据

如果修改了索引字段,数据库需要调整对应索引项的位置或内容。

删除数据

删除数据行时,相关索引项也需要删除。

事务一致性

数据和索引变更属于同一个事务:

  • 提交后一起对外可见;
  • 回滚时一起撤销;
  • 正常情况下不会出现业务数据已修改、索引内容却停留在旧值的情况。

数据库不会每修改一条记录就把整个索引重新排序,而是修改相关节点,必要时进行局部页分裂或合并。

十七、索引的成本是什么

索引通过额外存储和维护成本换取查询效率。

主要成本包括:

  1. 磁盘空间:索引需要保存索引键和定位信息;
  2. 内存占用:热点索引页也会占用缓冲池;
  3. 写入开销:插入、更新、删除时需要维护相关索引;
  4. 页分裂与碎片:某些写入模式可能带来额外维护成本;
  5. 优化器选择成本:过多、重复或相似索引会增加管理复杂度。

因此索引不是越多越好。

重复或低价值索引可能几乎没有查询收益,却持续增加每次写入的成本。

十八、使用 EXPLAIN 验证索引是否有效

创建索引后,应检查执行计划,而不是只看索引是否存在。

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 版本中,可以使用:

EXPLAIN ANALYZE
SELECT id, email
FROM demo_user
WHERE email = 'user@example.com';

它会实际执行语句,并给出实际执行时间、实际行数及循环次数等信息,有助于发现:

  • 估算行数与实际行数差异很大;
  • 某个节点实际耗时远高于预期;
  • 优化器选择了不合适的访问路径。

对于 SELECT,它也会真实执行查询,因此大查询仍可能消耗数据库资源,应在测试环境或低峰期谨慎使用。

二十、一个实用的索引优化流程

第一步:确认慢在数据库

通过日志、监控或分段计时确认耗时发生在 SQL 执行,而不是网络传输、结果处理或文件生成。

第二步:取得真实 SQL 和参数

同一条 SQL 使用不同参数时,匹配行数和执行计划可能不同。尽量分析真实慢请求,而不是随意构造条件。

第三步:查看数据规模和分布

关注:

  • 表总量;
  • 条件匹配量;
  • 索引字段不同值数量;
  • 是否存在极端数据倾斜。

第四步:检查现有索引

SHOW INDEX FROM demo_user;

避免创建重复索引,并确认字段顺序是否符合查询需求。

第五步:执行 EXPLAIN

确认当前访问类型、实际使用索引和预计扫描行数。

第六步:一次只修改一个关键变量

例如只新增一个目标索引,然后重新检查执行计划和耗时。不要同时修改 SQL、超时、缓存和多个索引,否则难以判断真正有效的变更。

第七步:验证写入影响

索引优化不仅要看查询,还要评估相关表的插入、更新和删除频率。

第八步:进行完整回归

确认:

  • 查询结果不变;
  • 实际耗时改善;
  • 执行计划稳定;
  • 写入功能正常;
  • 没有其他重要 SQL 因计划变化而退化。

二十一、常见误区总结

误区一:索引越多,查询越快

事实是:合适的索引才有价值,多余索引会增加写入和维护成本。

误区二:有 LIMIT 1 就不需要索引

事实是:LIMIT 只限制返回数量,不保证快速定位第一条匹配记录。

误区三:结果只有一条,查询就一定很轻量

事实是:数据库可能为了找到这一条记录检查了大量数据。

误区四:创建了索引,就一定会使用

事实是:优化器会根据成本选择访问路径,需要通过 EXPLAIN 验证。

误区五:全表扫描一定是错误的

事实是:小表或高匹配比例查询中,全表扫描可能是合理选择。

误区六:唯一索引只是为了提高速度

事实是:唯一索引首先是一种数据完整性约束,其次才是访问路径。

误区七:二级索引只保存显式指定的字段

事实是:InnoDB 二级索引还包含主键值,用于定位聚簇索引记录。

误区八:Using index 表示“使用了任意索引”

事实是:在 Extra 中,它通常表示覆盖索引;是否实际使用索引主要看 key 字段。

误区九:为了覆盖查询,应该把很多字段都加入索引

事实是:过宽索引会增加存储和维护成本,应针对明确的高价值查询设计。

误区十:索引能够解决所有慢查询

事实是:索引主要优化数据定位,无法自动解决超大结果集、锁等待、网络传输、复杂计算或应用处理缓慢。

二十二、结语

索引的本质,是用额外的存储空间和写入维护成本,为数据库建立更高效的数据定位路径。

理解索引时,最重要的不是记住多少条规则,而是抓住三个问题:

  1. 数据库为了找到目标数据,需要检查多少无关记录?
  2. 当前索引能否帮助它快速缩小范围?
  3. 索引带来的查询收益,是否值得它产生的存储和写入成本?

在实际优化中,应始终坚持:

真实 SQL 和参数
→ 数据规模与分布
→ 现有索引
→ EXPLAIN 执行计划
→ 单变量优化
→ 实际耗时与业务回归

索引不是魔法,也不是数量竞赛。真正有效的索引优化,是让正确的查询在正确的数据上使用正确的访问路径。

posted on 2026-11-05 10:13  SheepDog1998  阅读(38)  评论(0)    收藏  举报