Skip to content

MySQL 索引详解

这篇文章专门讲 MySQL 里的“索引”,不把它只当成一个概念名词,而是把它放回真实工程问题里来看:

  1. 为什么索引能加速查询
  2. 为什么索引也会拖慢写入
  3. 为什么有些 SQL 明明建了索引还是慢
  4. 怎样设计索引,才能兼顾查询效率和维护成本

如果你已经看过 MySQL 总览页,可以把这篇文章理解成其中“索引”部分的展开版。

1. 索引到底在解决什么问题

不建索引时,数据库很多查询只能做全表扫描。所谓全表扫描,本质上就是从头到尾把记录一行一行看过去,再判断哪一行符合条件。

当数据量小时,这件事未必明显;但一旦表里有几十万、几百万甚至更多数据,问题就出来了:

  1. 扫描范围太大
  2. 排序代价高
  3. 分组和联表成本上升
  4. 热点查询会持续吃掉 I/O 和 CPU

索引的核心思路是:提前把“如何快速找到数据”这件事组织好。

它不是凭空让查询变快,而是把原本“扫描很多数据”的过程,变成“沿着一棵有序结构快速定位目标范围”。

2. 没有索引 vs 有索引

mermaid
flowchart LR
    A[查询条件<br/>where id = 1001] --> B{有没有索引}
    B -- 没有 --> C[从第一行开始扫描]
    C --> D[逐行判断条件]
    D --> E[可能扫到很后面才命中]
    B -- 有 --> F[先走索引结构定位范围]
    F --> G[快速找到目标记录附近]
    G --> H[再读取目标数据]

这一部分最重要的不是形式,而是你要抓住一个本质:

  • 没有索引时,数据库更像“把所有记录翻一遍”
  • 有索引时,数据库更像“先查目录,再翻到对应页”

3. 🌟 为什么 InnoDB 主流用的是 B+ 树

在 InnoDB 里,最常见的索引结构是 B+ 树。你可以把它理解成一棵“为磁盘和范围查询优化过的多路查找树”。

它适合数据库,不是因为它概念上高级,而是因为它刚好满足数据库最在意的几件事:

  1. 树要尽量矮,减少 I/O 次数
  2. 数据要有序,方便范围查询
  3. 等值查询、排序、范围扫描要尽量统一处理

3.1 B+ 树的关键结构

你不需要一开始就把细节背得很死,只要先记住这三个点:

  1. 非叶子节点主要存 key 和子指针
  2. 真正的数据主要落在叶子节点
  3. 叶子节点之间按顺序连接

3.2 B+ 树索引结构

mermaid
flowchart TD
    R[根节点<br/>17、35]
    N1[非叶子节点<br/>9]
    N2[非叶子节点<br/>26]
    N3[非叶子节点<br/>42]
    L1[叶子节点<br/>3 6 8]
    L2[叶子节点<br/>9 12 16]
    L3[叶子节点<br/>17 20 24]
    L4[叶子节点<br/>26 30 34]
    L5[叶子节点<br/>35 38 40]
    L6[叶子节点<br/>42 45 48]

    R --> N1
    R --> N2
    R --> N3
    N1 --> L1
    N1 --> L2
    N2 --> L3
    N2 --> L4
    N3 --> L5
    N3 --> L6
    L1 -. 顺序连接 .-> L2
    L2 -. 顺序连接 .-> L3
    L3 -. 顺序连接 .-> L4
    L4 -. 顺序连接 .-> L5
    L5 -. 顺序连接 .-> L6

这一部分想说明的是:

  • 这是一张为了讲清结构而做的简化示意图,不代表真实页大小和阶数
  • 上层节点负责“导航”
  • 最下面一层叶子节点负责“真正命中数据”
  • 叶子节点天然有序,所以 between> <、排序扫描都很友好

这也是为什么数据库更偏爱 B+ 树,而不是红黑树或者哈希做通用索引。

4. 🌟 聚簇索引、二级索引、回表

这是 InnoDB 索引里最容易让人“背了名词但没真正理解”的一段。

4.1 聚簇索引是什么

在 InnoDB 中,主键索引通常就是聚簇索引。所谓聚簇,不是说它“更高级”,而是说:

表数据本身就是按主键索引组织存储的。

也就是说,聚簇索引的叶子节点里,放的不是“一个地址”,而是整行记录。

4.2 二级索引是什么

二级索引不是按完整行存储的,它的叶子节点通常存的是:

  1. 二级索引列的值
  2. 对应记录的主键值

所以当你通过二级索引查到一批主键后,如果 SQL 还需要其他列,MySQL 往往还得再去聚簇索引里把整行捞出来。

这个动作就叫回表。

4.3 🌟 回表流程

mermaid
flowchart LR
    A[SQL<br/>select id name age from user where name='张三'] --> B[先查 name 二级索引]
    B --> C[二级索引命中<br/>name='张三' -> id=1024]
    C --> D{要的列够不够}
    D -- 不够 --> E[拿着主键 id=1024]
    E --> F[回到聚簇索引]
    F --> G[取出整行记录<br/>id name age ...]
    D -- 够了 --> H[直接返回结果]

“回表”这个词听起来有点抽象,但你可以把它理解成:先通过普通索引找到线索,再根据主键回到主数据里把整行捞出来。

比如有这样一条 SQL:

sql
select id, name, age
from user
where name = '张三';

假设表上有 name 索引,但这个索引叶子节点里只保存:

  1. name
  2. 对应记录的主键 id

这时候 MySQL 会先走 name 这个二级索引,找到“张三”对应的主键值,比如 id = 1024

但问题是,这条 SQL 还要拿 age,而 age 不在这个二级索引里。

所以数据库接下来还得再做一步:拿着 id = 1024,再去聚簇索引里找到这条完整记录。

这第二次回到主键索引拿整行数据的动作,就是回表。

你可以把整个过程简化记成两步:

  1. 先查二级索引,拿到主键
  2. 再查聚簇索引,拿到整行

为什么回表会让查询变慢?

因为它本质上多了一次查找过程。如果命中的记录很多,就可能出现:

  1. 二级索引先扫出很多主键
  2. 再一条条回到聚簇索引取数据
  3. 随着数据量上来,随机 I/O 和整体开销都会增加

所以优化里经常会追求覆盖索引,本质上就是:尽量让需要的列直接在索引里拿到,避免回表。

5. 🌟 覆盖索引为什么常常更快

如果一个查询需要的列,恰好都能从索引本身拿到,那么数据库就不必再回到聚簇索引取整行。

这就是覆盖索引。

例如:

sql
select id, name
from user
where name = '张三';

如果有一个索引正好包含 nameid,那么 MySQL 可能直接从索引结果里把数据返回。

覆盖索引快的根本原因,不是概念上“高级”,而是它少了一次回表。

6. 🌟 联合索引和最左匹配

联合索引是多个列组合成一个索引,例如:

sql
create index idx_user_status_created_at on user(status, created_at, id);

这类索引最关键的规则,就是最左前缀匹配。

你可以把它理解成:索引是先按第一列排,再在第一列相同的范围里按第二列排,最后再按第三列排。

6.1 联合索引的排列方式

mermaid
flowchart TD
    A[联合索引<br/>status created_at id]
    A --> B[先按 status 分大范围]
    B --> C1[status = 0]
    B --> C2[status = 1]
    C1 --> D1[在 status = 0 内<br/>再按 created_at 排序]
    C2 --> D2[在 status = 1 内<br/>再按 created_at 排序]
    D1 --> E1[最后同值范围内<br/>再按 id 排序]
    D2 --> E2[最后同值范围内<br/>再按 id 排序]

所以:

  • where status = 1,可以很好利用索引
  • where status = 1 and created_at > ...,通常也能很好利用索引
  • where created_at > ...,如果跳过了最左列 status,利用效果就会明显变差

这就是最左匹配原则的根本原因。不是数据库故意刁难,而是索引本身就是按这个顺序组织的。

7. 🌟 哪些情况会让索引失效或利用变差

“建了索引为什么还是慢”是最常见的问题之一。很多时候不是索引完全失效,而是优化器判断“用索引不划算”或者“只能利用一部分”。

常见情况包括:

7.1 对索引列做函数、计算或隐式转换

例如:

sql
where date(create_time) = '2026-08-01'

这种写法相当于先对每行做函数计算,再比较结果,索引往往很难直接利用。

更常见的优化方式是改成范围查询:

sql
where create_time >= '2026-08-01 00:00:00'
  and create_time < '2026-08-02 00:00:00'

7.2 模糊查询前面带 %

例如:

sql
where name like '%abc'

因为前缀不确定,B+ 树很难直接定位起始位置。

但如果是:

sql
where name like 'abc%'

通常还能利用前缀有序性。

7.3 返回数据太多

即使建了索引,如果条件命中结果特别多,优化器也可能认为“走索引再回表”不如直接扫表。

7.4 联合索引使用顺序不合理

例如索引是 (a, b, c),结果你的查询大量是:

sql
where b = ? and c = ?

这种情况下,索引不是不存在,而是设计和查询模式没有对齐。

8. 索引设计不是越多越好

新手很容易走到另一个极端:一看到 SQL 慢,就继续加索引。

这会带来明显副作用:

  1. 写入变慢,因为每次插入、更新、删除都要维护索引
  2. 占用更多磁盘和内存
  3. 优化器选择变复杂
  4. 冗余索引会增加维护成本

所以索引设计的原则不是“尽量多”,而是“尽量让高频查询有合适的索引”。

9. 一个更实用的索引设计方法

如果你在真实项目里给表设计索引,可以按这个顺序思考:

  1. 看高频 SQL,而不是看表结构
  2. 明确查询条件、排序、分页、返回列
  3. 判断是等值查询优先,还是范围查询优先
  4. 让联合索引顺序尽量匹配最常用查询路径
  5. 尽量让热点查询变成覆盖索引
  6. 避免重复、重叠、长期不用的索引

10. 怎么把索引这件事讲清楚

如果要把 MySQL 索引真正讲顺,比起一上来背名词,更自然的顺序通常是:

  1. 先说索引解决的是查询范围缩小问题
  2. 再说 InnoDB 主流是 B+ 树,因为它适合磁盘和范围查询
  3. 接着说明聚簇索引、二级索引、回表、覆盖索引
  4. 再补联合索引和最左匹配
  5. 最后落到真实项目里如何根据高频 SQL 设计索引

这样梳理,比单独记“什么是回表”“什么是最左匹配”更接近真正理解了数据库索引。

11. 最后总结

你可以把 MySQL 索引理解成三层:

  1. 结构层:B+ 树为什么适合数据库
  2. 存储层:聚簇索引、二级索引、回表、覆盖索引
  3. 工程层:联合索引顺序、失效场景、真实 SQL 优化

真正会用索引,不是背出定义,而是看到一个 SQL 时,脑子里能立刻出现这几个问题:

  • 它会不会走索引
  • 走的是哪个索引
  • 会不会回表
  • 扫描范围大不大
  • 这个索引值不值得维护

当你开始用这种方式看 SQL,索引才算真正从“抽象概念”变成“工程工具”。

基于 VitePress 构建的个人技术笔记。