vault backup: 2026-05-21 12:20:54

This commit is contained in:
hhs
2026-05-21 12:20:54 +08:00
parent 2e84683d9c
commit 531d4b7d8c
8 changed files with 867 additions and 89 deletions
@@ -7,7 +7,13 @@ create time: 2026-05-20 14:00
## 概述
InnoDB 使用 B+ Tree 组织所有索引,但根据「索引叶子节点存什么」的不同,分为两类:**聚簇索引**和**二级索引**。理解这两者的差异,是你设计高效查询和执行计划的起点。
InnoDB 使用 B+ Tree 组织所有索引,但根据「索引叶子节点存什么」的不同,分为两类:**聚簇索引**和**二级索引**。
打个比方:如果把数据库表想象成一本通讯录——
- **聚簇索引**就是通讯录正文本身:按姓名排序,每个条目后面直接写上了电话、地址等所有信息。
- **二级索引**就像附录的索引卡片:只写了「姓名 → 第几页」,想看完整信息还得翻回正文。
理解这两者的差异,是你设计高效查询和执行计划的起点。
为了方便说明,本文档将始终使用以下 `users` 表作为示例:
@@ -24,8 +30,8 @@ CREATE TABLE users (
```
这张表中:
- **聚簇索引**:`id`(PRIMARY KEY)
- **二级索引**:`idx_email`(email)、`idx_status_created`(status, created_at)
- **聚簇索引**:`id`(PRIMARY KEY)—— 因为 InnoDB 的规则是:谁是 PRIMARY KEY,谁就是聚簇索引。你可以这样理解:InnoDB **根本不单独存"表数据"**,它把整张表的数据直接塞进了以 `id` 为键的 B+ Tree 的叶子节点里。所以 `id` 这棵 B+ Tree 既是索引,也是数据本身。(后面会详细展开这条规则。)
- **二级索引**:`idx_email`(email)、`idx_status_created`(status, created_at) —— 这两个索引的叶子节点里不存完整行数据,只存「索引列的值 + `id`」,所以叫"二级":它们是依附于聚簇索引的"小抄",查到 `id` 后还得回聚簇索引里拿完整数据。
> [!QUESTION] 思考
> 假设一张用户表按 `id` 排序存储在磁盘上——
@@ -69,7 +75,9 @@ block-beta
## 聚簇索引(Clustered Index)
聚簇索引就是数据本身。InnoDB 表中**只能有一个聚簇索引**。
"聚簇"这个词听起来很抽象,但意思很简单:**数据按照索引的顺序"聚集"在一起存储**。在 InnoDB 里,聚簇索引的叶子节点不是"指向"数据,叶子节点**就是**数据——整行记录都直接存在里面。
正因为数据只有一份,所以一张表**只能有一个**聚簇索引。
### 聚簇索引的形成规则
@@ -89,6 +97,8 @@ block-beta
### 聚簇索引的特性
数据按主键顺序存储带来了两个直接好处:写入快(顺序追加)和范围查询快(连续扫描):
```mermaid
flowchart TD
A["按主键排序存储数据"] --> B["数据页紧凑排列"]
@@ -109,6 +119,15 @@ flowchart TD
除了聚簇索引外的所有索引都是二级索引。叶子节点存储的是:**索引列的值 + 主键值**。
> [!QUESTION] 为什么二级索引要存主键?
> 因为二级索引本身没有完整数据,但它知道"这条记录在哪"——通过主键就能回到聚簇索引里找到完整行。主键就是二级索引回聚簇索引的"导航坐标"。
>
> 整个过程分两步:
> 1. 在二级索引里找到匹配条件的记录,拿到主键 `id`
> 2. 拿着 `id` 去聚簇索引里找完整行数据
>
> 这个"从二级索引跳回聚簇索引取数据"的过程,就叫 **回表**。
```mermaid
flowchart TD
subgraph "二级索引页<br/>idx_status_created = (status, created_at)"
@@ -131,6 +150,8 @@ flowchart TD
## 回表的代价
刚才说了回表是"跳回聚簇索引取数据",但这个跳转是有代价的——它意味着额外的磁盘 I/O。下面这张图展示了具体过程:
```mermaid
sequenceDiagram
participant App as 应用层
@@ -152,7 +173,9 @@ sequenceDiagram
### 减少回表的策略(Covering Index)
要减少回表,最直接的方法是让查询**完全在二级索引中完成**——这就是 Covering Index。
回表既然是因为二级索引里"信息不够"才跳回聚簇索引,那最直接的解决办法就是:**让二级索引里包含查询需要的所有信息**,这样就不需要跳回去了。
这就是 Covering Index(覆盖索引)——索引"覆盖"了查询的全部需求,查完索引就够了,不用回表。
```sql
-- ❌ 差:Covering Index 未命中,需要回表拿 name 字段
@@ -220,11 +243,13 @@ flowchart TD
```
> [!HIGHLIGHT] ICP 的本质
> 把 **Server 层**的过滤条件(如 `created_at > ...`)**下推到 Handler 层**,在读取二级索引页时就完成判断,命中了才回表。
> 用生活化的类比来说:以前的做法是,快递员(存储引擎)在仓库(二级索引)里找到 1000 个包裹,全部搬出来,再由前台(Server 层)一个一个拆开检查是不是客户要的。ICP 的做法是,直接告诉快递员"只搬 status=1 且 created_at 在这个日期之后的",在仓库里就筛掉了 950 个,只搬出 50 个。
>
> 技术上说,就是把过滤条件下推到存储引擎层,在读取二级索引页时就完成判断,命中了才回表。
>
> **适用条件:**
> - 必须是二级索引(聚簇索引不走 ICP)
> - 过滤条件中能利用到的部分必须在索引列范围内
> - 必须是二级索引(聚簇索引本身已经是完整数据,不存在"下推"的问题)
> - 过滤条件必须能利用到索引列(否则在索引里也没法判断)
> - MySQL 5.6+ 默认开启,无需额外配置
```sql