7.5 KiB
7.5 KiB
tags, create time
| tags | 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。索引中的数据已经按照这个顺序排好了。
最左前缀法则详解
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%')后,右侧列的索引失效。
-- 假设已有联合索引 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'。
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
实战:调整联合索引的顺序
-- 场景:订单表常用查询——按状态筛选 + 按时间范围 + 按类型过滤
-- ❌ 原始索引:范围列在中间,后续等值列无法使用
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] 联合索引列序黄金法则
- 等值优先于范围:等值列放在前面
- 选择性高的列靠前:区分度大的列(如 user_id)比区分度小的(如 gender)靠前
- 前缀长度要短:如果需要 LIKE,尽量用较短的前缀
索引失效的典型场景
-- ❌ 场景 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 三种需求。
-- 建索引: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-查询改写技巧 — 将失效的查询改写为可利用索引的形式