前提:先看执行计划
讨论"索引失效"前,先确认你的判断依据是 EXPLAIN 的输出,而不是感觉。
EXPLAIN SELECT * FROM t_order WHERE user_id = 1001 AND status = 1;
关注三列:type(访问类型)、key(实际使用的索引)、rows(预估扫描行数)。
场景一:联合索引未遵循最左前缀
索引 idx(a, b, c),以下查询无法用到索引:
WHERE b = 1 AND c = 2 -- 跳过 a
改写:调整索引顺序或补上最左列。
场景二:在索引列上做运算
WHERE YEAR(created_at) = 2026 -- 索引失效
WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01' -- 走索引
原则:让函数作用在常量上,而不是列上。
场景三:隐式类型转换
-- phone 是 varchar,传入数字会触发全表转换
WHERE phone = 13800000000
WHERE phone = '13800000000' -- 正确写法
场景四:LIKE 以通配符开头
WHERE name LIKE '%探微%' -- 无法使用索引
WHERE name LIKE '探微%' -- 可以使用索引
改写:前缀模糊场景考虑全文索引或搜索引擎。
场景五:OR 连接非索引列
WHERE user_id = 1 OR remark = 'x' -- remark 无索引,整体退化
改写:拆成 UNION 两部分。
场景六:范围条件之后的列用不上索引
索引 (a, b),查询 WHERE a > 10 AND b = 2。a 的范围查询之后,b 无法用于定位,只能用于覆盖。
改写:若 b 选择性更高,考虑调整索引顺序。
场景七:区分度太低的列单独建索引
性别、状态这类只有几个值的列,单独建索引往往不如全表扫描。
INDEX(status) -- 选择性差,优化器可能直接放弃
INDEX(status, created_at) -- 组合起来才有价值
场景八:N+1 查询伪装成性能问题
有时慢的不是单条 SQL,而是被调用了一千次:
for (Order o : orders) {
detailMapper.selectByOrderId(o.getId()); // 1000 次查询
}
改写:批量查询后在内存分组,把 1001 次压成 2 次。
收尾建议
优化前先测量,优化后要复测。养成用 EXPLAIN 验证的习惯,比记住一堆结论更可靠。
