背景
同事说他的SQL查询很慢,但他明明建了索引。
我过去一看:
SELECT * FROM orders WHERE user_id = 10086 AND status = 1;表有500万数据,user_id和status都有索引,但这条SQL执行要3秒。
用EXPLAIN一看:
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 AND status = 1; +----+-------------+--------+------+---------------+------+---------+------+---------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+------+---------+------+---------+-------------+ | 1 | SIMPLE | orders | ALL | idx_user_id | NULL | NULL | NULL | 5000000 | Using where | +----+-------------+--------+------+---------------+------+---------+------+---------+-------------+type=ALL,全表扫描,索引根本没用上。
为什么?总结了索引失效的常见原因。
一、对索引列做运算或函数
错误示例
-- 对索引列使用函数 SELECT * FROM users WHERE YEAR(create_time) = 2024; -- 对索引列做运算 SELECT * FROM orders WHERE order_id + 1 = 10087;原因
B+树索引存的是列的原始值,你用函数或运算处理后,MySQL没法直接用索引查找。
正确写法
-- 改成范围查询 SELECT * FROM users WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01'; -- 把运算移到右边 SELECT * FROM orders WHERE order_id = 10087 - 1;二、隐式类型转换
错误示例
-- phone是VARCHAR类型 SELECT * FROM users WHERE phone = 13800138000;phone是字符串,但传入的是数字,MySQL会做隐式转换。
EXPLAIN结果
type: ALL key: NULL索引失效了。
原因
MySQL的转换规则是:字符串转数字,而不是数字转字符串。
所以实际执行的是:
SELECT * FROM users WHERE CAST(phone AS SIGNED) = 13800138000;等于对索引列做了函数处理,索引失效。
正确写法
SELECT * FROM users WHERE phone = '13800138000';记住:类型要匹配,字符串就传字符串。
三、LIKE以%开头
错误示例
SELECT * FROM products WHERE name LIKE '%手机%';原因
B+树索引是按顺序排列的,%手机%没法利用索引定位,只能全表扫描。
可以用索引的写法
-- 前缀匹配可以用索引 SELECT * FROM products WHERE name LIKE '手机%';全文搜索怎么办
如果业务需要中间匹配:
- 全文索引:MySQL 5.7+支持中文全文索引
- Elasticsearch:专业的搜索引擎
- 搜索优化:用其他条件先过滤,再LIKE
四、OR条件只有部分有索引
错误示例
-- user_id有索引,remark没索引 SELECT * FROM orders WHERE user_id = 10086 OR remark = '测试';EXPLAIN结果
type: ALL全表扫描。
原因
OR的两个条件,只要有一个没索引,就没法用索引。
正确写法
-- 方案1:给remark也加索引 -- 方案2:改成UNION SELECT * FROM orders WHERE user_id = 10086 UNION SELECT * FROM orders WHERE remark = '测试';五、联合索引没遵循最左前缀
假设有联合索引
CREATE INDEX idx_abc ON orders(a, b, c);能用上索引的查询
WHERE a = 1 WHERE a = 1 AND b = 2 WHERE a = 1 AND b = 2 AND c = 3 WHERE a = 1 AND c = 3 -- 只能用到a用不上索引的查询
WHERE b = 2 -- 没有a,最左前缀断了 WHERE b = 2 AND c = 3 -- 没有a WHERE c = 3 -- 没有a原理
联合索引的B+树是按(a, b, c)顺序排列的,先按a排序,a相同的按b排序,b相同的按c排序。
如果查询不包含a,就没法利用这个排序结构。
六、范围查询后的列失效
联合索引
CREATE INDEX idx_abc ON orders(a, b, c);查询
SELECT * FROM orders WHERE a = 1 AND b > 10 AND c = 3;实际用到的索引
只用到了a和b,c没用上。
原因
范围查询(>、<、BETWEEN、LIKE)会导致后面的列无法使用索引。
因为在b > 10的范围内,c的值不是有序的。
优化建议
把等值查询的列放前面,范围查询的列放后面:
CREATE INDEX idx_acb ON orders(a, c, b);七、NOT IN和NOT EXISTS
示例
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM blacklist);情况
IN通常可以用索引NOT IN某些情况会导致全表扫描
优化
-- 用LEFT JOIN替代 SELECT u.* FROM users u LEFT JOIN blacklist b ON u.id = b.user_id WHERE b.user_id IS NULL;八、使用!=或<>
示例
SELECT * FROM orders WHERE status != 1;情况
不等于查询有时候会导致索引失效,取决于数据分布。
如果status != 1的数据占大多数,MySQL可能认为全表扫描更快。
建议
用EXPLAIN看实际执行计划,如果数据分布合适,可以改成:
SELECT * FROM orders WHERE status IN (0, 2, 3, 4);九、IS NULL的情况
老版本MySQL
IS NULL可能导致索引失效。
MySQL 5.7+
IS NULL可以使用索引:
SELECT * FROM users WHERE phone IS NULL; -- 可以用索引建议
- 尽量不要用NULL,用默认值代替
- 如果必须用NULL,确保MySQL版本较新
十、ORDER BY没用上索引
联合索引
CREATE INDEX idx_abc ON orders(a, b, c);能用索引排序的
ORDER BY a ORDER BY a, b ORDER BY a, b, c ORDER BY a DESC, b DESC, c DESC -- 方向一致不能用索引排序的
ORDER BY b -- 没有a ORDER BY a ASC, b DESC -- 方向不一致 ORDER BY a, c -- 跳过了b排查索引问题的流程
- EXPLAIN看执行计划
type:ALL是全表扫描,ref/range/const是用了索引key:实际使用的索引rows:预估扫描行数
- 看possible_keys和key
possible_keys有值但key是NULL:索引存在但没用上
- 开启optimizer_trace
SET optimizer_trace = 'enabled=on'; SELECT * FROM orders WHERE ...; SELECT * FROM information_schema.optimizer_trace\G
可以看到MySQL为什么选择了某个执行计划。
总结
索引失效的常见原因:
| 原因 | 示例 | 解决方案 |
|---|---|---|
| 对索引列做函数 | WHERE YEAR(date)=2024 | 改成范围查询 |
| 隐式类型转换 | WHERE phone=138xxx(数字) | 类型匹配 |
| LIKE %开头 | WHERE name LIKE '%xxx' | 改前缀或用ES |
| OR部分无索引 | WHERE a=1 OR b=2 | 都加索引或UNION |
| 最左前缀 | 联合索引(a,b,c)查WHERE b=1 | 调整索引或查询 |
| 范围查询后的列 | WHERE a>1 AND b=2 | 调整索引顺序 |
记住:写完SQL先EXPLAIN,养成习惯。