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

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 成瓶颈
百万级后性能下降原因 数据页无法全部缓存,回表命中率下降
相关推荐
Blossom i34 分钟前
大数据预处理与采集实验一:使用Python操作MySQL数据库
数据库·mysql
雾时之林1 小时前
Linux--软件管理、源码包安装
linux·服务器·数据库
老纪的技术唠嗑局3 小时前
超级干货分享:中小企业使用 OceanBase 实践经验汇总
数据库
CLOUD ACE3 小时前
谷歌云代理|零售商Target 如何利用 Spanner Graph 提升零售发现体验并将数据库维护成本降低 50%
数据库·零售
AI大模型-小雄3 小时前
Codex写分页接口为什么越翻越慢?用Cursor Pagination解决重复与漏数据
大数据·数据库·elasticsearch·搜索引擎·chatgpt·后端开发·codex
少晓年4 小时前
从 MySQL 迁移到人大金仓 KingbaseES:完整指南与实践
数据库·mysql
SelectDB5 小时前
Apache Doris AI RAG 实战:从基础 RAG 到知识图谱增强的技术能力与选型
数据库
SelectDB5 小时前
电商企业 PostgreSQL 迁移 Apache Doris:80TB 分析数据统一平台技术能力与实践
数据库
SelectDB5 小时前
阶跃星辰 Agent 可观测:Apache Doris / SelectDB 的技术能力与实践
数据库
SelectDB5 小时前
ApacheDoris Iceberg V3 湖仓 DML:Apache Doris / SelectDB 的技术能力与实践
数据库