联合索引的最左前缀原则是什么?哪些情况会导致索引失效?
一句话回答
联合索引 (a, b, c) 先按 a 排序,a 相同再按 b,b 相同再按 c,所以只有从最左边的列开始连续匹配才能用索引定位,单独查 b 或 c 用不上;遇到范围条件后,后面的列不能再用于定位。索引失效的本质是条件没法利用索引的有序性,比如对列做函数或运算、隐式类型转换、LIKE '%x'、OR 的一侧没有索引;!= 这类条件通常是因为匹配的行太多,优化器估算全表扫描更便宜。
详细解析
联合索引是怎么排序的
(a, b, c) 索引中的记录顺序:
(1, 1, 3) (1, 2, 1) (1, 2, 5) (2, 1, 2) (2, 3, 1) (3, 1, 1)
a 整体有序;a 相同时 b 才有序;a、b 都相同时 c 才有序
单看 b:1 2 2 1 3 1,是无序的,没法在索引上二分查找
像查字典:先按第一个字母排,再按第二个字母排。知道第一个字母能很快翻到,只知道第二个字母就只能从头翻到尾。
| 条件 | 能用于定位的列 | 说明 |
|---|---|---|
a = 1 |
a | 最左前缀 |
a = 1 AND b = 2 AND c = 3 |
a、b、c | 全部用上 |
b = 2 AND a = 1 |
a、b | 书写顺序无所谓,优化器会调整 |
b = 2 或 b = 2 AND c = 3 |
无 | 缺少最左边的列 |
a = 1 AND c = 3 |
a | 跳过了 b,c 只能在索引里过滤 |
a = 1 AND b > 2 AND c = 3 |
a、b | b 是范围条件,c 不能再用于定位 |
a IN (1, 2) AND b = 2 |
a、b | IN 相当于多个等值条件 |
a = 1 ORDER BY b |
a,排序不需要 filesort | a 固定时 b 本身有序 |
范围条件包括 >、<、>=、<=、BETWEEN 和 LIKE 'x%'。范围之后的列为什么不能定位:b > 2 匹配的是 b = 3、4、5 等多个 b 值,每个 b 值下面的 c 各自有序,合在一起就无序了。不过 c 的条件也不是完全没用,索引下推 可以在回表之前用它过滤。
索引失效的常见情况
| 写法 | 为什么用不上 | 改写 |
|---|---|---|
WHERE YEAR(create_time) = 2024 |
索引按列的原值排序,函数结果的顺序和它不一致 | create_time >= '2024-01-01' AND create_time < '2025-01-01' |
WHERE price * 2 > 100 |
对列做运算,和函数一样 | price > 50 |
phone 是 VARCHAR,WHERE phone = 13800000000 |
字符串和数字比较时,MySQL 把每行的字符串转成数字再比,相当于对列用了函数 | 写成字符串 '13800000000' |
WHERE name LIKE '%om' |
前导通配符没法确定从索引的哪个位置开始找 | 改成右模糊;后缀匹配可以额外存一个反转的列;全文检索用全文索引或搜索引擎 |
WHERE a = 1 OR d = 2,d 没有索引 |
d 的条件反正要全表扫描,优化器干脆整体全表扫描 | 给 d 也建索引,优化器可以分别查两个索引再合并(index_merge),或改写成 UNION |
还有两种情况常被说成"索引失效",其实是优化器的选择:
- 优化器认为全表扫描更快:比如
status = 1匹配了表里大部分行,走二级索引要回表几十万次随机读,不如直接顺序扫描聚簇索引。没有固定的比例阈值,取决于统计信息和成本估算 !=、NOT IN、IS NOT NULL:它们可以转成范围扫描,并不是一定不能用索引,只是匹配的行通常很多,回表代价高
字符集不一致的关联也容易踩坑:关联字段一边是 utf8mb3、一边是 utf8mb4,MySQL 会把 utf8mb3 那一侧转换成 utf8mb4 再比较,被转换的一侧上的索引就用不上了。
索引设计原则
- 从查询出发:为 WHERE、JOIN、ORDER BY、GROUP BY 中高频出现的列建索引,而不是每列各建一个
- 等值在前,范围在后:
WHERE user_id = ? AND status = ? AND create_time > ?建(user_id, status, create_time);多个等值列时,优先放能被更多查询复用的列,再考虑区分度 - 看区分度:
COUNT(DISTINCT col) / COUNT(*)越接近 1 越好。状态、性别这类区分度低的列单独建索引意义不大,但放在联合索引里配合其他列仍然有用 - 兼顾排序和覆盖:
WHERE user_id = ? ORDER BY create_time DESC LIMIT 20建(user_id, create_time),按索引顺序取到 20 条就停;常查的少量列加进索引可以避免回表 - 控制数量和长度:每个索引都会拖慢写入、占用空间;有了
(a, b)就不需要再单独建(a);长字符串可以用前缀索引INDEX (email(10)),但前缀索引用不了覆盖索引,也不能用于排序
代码示例
-- 对列用了函数,用不上 create_time 上的索引
EXPLAIN SELECT * FROM orders WHERE DATE(create_time) = '2024-06-01';
-- type: ALL
-- 改写成范围条件
EXPLAIN SELECT * FROM orders
WHERE create_time >= '2024-06-01' AND create_time < '2024-06-02';
-- type: range,key: idx_create_time
-- MySQL 8.0.13 起支持函数索引,查询中的表达式要和索引定义一致
ALTER TABLE orders ADD INDEX idx_create_date ((DATE(create_time)));
EXPLAIN SELECT * FROM orders WHERE DATE(create_time) = '2024-06-01';
-- type: ref,key: idx_create_date
面试官可能追问
联合索引 (a, b, c),WHERE a = 1 AND c = 3 用到了几列?怎么看?
只用 a 定位,c 可以借助索引下推,在索引里过滤掉不满足的记录后再回表。可以看 EXPLAIN 的 key_len,它是用于定位的索引列的字节数之和:假设 a、b、c 都是 INT NOT NULL(各 4 字节),key_len 为 4 说明只用了 a,为 12 说明三列都用上了。
不满足最左前缀,就一定不走索引吗?
不一定。如果查询的列都在这个联合索引里,优化器可能选择扫描整个索引(type 为 index):索引比整张表小,扫它比扫全表便宜,但这只是扫得少一点,不是定位。另外 MySQL 8.0.13 引入了 Skip Scan:在 (a, b) 上查 WHERE b = 2,如果 a 的不同值很少,优化器可以对 a 的每个值分别查 b = 2,EXPLAIN 显示 Using index for skip scan。它有使用条件,比如查询只能用到索引中的列。
为什么不给每个列都建一个单列索引?
每多一个索引,写入时就要多维护一棵 B+ 树,占用的磁盘和缓冲池也更多。而且多个单列索引一般只能用上一个:WHERE a = 1 AND b = 2 用 (a, b) 一次就能定位;index_merge 虽然能合并多个索引的结果,但代价往往比一个合适的联合索引高。
怎么判断一个索引建得好不好?
看 EXPLAIN:type 最好能达到 range 及以上,rows 和实际返回的行数接近,Extra 里没有不必要的 Using filesort。上线后可以用 sys.schema_unused_indexes 找出从没用过的索引,用 sys.schema_redundant_indexes 找出冗余索引,定期清理。
易错点
- WHERE 里条件的书写顺序不影响最左前缀,
b = 2 AND a = 1照样能用(a, b) - 隐式类型转换只在字符串列和数字比较时导致失效;INT 列和字符串常量比较,转换的是常量,索引仍然可用
LIKE 'abc%'可以用索引,但它属于范围条件,联合索引中后面的列不能再用于定位!=、IS NULL、IS NOT NULL不是一定不走索引,要以 EXPLAIN 为准
AI 模拟面试官
用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮
这道题你掌握了吗?
选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。
学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。