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

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 成瓶颈
百万级后性能下降原因 数据页无法全部缓存,回表命中率下降
相关推荐
斯普信专业组2 小时前
Redis Cluster 集群化部署全流程指南:从源码编译到容器化
数据库·redis·缓存
任子菲阳2 小时前
学JavaWeb第五天——MySQL
数据库·mysql
ZePingPingZe3 小时前
MySQL查看事务与锁
数据库·mysql
TDengine (老段)3 小时前
从“被动养护”到“主动预警”,TDengine IDMP 让智慧桥梁靠数据“说话”
大数据·数据库·人工智能·物联网·时序数据库·tdengine·涛思数据
白日做梦Q3 小时前
【MySQL】9.吃透关键SQL语法:从正则表达式、窗口函数、条件函数到结果集合并的实战拆解
数据库·sql·mysql·正则表达式
likuolei3 小时前
正则表达式 - 元字符
数据库·mysql·正则表达式
侧耳倾听1113 小时前
mysql中的binlog-介绍
数据库·mysql
少云清3 小时前
【接口测试】4_PyMySQL模块 _操作数据库
服务器·网络·数据库
IndulgeCui3 小时前
Kingbase-金仓企业级统一管控平台KEMCC一键部署主备集群及转换读写分离集群
数据库