vault backup: 2026-05-21 12:20:54
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user