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
@@ -132,7 +132,31 @@ sequenceDiagram
### 覆盖索引(Covering Index)
当查询需要的数据全部存在于某个索引的 B+ Tree 中时,就不需要再走聚簇索引了——这叫 **Index Only Scan**,在 `EXPLAIN` 中显示为 `Using 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 (
@@ -145,16 +169,39 @@ CREATE TABLE users (
-- ❌ 必须回表:email 在索引里,但 name 不在
SELECT name, email FROM users WHERE email = 'test@example.com';
-- ① 走 idx_email 找到 id → ② 回聚簇索引拿 name
-- ① 走 idx_email 找到 id → ② 回聚簇索引拿 name(随机 IO)
-- ✅ 覆盖索引:不需要回表!
SELECT email FROM users WHERE email = 'test@example.com';
-- 只需从 idx_email 叶子节点直接拿到 email
-- 只需从 idx_email 叶子节点直接拿到 email,零回表
```
> [!NOTE] Covering Index 的判断方法
> - `Extra = Using index`(无 `Using where`)→ 完整覆盖,零回表
> - `Extra = Using index condition` → 索引下推(ICP),部分过滤在索引层面完成
#### 用联合索引构造覆盖索引
实际业务中,单列索引很难做到覆盖——查询通常需要多个字段。更常见的做法是**通过联合索引把查询涉及的列"包"进去**:
```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-聚簇索引与二级索引]]