MySQL 索引失效场景 ⭐⭐⭐

面试题:什么情况下索引会失效?

核心回答

索引失效是 MySQL 查询优化中的重要知识点,常见场景包括:违反最左前缀原则、使用函数/运算、隐式类型转换、LIKE 左边通配符等。

常见索引失效场景

序号 场景 是否失效 说明
1 违反最左前缀原则 ⚠️ 部分失效 联合索引必须从最左列开始
2 LIKE %开头 ⚠️ 失效 “%abc” 导致全表扫描
3 使用 OR ⚠️ 部分失效 OR 两边都要有索引
4 对索引列运算 ⚠️ 失效 使用函数或算术运算
5 隐式类型转换 ⚠️ 失效 类型不匹配
6 负向查询 ⚠️ 可能失效 !=、NOT IN、IS NOT NULL
7 使用 SELECT * ⚠️ 可能失效 无法使用覆盖索引
8 数据量过小 ⚠️ 不走索引 优化器选择全表扫描

场景详解

1. 违反最左前缀原则

-- 创建联合索引
ALTER TABLE user ADD INDEX idx_name_age_city(name, age, city);

-- ✅ 有效:使用最左前缀
SELECT * FROM user WHERE name = '张三';
SELECT * FROM user WHERE name = '张三' AND age = 18;
SELECT * FROM user WHERE name = '张三' AND age = 18 AND city = '北京';

-- ⚠️ 部分有效:跳过中间列
SELECT * FROM user WHERE name = '张三' AND city = '北京';  -- 只用 name

-- ❌ 失效:违反最左前缀
SELECT * FROM user WHERE age = 18;
SELECT * FROM user WHERE age = 18 AND city = '北京';
SELECT * FROM user WHERE city = '北京';

原理

索引 idx_name_age_city 的 B+ 树结构:

[name=张三, age=18, city=北京] → row1
[name=张三, age=20, city=上海] → row2
[name=李四, age=18, city=北京] → row3

查询 age=18 时:
- 无法快速定位,需要扫描整个索引

2. LIKE 左边通配符

-- 创建索引
ALTER TABLE user ADD INDEX idx_name(name);

-- ✅ 有效:右侧通配符
SELECT * FROM user WHERE name LIKE '张%';    -- range 扫描
SELECT * FROM user WHERE name LIKE '张三%';  -- range 扫描

-- ❌ 失效:左侧通配符
SELECT * FROM user WHERE name LIKE '%三';    -- 全表扫描
SELECT * FROM user WHERE name LIKE '%张三';  -- 全表扫描

-- ❌ 失效:两侧通配符
SELECT * FROM user WHERE name LIKE '%张%';   -- 全表扫描

原理

name 索引的 B+ 树按字典序排列:

张三 → row1
张四 → row2
李四 → row3

"张%":可以定位到"张"开头的区间 → 使用索引
"%三":无法定位区间 → 全表扫描

优化方案

-- 方案1:使用覆盖索引
SELECT name FROM user WHERE name LIKE '%张%';  -- 使用索引

-- 方案2:使用全文索引
ALTER TABLE user ADD FULLTEXT INDEX idx_name_fulltext(name);
SELECT * FROM user WHERE MATCH(name) AGAINST('张三');

-- 方案3:使用 Elasticsearch

3. OR 条件

-- 创建索引
ALTER TABLE user ADD INDEX idx_name(name);
ALTER TABLE user ADD INDEX idx_age(age);

-- ⚠️ 失效:一边有索引,一边没有
SELECT * FROM user WHERE name = '张三' OR age = 18;
-- age 没有索引,导致全表扫描

-- ✅ 有效:两边都有索引(MySQL 5.0+)
SELECT * FROM user WHERE name = '张三' OR age = 18;
-- 可以使用索引合并

-- ❌ 失效:两边都没有索引
SELECT * FROM user WHERE name = '张三' OR phone = '138';

替代方案

-- 使用 UNION 替代 OR
SELECT * FROM user WHERE name = '张三'
UNION
SELECT * FROM user WHERE age = 18;

-- 使用 IN 替代 OR
SELECT * FROM user WHERE name IN ('张三', '李四');

4. 对索引列使用函数或运算

-- 创建索引
ALTER TABLE user ADD INDEX idx_create_time(create_time);

-- ❌ 失效:使用函数
SELECT * FROM user WHERE YEAR(create_time) = 2024;
SELECT * FROM user WHERE DATE_FORMAT(create_time, '%Y') = '2024';
SELECT * FROM user WHERE DAYOFMONTH(create_time) = 15;

-- ❌ 失效:使用运算
SELECT * FROM user WHERE price * 1.1 > 100;
SELECT * FROM user WHERE id + 1 = 10;

-- ✅ 正确:使用范围查询
SELECT * FROM user WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';
SELECT * FROM user WHERE price > 100 / 1.1;

原理

索引按原值排序:
create_time: 2024-01-01, 2024-02-01, 2024-03-01, ...

使用 YEAR(create_time) 时:
- 需要对每一行计算函数值
- 无法使用索引的有序性

5. 隐式类型转换

-- phone 是 VARCHAR 类型
ALTER TABLE user ADD INDEX idx_phone(phone);

-- ⚠️ 失效:整数传给字符串列
SELECT * FROM user WHERE phone = 13800138000;
-- 隐式转换为:WHERE phone = '13800138000'

-- ✅ 正确:使用字符串
SELECT * FROM user WHERE phone = '13800138000';

