1. 索引失效现象:为什么明明有索引却依然慢?
最近排查一个线上慢查询时遇到典型场景:一张300万行的用户表,在user_name字段建立了普通索引,但执行SELECT * FROM users WHERE user_name LIKE '%张%'却耗时超过2秒。这引出了我们今天要讨论的核心问题——索引失效的现场诊断。
索引失效的本质是数据库优化器(Optimizer)认为全表扫描比使用索引更高效。这种判断可能源于查询写法、数据特征或索引设计问题。当出现以下五种情况时,即使存在索引,MySQL也可能选择"无视"它:
关键认知:索引不是银弹。它的有效性取决于查询模式与数据分布的匹配程度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 五种典型失效场景深度解析
2.1 左模糊匹配:LIKE '%xxx%'
这是最常见的索引杀手。前文提到的LIKE '%张%'查询中:
- B+树索引遵循最左前缀原则,而通配符在前导致无法定位索引起点
- 优化器评估发现需要扫描整个索引树+回表,成本高于直接全表扫描
实测对比:
sql复制-- 失效案例(执行时间1.8s)
EXPLAIN SELECT * FROM articles WHERE content LIKE '%数据库%';
-- 优化方案(执行时间0.02s)
CREATE FULLTEXT INDEX idx_ft_content ON articles(content);
SELECT * FROM articles WHERE MATCH(content) AGAINST('数据库');
2.2 隐式类型转换:字段类型不匹配
当查询条件与字段类型不一致时:
sql复制-- user_id是varchar类型但传入数字(执行时间1.2s)
EXPLAIN SELECT * FROM orders WHERE user_id = 10086;
此时MySQL会:
- 对每行数据执行
CAST(user_id AS SIGNED)转换 - 转换后的值无法使用索引
类型转换矩阵:
| 字段类型 | 传入类型 | 是否失效 | 原因 |
|---|---|---|---|
| INT | STRING | 否 | MySQL会转换字符串为数字 |
| VARCHAR | INT | 是 | 需逐行转换字段值 |
2.3 函数操作:索引列参与计算
在索引列上使用函数会导致全表扫描:
sql复制-- 失效案例(执行时间2.4s)
EXPLAIN SELECT * FROM sales
WHERE DATE_FORMAT(create_time,'%Y-%m') = '2023-01';
-- 优化方案(执行时间0.03s)
EXPLAIN SELECT * FROM sales
WHERE create_time BETWEEN '2023-01-01' AND '2023-01-31';
2.4 OR条件不当使用
当OR条件包含非索引列时:
sql复制-- 假设只有user_id有索引(执行时间1.5s)
EXPLAIN SELECT * FROM logs
WHERE user_id = 1001 OR content LIKE '%error%';
优化器会采用全表扫描而非"索引合并"策略,因为:
content条件需要全表扫描- 合并操作成本高于直接扫描
2.5 复合索引顺序错误
对于复合索引idx_a_b_c(a,b,c):
sql复制-- 有效使用索引
WHERE a=1 AND b=2
WHERE a=1 ORDER BY b
-- 索引失效
WHERE b=2
WHERE a=1 ORDER BY c
最左前缀原则要求查询必须从索引最左列开始使用。
3. 诊断工具与优化方案
3.1 EXPLAIN执行计划解读
重点关注以下字段:
type:ALL表示全表扫描,range/index表示使用索引key:实际使用的索引名称rows:预估扫描行数Extra:Using filesort/Using temporary需要警惕
典型问题模式:
bash复制+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| 1 | SIMPLE | users | ALL | idx_name | NULL | NULL | 284k | Using where |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
3.2 索引优化策略
- 前缀索引:对长字符串使用
INDEX(column(10)) - 覆盖索引:SELECT的字段都包含在索引中
- 索引下推:MySQL 5.6+版本启用
SET optimizer_switch='index_condition_pushdown=on' - 强制索引:极端情况下使用
FORCE INDEX
4. 实战避坑指南
-
模糊查询优化:
- 右模糊
LIKE '张%'可用索引 - 全文检索改用FULLTEXT索引
- 业务允许时使用ES等专业搜索工具
- 右模糊
-
类型转换预防:
java复制// 错误示例 queryWrapper.eq("user_id", 10086); // 正确写法 queryWrapper.eq("user_id", "10086"); -
函数操作改造:
- 将
WHERE YEAR(create_time)=2023改为范围查询 - 计算字段建立冗余列并索引
- 将
-
OR条件拆分:
sql复制-- 优化为UNION ALL SELECT * FROM logs WHERE user_id = 1001 UNION ALL SELECT * FROM logs WHERE content LIKE '%error%' AND user_id != 1001;
5. 性能对比实验
通过sysbench创建测试表:
sql复制CREATE TABLE `index_test` (
`id` int NOT NULL AUTO_INCREMENT,
`code` varchar(20) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_code` (`code`),
KEY `idx_time` (`create_time`)
) ENGINE=InnoDB;
插入100万测试数据后对比:
| 查询场景 | 无索引耗时 | 有索引但失效耗时 | 优化后耗时 |
|---|---|---|---|
| LIKE '%abc%' | 420ms | 410ms | 5ms(FULLTEXT) |
| code=12345(int转varchar) | 380ms | 370ms | 2ms |
| MONTH(create_time)=3 | 350ms | 340ms | 8ms(range) |
最后分享一个排查技巧:当发现索引失效时,使用EXPLAIN FORMAT=JSON可以获取更详细的成本分析数据,其中特别关注optimizer_estimates部分的计算逻辑。
