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/18-联合索引与最左前缀.md
T
2026-05-17 00:06:11 +08:00

198 lines
7.5 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, 联合索引, 最左前缀, 索引失效]
create time: 2026-05-16 00:00
---
# 联合索引与最左前缀
## 概述
联合索引(Composite Index)是将多个列放在同一个 B+ Tree 中组织的索引。它的核心法则是**最左前缀匹配原则**——理解这一点就能避开 80% 的索引设计失误。
## 联合索引的结构
```
联合索引 idx(a, b, c) 的 B+ Tree 结构:
叶子节点按 a 排序,a 相同时按 b 排序,a 和 b 都相同时按 c 排序:
[a=1, b=10, c=3] → data
[a=1, b=10, c=7] → data
[a=1, b=20, c=1] → data
[a=1, b=20, c=9] → data
[a=2, b=5, c=2] → data
[a=2, b=15, c=4] → data
[a=3, b=10, c=1] → data
[a=3, b=10, c=8] → data
[a=3, b=30, c=5] → data
```
> [!QUESTION] 这意味着什么?
> 联合索引的本质是**多级排序**。你可以把它想象成 SQL 的 `ORDER BY a, b, c`。索引中的数据已经按照这个顺序排好了。
## 最左前缀法则详解
```mermaid
flowchart TD
IDX["联合索引 idx(a, b, c)"]
IDX --> U1["✅ WHERE a=1"]
IDX --> U2["✅ WHERE a=1 AND b=2"]
IDX --> U3["✅ WHERE a=1 AND b=2 AND c=3"]
IDX --> X1["❌ WHERE b=2"]
IDX --> X2["❌ WHERE c=3"]
IDX --> X3["❌ WHERE b=2 AND c=3"]
IDX --> U4["⚠️ WHERE a=1 AND c=3"]
U1 -. "用 1 列" .-> _u1
U2 -. "用 2 列" .-> _u2
U3 -. "用全部" .-> _u3
X1 -. "无效" .-> _x1
X2 -. "无效" .-> _x2
X3 -. "无效" .-> _x3
U4 -. "仅用 a" .-> _u4
style U1 fill:#00D866,color:#fff
style U2 fill:#00D866,color:#fff
style U3 fill:#00D866,color:#fff
style X1 fill:#EE5A24,color:#fff
style X2 fill:#EE5A24,color:#fff
style X3 fill:#EE5A24,color:#fff
style U4 fill:#FF9F43,color:#000
```
### 为什么 b=2 单独查不了?
```
查询 WHERE b=2 时,B+ Tree 的搜索路径是什么样的?
[b=20, c=1] 的位置在 [b=10, c=7] 之后,但它们之间穿插着 [b=5, c=2]...
数据在整个树中分散存放,没有连续的 b=2 区域 → 需要全树扫描
而 WHERE a=1 则不同:
[a=1, ...] 的所有记录都在树的左侧一段连续区域内 → 二分查找即可定位
```
## 范围查询后的断裂
联合索引遇到范围查询(`>`, `<`, `BETWEEN`, `LIKE 'prefix%'`)后,右侧列的索引失效。
```sql
-- 假设已有联合索引 idx(status, created_at, type)
-- ⚠️ 看起来像用上了 3 个列,实际上 type 不会走索引!
SELECT * FROM orders WHERE status = 'paid'
AND created_at > '2026-01-01' AND type = 'online';
-- 逐列分析:
-- status = 'paid' → ✅ 等值匹配,精确定位起始行
-- created_at > ... → ✅ 范围扫描,划定区间终点
-- type = 'online' → ❌ 区间内数据未按 type 排序 → 退化为内存过滤
```
> [!QUESTION] 为什么范围查询会打断后续列?
> 想象一下,当 `status='paid'` 锁定一个子树后,该子树内部按 `created_at` 升序排列。一旦用 `>` 做了范围扫描,得到的是一个 `created_at` 连续递增的数据段——这个段内的 `type` 值是乱序穿插的,无法再用二分查找定位 `type='online'`。
```mermaid
graph LR
A["等值列 = 定位起点"] -->|"精确找到起始位置"| B
B["范围列 = 确定终点"] -->|"划定扫描区间"| C
C["右侧列 = 失效"] -->|"区间内无序"| D["退化为 WHERE 过滤"]
style A fill:#00D866,color:#fff
style B fill:#00B6BC,color:#fff
style D fill:#EE5A24,color:#fff
```
### 实战:调整联合索引的顺序
```sql
-- 场景:订单表常用查询——按状态筛选 + 按时间范围 + 按类型过滤
-- ❌ 原始索引:范围列在中间,后续等值列无法使用
CREATE INDEX idx_sct ON orders (status, created_at, type);
-- 查询 A: WHERE status=? AND created_at>? AND type=?
-- → type 无法用索引(created_at 的范围扫描打断了后续列)
-- ✅ 优化方案 1:将等值列提前
CREATE INDEX idx_stc ON orders (status, type, created_at);
-- 查询 B: WHERE status=? AND type=? AND created_at>?
-- → 三个列全部用上!status 定位起点,type 进一步缩小,created_at 做范围
-- 💡 注意:即使 SQL 写的是 created_at 在前,优化器也会自动调整顺序匹配索引
-- ✅ 优化方案 2:如果两个查询频率差不多,拆成两个索引
CREATE INDEX idx_status ON orders (status);
CREATE INDEX idx_status_time ON orders (status, created_at);
-- 让每个索引专注于它的典型查询模式
```
> [!TIP] 联合索引列序黄金法则
> 1. **等值优先于范围**:等值列放在前面
> 2. **选择性高的列靠前**:区分度大的列(如 user_id)比区分度小的(如 gender)靠前
> 3. **前缀长度要短**:如果需要 LIKE,尽量用较短的前缀
## 索引失效的典型场景
```sql
-- ❌ 场景 1:隐式类型转换
ALTER TABLE users ADD INDEX idx_phone (phone);
-- phone 是 VARCHAR 类型
SELECT * FROM users WHERE phone = 13800138000; -- 传入 INT!
-- MySQL 会把 varchar 转成 int 再比较 → 函数作用于列 → 索引失效
-- ❌ 场景 2:函数/表达式包裹索引列
SELECT * FROM users WHERE YEAR(created_at) = 2026;
-- 应对:改为范围查询 WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01'
-- ❌ 场景 3:LIKE 以通配符开头
SELECT * FROM users WHERE name LIKE '%abc%';
-- 应对:考虑全文索引 FT
-- ❌ 场景 4:OR 条件中有列没有索引
SELECT * FROM users WHERE email = 'a@x.com' OR phone = '138...';
-- 如果 email 有索引但 phone 没有,整个查询不走索引
-- 应对:给 phone 加索引,或用 UNION ALL 拆分
-- ❌ 场景 5:字符集不一致导致隐式转换
-- 表 charset=utf8mb4,客户端 charset=gbk → 自动转换
-- 应对:确保连接字符集一致 SET NAMES utf8mb4
```
## 最左前缀的灵活应用
一个设计良好的联合索引可以**同时服务 WHERE、ORDER BY、GROUP BY 三种需求**。
```sql
-- 建索引:idx(status, type, created_at)
CREATE INDEX idx_status_type_time ON orders (status, type, created_at);
-- ── 查询 1:等值 + 范围 ──
SELECT * FROM orders WHERE status = 'pending'
AND created_at > '2026-01-01';
-- → status 等值定位起点,created_at 范围扫描。type 虽在中间但没用到,不影响前两列生效
-- ── 查询 2:利用 ORDER BY ──
SELECT * FROM orders WHERE status = 'pending'
ORDER BY type ASC;
-- → WHERE 条件锁定了 status='pending' 的子区间,该区间内数据天然按 type 排序
-- → 无需额外 filesort!
-- ── 查询 3:利用 GROUP BY ──
SELECT type, COUNT(*) FROM orders WHERE status = 'pending'
GROUP BY type;
-- → 同上,子区间内 type 已有序,分组可以直接跳过
```
> [!NOTE] 关键理解
> ORDER BY / GROUP BY 能复用联合索引,靠的不是"巧合",而是 B+ Tree **叶子节点本身有序**这一物理特性。只要 WHERE 过滤条件匹配了联合索引的左侧列,剩余列就是有序的——优化器只是利用了已有的顺序,并没有多做一次排序。
> [!TIP] 一索引多用
> 一个好的联合索引可以同时服务 WHERE、ORDER BY、GROUP BY 三种需求。在设计索引时要考虑查询的整体模式,而不是单一查询。
## 关联笔记
- [[hhs/MySQL/17-聚簇索引与二级索引]] — 聚簇索引本身就是特殊的联合索引 (PK)
- [[hhs/MySQL/19-EXPLAIN 完全指南]] — 用 EXPLAIN 验证联合索引是否按预期生效
- [[hhs/MySQL/20-慢查询日志分析]] — 如何从 slow log 中识别索引未命中
- [[hhs/MySQL/21-查询改写技巧]] — 将失效的查询改写为可利用索引的形式