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

7.5 KiB
Raw Blame History

tags, create time
tags create time
MySQL
联合索引
最左前缀
索引失效
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] 联合索引列序黄金法则

  1. 等值优先于范围:等值列放在前面
  2. 选择性高的列靠前:区分度大的列(如 user_id)比区分度小的(如 gender)靠前
  3. 前缀长度要短:如果需要 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 三种需求。在设计索引时要考虑查询的整体模式,而不是单一查询。

关联笔记