引言
创建了索引不一定会使用。理解索引失效的场景,才能写出高效的SQL。
一、违反最左前缀原则
1.1 联合索引规则
-- 索引
CREATE INDEX idx_a_b_c ON table(a, b, c);
-- ✅ 使用索引
WHERE a = 1;
WHERE a = 1 AND b = 2;
WHERE a = 1 AND b = 2 AND c = 3;
-- ❌ 不使用索引
WHERE b = 2; -- 缺少a
WHERE c = 3; -- 缺少a和b
WHERE b = 2 AND c = 3; -- 缺少a
1.2 跳跃列
-- 索引
CREATE INDEX idx_a_b_c ON table(a, b, c);
-- ⚠️ 部分使用索引
WHERE a = 1 AND c = 3; -- 只使用a,c失效
二、索引列上使用函数
2.1 函数导致失效
-- ❌ 失效:YEAR函数
SELECT * FROM orders WHERE YEAR(created_at) = 2024;
-- ✅ 改进:使用范围查询
SELECT * FROM orders
WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31 23:59:59';
2.2 常见函数失效案例
-- ❌ 失效
WHERE DATE(created_at) = '2024-11-21';
WHERE LEFT(name, 3) = 'abc';
WHERE UPPER(email) = 'USER@EXAMPLE.COM';
WHERE price * 0.8 > 100;
-- ✅ 改进
WHERE created_at >= '2024-11-21' AND created_at < '2024-11-22';
WHERE name LIKE 'abc%';
WHERE email = 'user@example.com'; -- 应用层转小写
WHERE price > 125; -- 100 / 0.8
三、隐式类型转换
3.1 字符串与数字
-- 表结构
CREATE TABLE users (
id INT,
phone VARCHAR(20) -- 字符串类型
);
CREATE INDEX idx_phone ON users(phone);
-- ❌ 失效:数字查询字符串列
SELECT * FROM users WHERE phone = 13800138000; -- 数字
-- MySQL转换为:WHERE CAST(phone AS SIGNED) = 13800138000
-- 索引失效
-- ✅ 使用索引
SELECT * FROM users WHERE phone = '13800138000'; -- 字符串
3.2 反向情况
-- 表结构
CREATE TABLE users (
id INT,
age INT -- 数字类型
);
CREATE INDEX idx_age ON users(age);
-- ✅ 使用索引:字符串查询数字列
SELECT * FROM users WHERE age = '20';
-- MySQL转换为:WHERE age = CAST('20' AS SIGNED)
-- 索引仍然有效
规则:字符串列查询时必须用字符串类型。
四、LIKE左模糊
4.1 左模糊失效
-- ❌ 失效
WHERE name LIKE '%张三';
WHERE name LIKE '%abc%';
-- ✅ 使用索引
WHERE name LIKE '张三%'; -- 右模糊
WHERE name LIKE 'abc%';
4.2 解决方案
-- 方案1:全文索引
CREATE FULLTEXT INDEX ft_name ON users(name);
SELECT * FROM users WHERE MATCH(name) AGAINST('张三');
-- 方案2:反转字符串(如果需要后缀搜索)
ALTER TABLE users ADD COLUMN name_reverse VARCHAR(50);
UPDATE users SET name_reverse = REVERSE(name);
CREATE INDEX idx_name_reverse ON users(name_reverse);
SELECT * FROM users WHERE name_reverse LIKE REVERSE('张三%');
五、OR条件
5.1 OR导致失效
-- ❌ 可能失效
SELECT * FROM users WHERE name = '张三' OR age = 20;
-- 如果name有索引,age没有索引 → 全表扫描
5.2 改进方案
-- 方案1:都有索引
CREATE INDEX idx_name ON users(name);
CREATE INDEX idx_age ON users(age);
-- OR两边都有索引,会使用index_merge
-- 方案2:改用UNION
SELECT * FROM users WHERE name = '张三'
UNION
SELECT * FROM users WHERE age = 20;
-- 方案3:改用IN(如果是同一列)
SELECT * FROM users WHERE age IN (20, 30, 40);
六、NOT、!=、<>
6.1 负向条件
-- ❌ 可能失效
WHERE status != 'deleted';
WHERE status <> 'deleted';
WHERE NOT status = 'deleted';
WHERE age NOT IN (20, 30);
WHERE name NOT LIKE '%abc%';
6.2 改进
-- 如果状态只有几种,用IN
WHERE status IN ('active', 'pending', 'completed'); -- 排除deleted
-- 或者反向思考
WHERE status = 'active'; -- 直接查询需要的状态
七、IS NULL / IS NOT NULL
7.1 情况复杂
-- ⚠️ 可能失效,取决于NULL比例
WHERE column IS NULL;
WHERE column IS NOT NULL;
规则:
- NULL值很少:IS NULL可能使用索引
- NULL值很多:IS NOT NULL可能使用索引
- 取决于优化器判断
7.2 建议
-- 尽量避免NULL
-- 方案1:设置DEFAULT
ALTER TABLE users ALTER COLUMN phone SET DEFAULT '';
-- 方案2:使用NOT NULL
ALTER TABLE users MODIFY phone VARCHAR(20) NOT NULL DEFAULT '';
八、范围查询后的列失效
8.1 范围后失效
-- 索引
CREATE INDEX idx_a_b_c ON table(a, b, c);
-- ⚠️ 只使用(a, b)
WHERE a = 1 AND b > 10 AND c = 3;
-- b是范围查询,c失效
8.2 优化
-- 方案1:范围列放最后
CREATE INDEX idx_a_c_b ON table(a, c, b);
WHERE a = 1 AND c = 3 AND b > 10; -- 使用(a, c, b)
-- 方案2:改为等值
WHERE a = 1 AND b IN (10, 11, 12, ..., 20) AND c = 3;
九、索引列计算
9.1 计算导致失效
-- ❌ 失效
WHERE id + 1 = 100;
WHERE price * 0.8 > 100;
WHERE CONCAT(first_name, last_name) = '张三';
-- ✅ 改进
WHERE id = 99; -- 100 - 1
WHERE price > 125; -- 100 / 0.8
WHERE first_name = '张' AND last_name = '三'; -- 或使用生成列
十、优化器选择
10.1 索引选择性差
-- 如果筛选出50%以上数据,优化器可能选择全表扫描
SELECT * FROM users WHERE gender = '男'; -- 50%数据
-- 强制使用索引(不推荐)
SELECT * FROM users FORCE INDEX (idx_gender) WHERE gender = '男';
10.2 表数据量小
-- 表只有几百行,全表扫描更快
SELECT * FROM small_table WHERE id = 10;
-- 即使id有索引,也可能全表扫描
十一、实战分析
11.1 使用EXPLAIN验证
EXPLAIN SELECT * FROM users WHERE YEAR(created_at) = 2024;
-- type: ALL(全表扫描)
-- key: NULL(未使用索引)
EXPLAIN SELECT * FROM users
WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31';
-- type: range
-- key: idx_created_at
11.2 索引失效检查清单
-- 1. 检查WHERE条件
-- 2. 检查是否使用函数
-- 3. 检查类型转换
-- 4. 检查LIKE模式
-- 5. 检查OR条件
-- 6. 检查联合索引顺序
十二、总结
索引失效场景汇总
| 场景 | 示例 | 解决方案 |
|---|---|---|
| 违反最左前缀 | WHERE b = 2 | 包含前导列 |
| 使用函数 | YEAR(date) | 使用范围查询 |
| 类型转换 | phone = 123 | 统一类型 |
| 左模糊 | LIKE ‘%abc’ | 右模糊或全文索引 |
| OR条件 | name OR age | 都加索引或UNION |
| 负向条件 | != ‘value’ | 改用IN |
| 索引列计算 | id + 1 = 100 | 右边计算 |
记忆口诀
索引失效要记牢,十大场景要避免,
最左前缀是关键,函数计算索引断。
类型转换要注意,字符串列用引号,
左模糊查全表扫,右模糊才能用。
OR条件要当心,两边都要建索引,
负向条件尽量少,范围查询放最后。
本文字数:约2,600字 难度等级:⭐⭐⭐⭐(索引进阶)