-- age 是 INT 类型
-- ⚠️ 失效:字符串传给整数列
SELECT * FROM user WHERE age = '18';
-- 字符串转整数:WHERE age = 18
-- 可能导致全表扫描或索引失效

原则:确保查询参数类型与列类型一致

6. 负向查询

-- 创建索引
ALTER TABLE user ADD INDEX idx_status(status);

-- ⚠️ 可能失效:负向查询
SELECT * FROM user WHERE status != 1;
SELECT * FROM user WHERE status <> 1;
SELECT * FROM user WHERE status NOT IN (1, 2);
SELECT * FROM user WHERE status IS NOT NULL;

-- ✅ 优化:使用正向查询
SELECT * FROM user WHERE status IN (2, 3, 4);
SELECT * FROM user WHERE status > 1;

-- 对于 IS NOT NULL
-- 如果列有默认值,MySQL 可能仍使用索引
-- 如果列大量为 NULL,可能全表扫描更好

注意:MySQL 优化器会根据数据分布决定是否使用索引

7. 使用 SELECT *

-- 创建联合索引
ALTER TABLE user ADD INDEX idx_name_age(name, age);

-- ✅ 有效:使用覆盖索引
SELECT name, age FROM user WHERE name = '张三';
-- 索引包含所有需要的数据,无需回表

-- ⚠️ 失效:需要回表
SELECT * FROM user WHERE name = '张三';
-- 虽然能用 name 索引,但需要回表获取其他字段

-- ⚠️ 部分失效:索引列不在 WHERE 中
SELECT name, age FROM user WHERE age = 18;
-- 违反最左前缀,无法使用索引

8. 数据量过小

-- 当数据量很小时,MySQL 优化器可能选择全表扫描
-- 因为索引需要额外的 IO 操作

-- 验证是否使用索引
EXPLAIN SELECT * FROM user WHERE name = '张三';

-- type = ALL 表示全表扫描
-- type = ref 或 range 表示使用索引

复杂场景分析

场景1:多个条件组合

-- 创建联合索引
ALTER TABLE user ADD INDEX idx_a_b_c(a, b, c);

-- ❌ 完全失效
SELECT * FROM user WHERE b = 2;

-- ✅ 使用 a 列
SELECT * FROM user WHERE a = 1 AND b = 2;

-- ⚠️ 使用 a、b 列
SELECT * FROM user WHERE a = 1 AND c = 3;

-- ✅ 完全使用
SELECT * FROM user WHERE a = 1 AND b = 2 AND c = 3;

-- ✅ 使用 a 列,b 范围查询
SELECT * FROM user WHERE a = 1 AND b > 2 AND c = 3;
-- 使用 a, b,c 无法使用(b 是范围)

场景2:索引列参与计算

-- 创建索引
ALTER TABLE orders ADD INDEX idx_amount(amount);

-- ❌ 失效
SELECT * FROM orders WHERE amount + 10 > 100;

-- ✅ 正确
SELECT * FROM orders WHERE amount > 90;

-- ❌ 失效
SELECT * FROM orders WHERE SUBSTRING(name, 1, 2) = '张';

-- ✅ 正确
SELECT * FROM orders WHERE name LIKE '张%';

场景3:字符集不一致

-- 表使用 utf8mb4
-- 查询参数使用 latin1
SELECT * FROM user WHERE name = '张三';  -- 可能失效

-- 解决方案
SET NAMES utf8mb4;
SELECT * FROM user WHERE name = '张三';

EXPLAIN 分析索引使用

-- 查看执行计划
EXPLAIN SELECT * FROM user WHERE name = '张三';

-- 关键字段
-- type: 查询类型(ALL=全表, ref=索引, range=范围)
-- key: 实际使用的索引
-- rows: 扫描行数
-- Extra: 额外信息(Using index=覆盖索引, Using where=条件过滤)

type 值从好到差

system > const > eq_ref > ref > range > index > ALL

高频面试题

Q1: 为什么 LIKE “%abc” 会导致索引失效?

索引按值排序,如:张三、李四、王五

"张三%":可以找到"张"开头的范围
"%三":无法确定范围,必须逐个比较

Q2: OR 一定失效吗?

不一定:
1. MySQL 5.0+ 支持索引合并(Index Merge)
2. OR 两边都有索引时,可能使用索引合并
3. 如果优化器认为全表扫描更快,则不使用索引

Q3: 为什么建议使用覆盖索引?

减少回表操作:
1. 索引树包含所有需要的数据
2. 无需访问主表
3. 减少磁盘 IO

Q4: 如何优化 LIKE “%abc%” 查询?

-- 1. 使用覆盖索引
SELECT id, name FROM articles WHERE name LIKE '%MySQL%';

-- 2. 使用全文索引
ALTER TABLE articles ADD FULLTEXT INDEX idx_name(name);
SELECT * FROM articles WHERE MATCH(name) AGAINST('MySQL');

-- 3. 使用搜索引擎
-- Elasticsearch、MongoDB Atlas Search

最佳实践

-- 1. 创建合理的联合索引
ALTER TABLE orders ADD INDEX idx_user_status_time(user_id, status, create_time);

-- 2. 按需创建索引,考虑查询模式
-- 分析慢查询日志,找出高频查询

-- 3. 使用 EXPLAIN 验证索引
EXPLAIN SELECT * FROM orders WHERE user_id = 1 AND status = 1;

-- 4. 避免过多索引
-- 每个索引都会增加写操作开销

-- 5. 定期分析表
ANALYZE TABLE user;
OPTIMIZE TABLE user;

参考链接: