【数据库知识】聚簇索引&二级索引

1 缘起

在日常开发中,我们几乎每天都在写 SQL,也习惯性地在遇到性能问题时说一句"加个索引吧"。然而,当数据量从几十万增长到几百万、几千万时,原本运行顺畅的查询突然变慢,Explain 输出里出现的 Using index condition、Using temporary、Using filesort 也让人摸不着头脑。更让人困惑的是:明明已经加了索引,为什么查询还是慢?

随着问题不断积累,我逐渐意识到:

很多性能瓶颈,并不是 SQL 写得不好,而是我们对 InnoDB 索引结构的理解还停留在表面。

当我真正深入到 InnoDB 的内部实现,看到:

聚簇索引的叶子节点存的是整行数据

二级索引的叶子节点只存索引列和主键

二级索引查询必须"回表"

回表意味着随机 IO,而 IO 才是性能的真正杀手

那一刻,我才明白:

理解索引结构,不是数据库优化的高级技巧,而是每个开发者都必须掌握的基础能力。

也正是从那时起,我开始系统整理聚簇索引与二级索引的知识,希望用更直观的方式解释它们的结构、差异与性能影响,让每一个写 SQL 的人都能真正理解:

为什么有的查询飞快,有的查询却慢如蜗牛;

为什么同样的数据量,有的索引有效,有的索引却毫无作用。

这篇文章,就是从这样的思考中诞生的。

2 聚簇索引(Clustered Index)

InnoDB 的主键索引就是 聚簇索引,叶子节点 存储: 整行数据

2.1 聚簇索引 B+Tree 结构示意图

复制代码
                 [Root Page]
                     |
        --------------------------------
        |                              |
   [Non-leaf Page]                [Non-leaf Page]
        |                              |
   -----------                     -----------
   |    |    |                     |    |    |
[Leaf][Leaf][Leaf]             [Leaf][Leaf][Leaf]
   |      |                        |      |
   |      |                        |      |
   ↓      ↓                        ↓      ↓

================= 聚簇索引叶子页(存整行) =================
| PK=1 | colA | colB | colC | ... | 整行数据                |
| PK=2 | colA | colB | colC | ... | 整行数据                |
| PK=3 | colA | colB | colC | ... | 整行数据                |
============================================================

特点

叶子节点 = 整行数据

主键顺序存储

查询主键不需要回表

3 二级索引(Secondary Index)

二级索引的叶子节点 不存整行数据,只存:

  • 索引列值
  • 主键值(指向聚簇索引的"指针")

3.1 二级索引 B+Tree 结构示意图

复制代码
                 [Root Page]
                     |
        --------------------------------
        |                              |
   [Non-leaf Page]                [Non-leaf Page]
        |                              |
   -----------                     -----------
   |    |    |                     |    |    |
[Leaf][Leaf][Leaf]             [Leaf][Leaf][Leaf]
   |      |                        |      |
   ↓      ↓                        ↓      ↓

================= 二级索引叶子页(不存整行) =================
| idx_col=18 | PK=3 |
| idx_col=18 | PK=7 |
| idx_col=20 | PK=1 |
| idx_col=21 | PK=9 |
==============================================================

3.2 特点

二级索引叶子节点 = 索引列 + 主键

需要根据主键再去聚簇索引查整行(回表)

4 二级索引查询 + 回表过程图

假设执行:age 有二级索引:

复制代码
SELECT name FROM user WHERE age = 18;

4.1 查询流程

二级索引找到主键, 再根据主键去聚簇索引查整行,这一步就是 回表(Bookmark Lookup)。

复制代码
                二级索引(age)
                      |
               找到叶子节点
                      |
       --------------------------------
       |                              |
  (age=18, PK=3)                (age=18, PK=7)
       |                              |
       ↓                              ↓
  回表到聚簇索引                 回表到聚簇索引
       |                              |
       ↓                              ↓

================= 聚簇索引(主键) =================
| PK=3 | name=Tom  | age=18 | ... |
| PK=7 | name=Lucy | age=18 | ... |
====================================================

4.2 为什么二级索引需要回表?

因为 InnoDB 只有 一个聚簇索引(主键),整行数据只存储在主键索引的叶子节点。

二级索引为了节省空间,只能存:索引列;主键值(指针)

所以必须回表。

5 回表导致 IO 的原因

5.1 原因 1:二级索引的主键分布随机 → 回表访问的数据页随机

复制代码
(age=18, PK=3)
(age=18, PK=7)
(age=18, PK=1024)
(age=18, PK=900000)

这些主键对应的行:

  • 分布在不同的数据页
  • 页之间没有连续性
  • 很难被缓存命中
  • 随机访问 = 随机磁盘 IO(最慢)

5.2 原因 2:数据页比索引页大得多,无法全部缓存

假设:

每行 200B

100 万行 ≈ 200MB 数据页

Buffer Pool 只有 1GB

虽然索引页(几十 MB)能缓存住,但:

  • 数据页无法全部缓存
  • 回表时很可能需要从磁盘读数据页

磁盘随机读一次 ≈ 5ms

内存读一次 ≈ 100ns

差距 5 万倍

5.3 原因 3:每次回表都可能触发一次磁盘随机读

一次二级索引查询可能需要:

3 次索引页访问(B+Tree 高度 3)

1 次回表访问(聚簇索引)

如果这些页不在内存:
(3+1)×5𝑚𝑠=20𝑚𝑠(3+1)×5𝑚𝑠=20𝑚𝑠(3+1)×5ms=20ms

每秒只能处理 50 次查询。

6 为什么百万级数据后回表 IO 会爆炸?

  • 数据页数量大
  • 数据页随机访问
  • Buffer Pool 无法缓存全部数据页
  • 回表命中率下降
  • IO 次数急剧增加

最终表现为:

  • 查询变慢
  • QPS 降低
  • CPU 空闲但磁盘忙(典型 IO 瓶颈)

7 如何减少回表 IO?

方案 描述
使用覆盖索引(最有效) 让查询只用二级索引,不回表
增大 Buffer Pool 让更多数据页常驻内存
避免 SELECT * 减少回表需要读取的列
使用更短的主键 减少二级索引大小,提高缓存命中率
分区/分表 减少单表数据页数量

8 小结

项目 说明
回表是什么 二级索引查到主键后,再去主键索引查整行
为什么需要回表 二级索引不存整行数据
为什么回表慢 需要随机访问聚簇索引页
IO 与回表关系 回表 = 随机 IO,数据量大时 IO 成瓶颈
百万级后性能下降原因 数据页无法全部缓存,回表命中率下降
相关推荐
科技小花4 小时前
数据治理平台架构演进观察:AI原生设计如何重构企业数据管理范式
数据库·重构·架构·数据治理·ai-native·ai原生
一江寒逸4 小时前
零基础从入门到精通MySQL(中篇):进阶篇——吃透多表查询、事务核心与高级特性,搞定复杂业务SQL
数据库·sql·mysql
D4c-lovetrain4 小时前
linux个人心得22 (mysql)
数据库·mysql
阿里小阿希4 小时前
CentOS7 PostgreSQL 9.2 升级到 15 完整教程
数据库·postgresql
荒川之神4 小时前
Oracle 数据仓库雪花模型设计(完整实战方案)
数据库·数据仓库·oracle
做个文艺程序员5 小时前
MySQL安全加固十大硬核操作
数据库·mysql·安全
不吃香菜学java5 小时前
Redis简单应用
数据库·spring boot·tomcat·maven
一个天蝎座 白勺 程序猿5 小时前
Apache IoTDB(15):IoTDB查询写回(INTO子句)深度解析——从语法到实战的ETL全链路指南
数据库·apache·etl·iotdb
不知名的老吴5 小时前
Redis的延迟瓶颈:TCP栈开销无法避免
数据库·redis·缓存
YOU OU5 小时前
三大范式和E-R图
数据库