一、先理解索引长什么样
InnoDB 的主键索引是聚簇索引:叶子节点直接存整行数据,所以按主键查最快。二级索引的叶子节点存的是「索引列 + 主键值」,查到主键后还要回表再取整行。
这解释了两件事:
- 为什么主键要短且有序(每个二级索引都带着主键)。
- 为什么「覆盖索引」快——查询的列都在索引里,不用回表。
二、最左前缀原则
联合索引 (a, b, c) 相当于按 a 排序、a 相同按 b 排、再按 c 排。所以:
| 查询条件 | 能否用索引 |
|---|---|
a = ? | 能 |
a = ? AND b = ? | 能 |
a = ? AND b = ? AND c = ? | 能 |
b = ? | 不能(跳过最左列) |
a = ? AND c = ? | 部分能用(只用到 a) |
a = ? AND b > ? AND c = ? | a、b 能用,c 用不上(范围之后失效) |
最后一行是重点:范围查询之后的列无法继续用于索引定位(但可用于覆盖索引,避免回表)。
三、六个失效场景
1. 在索引列上做运算或函数
-- 失效
WHERE DATE(created_at) = '2026-11-03'
WHERE id + 1 = 100
-- 改写为范围
WHERE created_at >= '2026-11-03 00:00:00' AND created_at < '2026-11-04 00:00:00'
2. 隐式类型转换
-- phone 是 varchar,传数字会触发全表转换
WHERE phone = 13800138000 -- 失效
WHERE phone = '13800138000' -- 正常
反过来,int 列传字符串通常仍能用索引,但 varchar 列传数字几乎必然失效。统一加引号最省事。
3. 前导模糊匹配
WHERE name LIKE '%张' -- 失效
WHERE name LIKE '张%' -- 能用
需要全文检索就用全文索引或搜索引擎,别指望 B+ 树。
4. OR 连接了无索引列
WHERE a = 1 OR b = 2 -- b 无索引 → 整体退化为全表扫描
改成 UNION ALL 两个各自能走索引的查询,往往更快。
5. 联合索引顺序不对
建了 (user_id, status) 却按 status 单独查,用不上。要么调整索引顺序,要么补一个 (status) 的索引。
6. 选择性太低
status 只有 0/1 两个值,索引扫一半数据还不如全表。优化器会主动放弃索引,这是正常行为,不是 bug。低区分度列适合放在联合索引的后面,而不是单独建索引。
四、覆盖索引
-- 索引 (user_id, created_at)
SELECT user_id, created_at FROM orders WHERE user_id = 7;
查询的列全在索引里,Extra 会显示 Using index,不需要回表,速度快一个量级。所以「少用 SELECT *」不只是规范问题,直接影响能否覆盖。
五、怎么验证
EXPLAIN SELECT ...;
重点看三列:type(ALL 是全表扫)、key(实际用了哪个索引)、rows(预估扫描行数)。详细解读见 EXPLAIN 怎么看。
六、索引不是越多越好
- 每个索引都会增加写入成本(INSERT/UPDATE 要维护所有索引)。
- 占用磁盘与内存(缓冲池被索引挤占)。
- 优化器选择变多,可能选错。
建议:单表索引控制在 5 个以内,优先建联合索引覆盖多个查询,定期用 sys.schema_unused_indexes 找从未使用的索引并清理。
七、实操顺序
- 找出慢查询(见 慢查询定位)。
EXPLAIN看是否全表扫、扫描行数多少。- 按上面的清单核对失效原因。
- 调整 SQL 或补索引,再
EXPLAIN验证。 - 上线后观察写入性能是否受影响。
小结
联合索引遵守最左前缀,范围查询之后的列失效;列上做函数运算、隐式类型转换、前导 % 都会失效;低区分度列别单独建索引;用 EXPLAIN 的 type 和 key 验证。