455 lines
18 KiB
Markdown
455 lines
18 KiB
Markdown
---
|
||
tags: [MySQL, B+ Tree, 索引原理, Clustered Index]
|
||
create time: 2026-05-16 00:00
|
||
---
|
||
|
||
# B+ Tree 索引原理
|
||
|
||
## 概述
|
||
|
||
MySQL InnoDB 的索引默认使用 B+ Tree(Balance Plus Tree)。理解它的工作方式是掌握所有索引优化技巧的前提。
|
||
|
||
## 为什么用 B+ Tree?
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
BT["B+ Tree<br/>多路平衡树"] --> W1["胜出的原因"]
|
||
BT2["B Tree<br/>传统平衡树"] --> W2["缺点"]
|
||
HT["Hash<br/>哈希表"] --> W3["缺陷"]
|
||
BST["RBTree<br/>二叉平衡树"] --> W4["不适合磁盘"]
|
||
|
||
W1 -.->|"范围扫描 + 磁盘友好 + 叶子链表串联"| WINNER["✅ B+ Tree 胜出"]
|
||
W2 -.->|"非叶子也存数据 → IO 更多"| REASON
|
||
W3 -.->|"无范围查询能力"| REASON
|
||
W4 -.->|"树太高 IO 频繁"| REASON
|
||
|
||
style WINNER fill:#00D866,color:#fff
|
||
style BT fill:#00D866,color:#fff
|
||
```
|
||
|
||
### 关键差异点
|
||
|
||
| 特性 | B+ Tree | B Tree | Hash | 红黑树 |
|
||
|------|---------|--------|------|--------|
|
||
| **范围查询** | ✅ 叶子节点链表遍历 | ❌ 需回溯祖先 | ❌ | ❌ |
|
||
| **全表扫描** | ✅ 顺序扫描叶子 | ❌ 需要层序遍历 | — | ❌ |
|
||
| **磁盘 IO 次数** | 低(阶数高,树矮) | 高 | O(1) 但仅等值 | 极高 |
|
||
| **插入删除稳定性** | ✅ 分裂/合并均衡 | ✅ | ❌ 缩容重哈希 | ✅ |
|
||
| **查询性能可预测** | ✅ 始终 O(logₘn) | ✅ | ⚠️ 冲突时退化 | ✅ |
|
||
|
||
## B+ Tree 结构
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
ROOT["根节点<br/>键: 15, 30, 45"] --> N1["内部节点<br/>键: 5, 10"]
|
||
ROOT --> N2["内部节点<br/>键: 20, 25"]
|
||
ROOT --> N3["内部节点<br/>键: 35, 40"]
|
||
ROOT --> N4["内部节点<br/>键: 50, 55"]
|
||
|
||
N1 --> LEAF1["[1][3][5][8][10]<br/>叶子 · 存数据 + 双向链表"]
|
||
N2 --> LEAF2["[12][15][20][22][25]"]
|
||
N3 --> LEAF3["[28][32][35][40][45]"]
|
||
N4 --> LEAF4["[48][50][52][55][60]"]
|
||
|
||
LEAF1 -.-> LEAF2 -.-> LEAF3 -.-> LEAF4
|
||
|
||
style ROOT fill:#4FC08D,color:#fff
|
||
style N1 fill:#A0AEC0,color:#fff
|
||
style N2 fill:#A0AEC0,color:#fff
|
||
style N3 fill:#A0AEC0,color:#fff
|
||
style N4 fill:#A0AEC0,color:#fff
|
||
style LEAF1 fill:#00B6BC,color:#fff
|
||
style LEAF2 fill:#00B6BC,color:#fff
|
||
style LEAF3 fill:#00B6BC,color:#fff
|
||
style LEAF4 fill:#00B6BC,color:#fff
|
||
```
|
||
|
||
### 核心特征
|
||
|
||
1. **非叶子节点只存储键(Key)和指针**,不存储完整数据行 —— 一个页可以放更多键,树更矮
|
||
2. **所有数据存储在叶子节点**,形成有序链表 —— 支持范围扫描
|
||
3. **叶子节点之间通过双向链表连接** —— 相邻页面不用回溯父节点
|
||
4. **所有叶子节点在同一深度** —— 查询性能稳定
|
||
|
||
## 为什么树这么矮?
|
||
|
||
InnoDB 一页 16KB(可配置),扣除页头 / 页尾等元数据开销后,**实际可用空间约 14~15 KB**。假设:
|
||
- 主键 BIGINT = 8 bytes
|
||
- 指针(Page Pointer)= 6 bytes
|
||
- 每个内部节点 Entry(Key + Pointer + 冗余信息)≈ **16 bytes**
|
||
- 一页可存 ≈ 14.5 KB / 16 bytes ≈ **900 ~ 1170 个子节点**(取保守值 1170)
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
D1["1层<br/>1,170 行"] --> D2["2层<br/>约 137 万行"]
|
||
D2 --> D3["3层<br/>约 16 亿行"]
|
||
D3 --> D4["4层<br/>约 1900 亿行"]
|
||
|
||
style D1 fill:#A0AEC0,color:#fff
|
||
style D2 fill:#FF9F43,color:#000
|
||
style D3 fill:#00D866,color:#fff
|
||
style D4 fill:#EE5A24,color:#fff
|
||
```
|
||
|
||
> [!TIP] 这意味着什么?
|
||
> 即使是一张有 1 亿行的表,查找任意一条记录也只需要 **3~4 次磁盘 IO**(每次读取一个页)。这就是 B+ Tree 在磁盘介质上无可替代的原因。
|
||
|
||
> [!QUESTION] 思考一下
|
||
> 如果每个内部节点只存一个键,那这棵树会变成什么样子?
|
||
> ——退化成一棵二叉树(红黑树的高度)。所以 B+ Tree 的**阶数越高,树越矮,IO 越少**。
|
||
>
|
||
> [!TIP] 直观感受
|
||
> 通过 `information_schema.statistics` 查看索引基数,关注 CARDINALITY(基数)——
|
||
> 基数接近总行数说明区分度高,索引效果好:
|
||
> ```sql
|
||
> -- 查看表的索引基数统计
|
||
> SELECT INDEX_NAME, NON_UNIQUE, CARDINALITY
|
||
> FROM information_schema.statistics
|
||
> WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users';
|
||
> ```
|
||
|
||
## 索引查找过程
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Q as "查询"
|
||
participant R as "根节点"
|
||
participant I as "内部节点"
|
||
participant L as "叶子节点"
|
||
participant D as "数据行"
|
||
|
||
Q->>R: "id > 30 → 右分支"
|
||
R->>I: "定位到第二层右子节点"
|
||
I->>L: "命中对应叶子节点"
|
||
L->>D: "读取完整行数据"
|
||
|
||
Note over Q,D:"总共 3 次随机 IO"
|
||
```
|
||
|
||
> [!QUESTION] 思考一下
|
||
> 如果二级索引也存整行数据,还需要回表吗?
|
||
> ——不需要了。这就是**覆盖索引(Covering Index)**的核心思想。
|
||
|
||
### 覆盖索引(Covering Index)
|
||
|
||
什么是**回表**?二级索引的叶子节点只存了**索引列 + 主键**,不存完整行数据。当查询需要的列不在索引中时,MySQL 必须拿着主键再去聚簇索引里找完整行——这个"再跳一次"的过程就叫回表(详见下方 [[#聚簇索引 vs 二级索引]])。每回表一次就是一次**随机 IO**,匹配 1000 行就是 1000 次随机跳转,代价非常高。
|
||
|
||
**覆盖索引的核心思想**:如果查询所需的**所有列**都存在于某个索引的叶子节点中,就省掉回表这一步——直接从二级索引中把数据读完。这在 `EXPLAIN` 中显示为 `Using index`,也叫 **Index Only Scan**。
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph "回表路径(普通查询)"
|
||
A1["二级索引<br/>叶子节点"] -->|"拿到主键 id"| A2["聚簇索引<br/>再次定位"]
|
||
A2 -->|"随机 IO"| A3["完整行数据"]
|
||
end
|
||
|
||
subgraph "覆盖索引路径(Index Only Scan)"
|
||
B1["二级索引<br/>叶子节点"] -->|"所有列都在这里"| B2["直接返回"]
|
||
end
|
||
|
||
style A1 fill:#FF9F43,color:#000
|
||
style A2 fill:#EE5A24,color:#fff
|
||
style A3 fill:#FF9F43,color:#000
|
||
style B1 fill:#00D866,color:#fff
|
||
style B2 fill:#00D866,color:#fff
|
||
```
|
||
|
||
> [!QUESTION] 为什么避免回表这么重要?
|
||
> 回表是**随机 IO**,而顺序 IO 和随机 IO 的性能差距是 **百倍到千倍** 级别(机械磁盘尤其明显,SSD 也有 10~50 倍差距)。
|
||
> 当匹配行数较多时,覆盖索引的收益会非常显著。
|
||
|
||
```sql
|
||
CREATE TABLE users (
|
||
id BIGINT PRIMARY KEY,
|
||
email VARCHAR(255),
|
||
name VARCHAR(100),
|
||
age INT,
|
||
INDEX idx_email(email)
|
||
);
|
||
|
||
-- ❌ 必须回表:email 在索引里,但 name 不在
|
||
SELECT name, email FROM users WHERE email = 'test@example.com';
|
||
-- ① 走 idx_email 找到 id → ② 回聚簇索引拿 name(随机 IO)
|
||
|
||
-- ✅ 覆盖索引:不需要回表!
|
||
SELECT email FROM users WHERE email = 'test@example.com';
|
||
-- 只需从 idx_email 叶子节点直接拿到 email,零回表
|
||
```
|
||
|
||
#### 用联合索引构造覆盖索引
|
||
|
||
实际业务中,单列索引很难做到覆盖——查询通常需要多个字段。更常见的做法是**通过联合索引把查询涉及的列"包"进去**:
|
||
|
||
```sql
|
||
-- 高频查询:根据 user_id 查该用户已完成的订单金额
|
||
SELECT order_id, amount
|
||
FROM orders
|
||
WHERE user_id = 100 AND status = 'paid';
|
||
|
||
-- 方案 A:索引只有 (user_id) → 必须回表拿 status、order_id、amount
|
||
-- 方案 B:联合索引 (user_id, status, order_id, amount)
|
||
-- 叶子节点存了全部所需列 → 零回表 ✅
|
||
ALTER TABLE orders ADD INDEX idx_user_cover(user_id, status, order_id, amount);
|
||
```
|
||
|
||
> [!NOTE] 设计覆盖索引的思路
|
||
> 1. **先看高频查询**:`SELECT` 了哪些列?`WHERE` 和 `ORDER BY` 涉及哪些列?
|
||
> 2. **把 WHERE + ORDER BY + SELECT 涉及的列**按合理顺序放入联合索引
|
||
> 3. 等值列在前、范围列在后(最左前缀原则),`SELECT` 中无法参与过滤的列放最后
|
||
>
|
||
> 代价是索引更宽、占用更多空间、写入维护成本更高——需要权衡。
|
||
|
||
> [!NOTE] EXPLAIN 中怎么判断覆盖索引?
|
||
> - `Extra = Using index` → 完整覆盖,零回表
|
||
> - `Extra = Using index condition` → 索引下推(ICP),部分过滤在索引层面完成,但仍需回表
|
||
> - Extra 中**没有** `Using index` → 触发了回表
|
||
>
|
||
> 💡 更多细节(回表代价、ICP 原理、空间对比)详见 [[hhs/MySQL/03-索引与查询优化/13-聚簇索引与二级索引]]
|
||
|
||
## 聚簇索引 vs 二级索引
|
||
|
||
这是本节最重要的概念延伸,也是理解后续所有索引优化的基础。
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
subgraph "聚簇索引 = 数据本身"
|
||
C1["id=1 · 整行数据"]
|
||
C2["id=2 · 整行数据"]
|
||
C3["id=3 · 整行数据"]
|
||
end
|
||
|
||
subgraph "二级索引 idx_email"
|
||
S1["email='a' → id=1"]
|
||
S2["email='b' → id=2"]
|
||
S3["email='c' → id=3"]
|
||
end
|
||
|
||
S1 -.回表.-> C1
|
||
S2 -.回表.-> C2
|
||
S3 -.回表.-> C3
|
||
|
||
style C1 fill:#00D866,color:#fff
|
||
style C2 fill:#00D866,color:#fff
|
||
style C3 fill:#00D866,color:#fff
|
||
style S1 fill:#FF9F43,color:#000
|
||
style S2 fill:#FF9F43,color:#000
|
||
style S3 fill:#FF9F43,color:#000
|
||
```
|
||
|
||
> [!QUESTION] 什么是回表?
|
||
> 当用二级索引 `idx_email` 查 `WHERE email = 'a'` 时:
|
||
> 1. 先在二级索引中找到 `email='a'` → 得到主键 `id=1`
|
||
> 2. 再用 `id=1` 去聚簇索引中查找完整行数据
|
||
> 这个两步过程叫**回表(Table Lookup)**。
|
||
>
|
||
> **优化方向**:让查询条件直接走聚簇索引,或者用 Covering Index 避免回表。
|
||
|
||
## MySQL 索引类型总览
|
||
|
||
在 B+ Tree 的基础上,InnoDB 支持多种索引类型。理解它们的选择时机也是面试和实战的常考点。
|
||
|
||
| 索引类型 | 底层结构 | 适用场景 | 等值查找 | 范围查找 | 是否有序 |
|
||
|----------|----------|----------|----------|----------|----------|
|
||
| **聚簇索引 (Clustered)** | B+ Tree 叶子存整行数据 | 主键查询(默认自带) | ✅ O(log n) | ✅ 顺序扫描叶子 | ✅ |
|
||
| **二级索引 (Secondary)** | B+ Tree 叶子存 `索引列 + 主键` | 非主键字段查询 | ✅ O(log n) | ✅ | ✅ |
|
||
| **联合索引 (Composite)** | 单个 B+ Tree,多列组合排序 | 前缀匹配的多字段过滤 | ✅ | ✅ | ✅ |
|
||
| **唯一索引 (Unique)** | 基于 B+ Tree 加约束 | 保证列值唯一(如手机号) | ✅ | ✅ | ✅ |
|
||
| **前缀索引 (Prefix)** | B+ Tree 只取字符前 N 位 | 长字符串字段(如 URL) | ✅ | ⚠️ 精度降低 | ✅ |
|
||
| **全文索引 (Fulltext)** | InnoDB 使用倒排索引 | 文本搜索 (`MATCH...AGAINST`) | — | — | ❌ |
|
||
| **空间索引 (Spatial)** | R-Tree | GIS 地理空间数据 | — | — | ❌ |
|
||
|
||
> [!NOTE] 重点记忆
|
||
> 除 **全文索引** 和 **空间索引** 外,其余全部依赖 B+ Tree。所以本文的核心内容覆盖了 InnoDB 90% 以上的日常使用场景。
|
||
|
||
### 联合索引的最左前缀原则
|
||
|
||
联合索引 `(a, b, c)` 的本质是:**先按 a 排序,a 相同时按 b 排序,a、b 都相同时按 c 排序**。
|
||
|
||
```mermaid
|
||
graph TD
|
||
subgraph "B+ Tree 叶子节点中数据的实际存储顺序"
|
||
R1["(10, 'pending')"]
|
||
R2["(10, 'paid')"]
|
||
R3["(10, 'shipped')"]
|
||
R4["(20, 'pending')"]
|
||
R5["(20, 'paid')"]
|
||
R6["(30, 'pending')"]
|
||
end
|
||
|
||
Q1["WHERE user_id = 10<br/>✅ 精确匹配一层"] --> S1["命中: R1, R2, R3"]
|
||
Q2["WHERE user_id = 10 AND status = 'paid'<br/>✅ 精确匹配两层"] --> S2["命中: R2"]
|
||
Q3["WHERE status = 'paid'<br/>❌ 缺少最左列 user_id"] --> S3["全表扫描"]
|
||
Q4["WHERE user_id = 10 AND created_at > ...<br/>⚠️ 只能用到 user_id"] --> S4["R1,R2,R3 + 逐行过滤"]
|
||
|
||
style S1 fill:#00D866,color:#fff
|
||
style S2 fill:#00D866,color:#fff
|
||
style S3 fill:#EE5A24,color:#fff
|
||
style S4 fill:#FF9F43,color:#000
|
||
```
|
||
|
||
> [!NOTE] 核心直觉
|
||
> - 联合索引的 B+ Tree **不是按列独立排序**的,而是把所有列拼成一条记录整体排序。
|
||
> - 跳过最左列 → 相当于跳过了目录的第一层,直接翻到第二层找,找不到就退化全表扫描。
|
||
|
||
```sql
|
||
CREATE TABLE orders (
|
||
id BIGINT PRIMARY KEY,
|
||
user_id INT,
|
||
status VARCHAR(20),
|
||
created_at DATETIME,
|
||
INDEX idx_user_status(user_id, status) -- 联合索引
|
||
);
|
||
|
||
-- 以下可以命中索引
|
||
SELECT * FROM orders WHERE user_id = 1; -- ✅ (user_id)
|
||
SELECT * FROM orders WHERE user_id = 1 AND status = 'paid'; -- ✅ (user_id, status)
|
||
|
||
-- 以下无法完全命中联合索引
|
||
SELECT * FROM orders WHERE status = 'paid'; -- ❌ 缺少最左列 user_id
|
||
SELECT * FROM orders WHERE user_id = 1 AND created_at > ...; -- ⚠️ 只能用到 user_id
|
||
```
|
||
|
||
> [!TIP] 设计联合索引的黄金法则
|
||
> 1. **把区分度最高的列放在最左边**——能最大程度缩小搜索范围
|
||
> 2. **等值匹配的列排在范围查询之前**
|
||
> 3. **覆盖最常出现的查询模式**,而不是把所有可能的列堆在一起
|
||
|
||
## 实战常见陷阱:索引为什么不生效?
|
||
|
||
B+ Tree 再优秀,也用不好 SQL。以下是最常见的索引失效场景:
|
||
|
||
### 1. 函数 / 运算包裹了索引列
|
||
|
||
```sql
|
||
-- ❌ 索引失效 —— 每行都要计算 DATE(created_at)
|
||
SELECT * FROM orders WHERE DATE(created_at) = '2025-05-01';
|
||
|
||
-- ✅ 改用范围查询,利用 B+ Tree 的范围扫描能力
|
||
SELECT * FROM orders
|
||
WHERE created_at >= '2025-05-01 00:00:00'
|
||
AND created_at < '2025-05-02 00:00:00';
|
||
```
|
||
|
||
> [!NOTE] 原理
|
||
> B+ Tree 按原始值排序,对 `created_at` 建了索引但查的是 `DATE(created_at)`,相当于换了个"钥匙"去找"锁",树根目录里根本找不到这个新钥匙。
|
||
|
||
### 2. LIKE 以通配符开头
|
||
|
||
```sql
|
||
-- ❌ 走全表扫描
|
||
SELECT * FROM users WHERE name LIKE '%abc%';
|
||
|
||
-- ✅ 前缀匹配仍然走索引
|
||
SELECT * FROM users WHERE name LIKE 'abc%';
|
||
```
|
||
|
||
### 3. OR 条件中有一列无索引
|
||
|
||
```sql
|
||
-- ❌ 即使 id 有索引、email 也有索引,但一旦 email 没索引,整个 OR 就可能不走索引
|
||
SELECT * FROM users WHERE id = 1 OR email = 'test@x.com';
|
||
```
|
||
|
||
### 4. 隐式类型转换
|
||
|
||
```sql
|
||
-- 假设 phone 是 VARCHAR 类型并建有索引
|
||
-- ❌ 传入的是数字类型,MySQL 要对每行做 CAST(phone AS SIGNED)
|
||
SELECT * FROM users WHERE phone = 13800138000;
|
||
|
||
-- ✅ 保持类型一致
|
||
SELECT * FROM users WHERE phone = '13800138000';
|
||
```
|
||
|
||
### 5. 回表太多导致 Optimizer 选择全表扫描
|
||
|
||
```sql
|
||
-- 当二级索引需要回表的行数占总行数很大比例时(通常 > 20%~30%),
|
||
-- Optimizer 会认为走索引反而更慢(频繁随机 IO),主动选择全表扫描。
|
||
-- 这是正常行为,强行用 FORCE INDEX 往往适得其反。
|
||
```
|
||
|
||
> [!TIP] 怎么判断要不要加索引?
|
||
> - 先用 `EXPLAIN` 看执行计划
|
||
> - `type` 从 `ALL → index → range → ref → const` 依次越来越优
|
||
> - 如果已经是 `ref` 或 `range` 且 Extra 没有异常提示,说明索引已经在工作
|
||
|
||
## 索引的设计权衡
|
||
|
||
好索引带来快查询,但也带来慢写入。设计时需要权衡以下代价:
|
||
|
||
### 写入放大(Write Amplification)
|
||
|
||
聚簇索引只需维护一个 B+ Tree,但**每个二级索引都是独立的 B+ Tree**。每插入一行数据:
|
||
|
||
```sql
|
||
INSERT INTO users (id, email, name, age) VALUES (1, 'a@x.com', 'Tom', 25);
|
||
-- InnoDB 需要同时更新:
|
||
-- ① 聚簇索引 idx__PRIMARY → 1 次 IO(顺序插入在末尾)
|
||
-- ② 二级索引 idx_email → 1 次 IO(定位 + 可能的页分裂)
|
||
-- ③ 每多一个二级索引 → 各加 1 次 IO
|
||
-- 总写入成本 = 1(聚簇)+ N(N 个二级索引)
|
||
```
|
||
|
||
> [!NOTE] 直观理解
|
||
> 假设有 5 个二级索引,插入一条记录就要写 **6 棵 B+ Tree**。
|
||
> 删除、更新同理——所有相关索引都要同步修改。这就是为什么**索引越多,写越慢**。
|
||
|
||
### 空间占用
|
||
|
||
```sql
|
||
-- 一张表 100 GB,建了 4 个二级索引:
|
||
-- 聚簇索引 ≈ 100 GB(就是数据本身)
|
||
-- 二级索引 × 4 ≈ 30~50 GB(取决于索引列的宽度)
|
||
-- 总磁盘用量 ≈ 130~150 GB
|
||
```
|
||
|
||
> [!QUESTION] 思考一下
|
||
> 如果你有一张日增百万行的日志表,你会给它建几个二级索引?
|
||
> ——答案通常是:**少而精**。先分析最慢的几个查询,针对性加索引,而不是"以防万一全加上"。
|
||
|
||
### 维护开销
|
||
|
||
| 操作 | 聚簇索引影响 | 二级索引影响 |
|
||
|------|-------------|-------------|
|
||
| **INSERT** | 叶子节点末尾追加(顺序 IO) | 需定位正确位置 + 可能的页分裂(随机 IO) |
|
||
| **UPDATE** | 若主键不变则无影响;变化则删除+重建 | 所有涉及列变化的索引都需要更新 |
|
||
| **DELETE** | 标记删除或合并页 | 同上,且可能有页合并开销 |
|
||
| **页分裂** | 顺序写满一页后可能触发 | 频率更高(写入定位分散 + 热点行密集) |
|
||
|
||
> [!TIP] 经验法则
|
||
> - 写密集型系统(如日志、订单创建),索引数量控制在 **3 个以内**
|
||
> - 读密集型系统(如报表查询),可以放宽到 **5 个左右**
|
||
> - 超过 10 个索引几乎必然影响写入性能,应审慎评估
|
||
|
||
## 最佳实践清单
|
||
|
||
结合上述原理与实战经验,整理一份日常工作中可以直接使用的检查清单:
|
||
|
||
> [!CHECKLIST] 索引设计与审查清单
|
||
>
|
||
> **[建表阶段]**
|
||
> - [ ] 主键选择合理(自增 BIGINT / UUID 替代方案考虑过?)
|
||
> - [ ] 高频查询字段已建索引
|
||
> - [ ] 联合索引区分度最高的列放在最左
|
||
> - [ ] 避免过度索引(写多读少的表控制在 3 个以内)
|
||
>
|
||
> **[上线前]**
|
||
> - [ ] 所有慢查询 `EXPLAIN` 后 type 不是 `ALL`
|
||
> - [ ] 覆盖索引场景用 `Using index` 确认不走回表
|
||
> - [ ] 范围查询列不要和其他等值列反序排列
|
||
> - [ ] VARCHAR 长字符串考虑使用前缀索引
|
||
>
|
||
> **[线上巡检]**
|
||
> - [ ] `SHOW INDEXES` 检查 Cardinality 接近行数
|
||
> - [ ] 无用索引定期清理(使用 pt-duplicate-key-checker 等工具)
|
||
> - [ ] 大表 DDL 使用 `ALGORITHM=INPLACE, LOCK=NONE` 避免锁表
|
||
> - [ ] 监控慢查询日志,按需调整索引策略
|
||
|
||
## 关联笔记
|
||
|
||
- [[hhs/GORM/02-模型定义]] — GORM 创建索引的 struct tag 映射到 B+ Tree
|
||
- [[hhs/MySQL/03-索引与查询优化/13-聚簇索引与二级索引]] — 深入解析聚簇索引的物理存储结构与二级索引的回表优化
|
||
- [[hhs/MySQL/03-索引与查询优化/14-联合索引与最左前缀]] — 联合索引的设计原则与实践
|
||
- [[hhs/MySQL/03-索引与查询优化/15-EXPLAIN 完全指南]] — type=index / range 的物理含义与索引执行计划解读
|