引言

创建了索引不一定会使用。理解索引失效的场景,才能写出高效的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字 难度等级:⭐⭐⭐⭐(索引进阶)