This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/MySQL/17-聚簇索引与二级索引.md
T
2026-05-17 00:06:11 +08:00

201 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [MySQL, 聚簇索引, 二级索引, Covering Index, 回表]
create time: 2026-05-16 00:00
---
# 聚簇索引 vs 二级索引
## 概述
InnoDB 使用 B+ Tree 组织所有索引,但根据「索引叶子节点存什么」的不同,分为两类:**聚簇索引**和**二级索引**。理解这两者的差异,是你设计高效查询和执行计划的起点。
> [!QUESTION] 思考
> 假设一张用户表按 `id` 排序存储在磁盘上——
> 现在要查 `WHERE email = 'alice@test.com'`,数据库需要做什么?
> (提示:email 不是存储排序的依据。)
带着这个问题往下看。
## 聚簇索引(Clustered Index)
聚簇索引就是数据本身。InnoDB 表中**只能有一个聚簇索引**。
### 聚簇索引的形成规则
| 优先级 | 条件 | 说明 |
|--------|------|------|
| ① | **PRIMARY KEY** | 如果有显式 PK,它自动成为聚簇索引 |
| ② | **UNIQUE + NOT NULL** | 没有 PK 但有唯一非空列,用它 |
| ③ | **隐藏 row_id** | 都没有时,InnoDB 自动生成隐藏的 6-byte row_id |
> [!WARNING] 隐藏的 row_id 是个坑
> 如果你的表既没 PK 也没有 UNIQUE NOT NULL 列,InnoDB 会自动生成隐藏主键。这时:
> - 你自定义的任何索引都会变成二级索引
> - 二级索引回表时需要额外跳转 → 回表代价更高
> - 外键引用不可靠(ID 对用户透明)
>
> **结论:每张表必须有显式 PRIMARY KEY。**
### 聚簇索引的特性
```mermaid
flowchart TD
A["按主键排序存储数据"] --> B["数据页紧凑排列"]
B --> C["连续主键 = 顺序写入"]
C --> D["最小化页分裂"]
D --> E["最佳写入性能"]
A --> F["范围查询极快<br/>WHERE id BETWEEN 100 AND 200<br/>只需扫描一段连续的叶子页"]
F --> G["顺序 I/O 而非随机 I/O"]
style E fill:#00D866,color:#fff
style G fill:#00B6BC,color:#fff
```
## 二级索引(Secondary Index)
除了聚簇索引外的所有索引都是二级索引。叶子节点存储的是:**索引列的值 + 主键值**。
```mermaid
flowchart TD
subgraph "二级索引页<br/>idx_status_created = (status, created_at)"
direction LR
R1["status=1 \| created_at=01 → PK=5"]
R2["status=1 \| created_at=02 → PK=12"]
R3["status=1 \| created_at=03 → PK=18"]
R4["status=0 \| created_at=01 → PK=3"]
R5["status=0 \| created_at=04 → PK=25"]
end
style R1 fill:#E8DFF5,color:#333
style R3 fill:#00B6BC,color:#fff
```
> [!NOTE] 关键观察
> - 每行都包含 **索引列值**(用于匹配查询条件)和 **主键值**(用于回表定位完整行数据)
> - 二级索引本身是 B+ Tree,按索引列排序;叶子层每一行指向聚簇索引中的对应数据
> - 如果只需要索引中的列(如 `SELECT status WHERE status=1`),就 **无需回表**——这就是 Covering Index
## 回表的代价
```mermaid
sequenceDiagram
participant App as 应用层
participant SI as 二级索引(idx_email)
participant CI as 聚簇索引(PK=id)
participant DB as 数据盘
App->>SI: 查找 email='alice@test.com'
SI-->>App: 找到 → id=12345
App->>CI: 用 id=12345 查找
CI->>DB: 定位叶子页 (随机 IO #1)
DB-->>CI: 返回完整行
CI-->>App: 返回完整行
Note over SI,DB: 至少 2 次 IO:1次二级索引 + 1次回表
```
### 减少回表的策略
```sql
-- ❌ 差:Covering Index 未命中,需要回表拿 name 字段
SELECT name, email FROM users WHERE email = 'alice@test.com';
-- idx_email 能匹配 WHERE,但 name 不在索引里 → 需要回表
-- ✅ 好:覆盖索引,无需回表
ALTER TABLE users ADD INDEX idx_email_name (email, name);
SELECT name, email FROM users WHERE email = 'alice@test.com';
-- EXPLAIN Extra: Using index ← 完美!
```
> [!SUCCESS] Covering Index 的黄金法则
> **把 SELECT 中的列 + WHERE 中的列放到同一个联合索引中**,就能实现 Index Only Scan。
> - 适合:高频查询、固定列选择
> - 不适合:SELECT *(永远无法覆盖)、列变化频繁的查询
## 回表 vs 索引下推(ICP)
MySQL 5.6 引入的 Index Condition Pushdown 优化了部分回表场景。
```sql
-- 假设联合索引 idx_name_status = (name, status)
-- 查询:WHERE name LIKE '张%' AND status = 1
-- ❌ 无 ICP:所有匹配的 name 都要回表查 status
SELECT * FROM users WHERE name LIKE '张%' AND status = 1;
-- 步骤:1) 找到所有 '张%' 的行 2) 逐条回表查 status 3) 过滤
-- ✅ 有 ICP:在二级索引中就先过滤 status
-- 引擎层直接读二级索引页,提取 name 和 status,先判断 status=1
-- 只有满足条件的才回表
-- 减少了大量无效回表
-- EXPLAIN 验证
EXPLAIN SELECT * FROM users WHERE name LIKE '张%' AND status = 1\G
-- Extra 显示: Using index condition
```
```mermaid
flowchart TD
N["无 ICP"] --> A["查到 1000 条 '张%' 的记录"]
A --> B["1000 次回表检查 status"]
B --> C["最终只有 10 条符合"]
Y["有 ICP"] --> D["在索引中预检 status"]
D --> E["1000 条中筛出 10 条"]
E --> F["仅 10 次回表"]
style B fill:#EE5A24,color:#fff
style F fill:#00D866,color:#fff
```
## 两种索引的空间对比
```mermaid
graph TB
subgraph Clustered["聚簇索引 — 100 万行"]
CI["每页约 200 行 | 16KB / 80 bytes"]
CI2["总页数 ≈ 5000 页"]
end
subgraph Secondary["二级索引 — idx_email VARCHAR(255)"]
SI["每页约 80 行 | 16KB / 200 bytes"]
SI2["总页数 ≈ 12500 页"]
end
CI --> CI2
SI --> SI2
style CI2 fill:#00B6BC,color:#fff
style SI2 fill:#C44569,color:#fff
```
> [!NOTE] 二级索引通常比聚簇索引大得多
> 因为二级索引存的是「索引列 + 主键」,而聚簇索引存的是「整行」。如果索引列很长(如 VARCHAR(255)),二级索引会膨胀得很厉害。这也是为什么长字符串列做索引时要限制长度:`(name(50))`。
## 两种索引的对比总结
| 维度 | 聚簇索引 (Clustered) | 二级索引 (Secondary) |
|------|---------------------|---------------------|
| **数量** | 每张表仅一个 | 可以有多个 |
| **叶子节点存储** | 整行数据 | 索引列值 + 主键值 |
| **形成依据** | PK / 唯一非空列 / 隐藏 row_id | `CREATE INDEX` 或 `UNIQUE KEY` |
| **范围查询** | 极快(顺序扫描连续页) | 需要回表,代价高 |
| **覆盖索引** | 天然覆盖(本身就是数据) | 仅当所需列都在索引中时生效 |
| **空间占用** | 基准大小 | 通常更大(多一列主键 + 膨胀风险) |
| **写入代价** | 插入可能触发页分裂 | 更新索引列需改索引 + 回表改数据 |
> [!SUMMARY] 核心记忆点
> 1. 聚簇索引 = 数据本身,**一张表只能有一个**
> 2. 二级索引 = 「索引列 + 主键」,查数据要**回表**
> 3. 能用 Covering Index 的场景永远优于回表
> 4. ICP 是 MySQL 5.6 对回表的温和优化——能省则省
## 关联笔记
- [[hhs/MySQL/16-B+Tree 索引原理]] — B+ Tree 作为底层数据结构的工作原理
- [[hhs/MySQL/18-联合索引与最左前缀]] — 联合索引的搜索模式对回表的影响
- [[hhs/GORM/15-性能优化]] — GORM 场景下的 Covering Index 实践