1. MySQL索引失效的底层原理与核心机制
索引失效的本质是查询优化器认为全表扫描比使用索引更高效。要理解这一点,需要先了解MySQL的查询执行流程:
- 解析SQL语句,生成语法树
- 查询优化器评估各种执行计划的成本
- 选择成本最低的执行计划
- 执行引擎按照选定的计划获取数据
成本估算主要考虑以下因素:
- 需要扫描的数据页数量
- 是否需要回表操作
- 内存中缓冲池的命中率
- 临时表的使用情况
当出现以下情况时,优化器会倾向于放弃使用索引:
- 需要回表的记录数超过表总行数的30%
- 索引的选择性太低(不同值太少)
- 查询需要访问的列大部分不在索引中
注意:即使SQL写法正确,数据分布的变化也可能导致原本有效的索引突然失效。这就是为什么需要定期分析表(ANALYZE TABLE)来更新统计信息。
2. 10种典型索引失效场景深度解析
2.1 违反最左前缀原则
联合索引(a,b,c)的实际存储结构是按a排序,在a相同的情况下按b排序,以此类推。因此:
有效查询:
sql复制WHERE a=1 AND b=2
WHERE a=1 AND b>2
WHERE a=1 ORDER BY b
失效查询:
sql复制WHERE b=2 /* 缺少a条件 */
WHERE a=1 AND c=3 /* 跳过b */
实战技巧:设计联合索引时,将区分度高的列放在左边。可以通过
SELECT COUNT(DISTINCT column)/COUNT(*)计算区分度。
2.2 在索引列上使用函数或运算
sql复制-- 失效
WHERE DATE(create_time) = '2023-01-01'
WHERE amount + 100 > 500
-- 优化后
WHERE create_time >= '2023-01-01' AND create_time < '2023-01-02'
WHERE amount > 400
原理:B+树索引存储的是列原始值,对列计算后无法使用索引的有序性。
2.3 隐式类型转换
常见陷阱:
sql复制-- user_id是varchar类型但传入数字
WHERE user_id = 123 /* 失效 */
WHERE user_id = '123' /* 有效 */
-- 反过来也一样
-- age是int类型但传入字符串
WHERE age = '25' /* 失效 */
WHERE age = 25 /* 有效 */
原理:类型转换相当于对列使用了CAST函数,导致索引失效。
2.4 使用LIKE通配符开头
sql复制-- 失效
WHERE name LIKE '%张%'
WHERE name LIKE '%三'
-- 有效
WHERE name LIKE '张%'
特殊场景:对于固定长度的CHAR类型,即使使用前导通配符,在某些情况下仍可能使用索引。
2.5 OR条件使用不当
sql复制-- 失效(其中一个条件无索引)
WHERE age = 25 OR address = '北京'
-- 优化方案1:使用UNION ALL
SELECT * FROM users WHERE age = 25
UNION ALL
SELECT * FROM users WHERE address = '北京' AND age != 25
-- 优化方案2:建立复合索引
ALTER TABLE users ADD INDEX idx_age_address(age, address);
2.6 使用NOT、!=、<>操作符
sql复制-- 失效
WHERE status != 1
WHERE NOT EXISTS(...)
-- 优化方案
WHERE status IN (0,2,3) /* 明确列出所有可能值 */
2.7 索引列参与IS NULL判断
sql复制-- 可能失效(取决于数据分布)
WHERE mobile IS NULL
-- 优化方案
/* 如果NULL记录很少,可以考虑 */
ALTER TABLE users ADD INDEX idx_mobile(mobile);
/* 然后使用 */
WHERE mobile IS NULL
2.8 IN列表值过多
sql复制-- 当值超过一定数量(通常1000+)时会失效
WHERE id IN (1,2,3,...,1001)
-- 优化方案
/* 分批查询或使用临时表关联 */
2.9 使用ORDER BY导致filesort
sql复制-- 失效(未使用索引排序)
SELECT * FROM users ORDER BY create_time DESC
-- 优化方案1
ALTER TABLE users ADD INDEX idx_create_time(create_time)
-- 优化方案2
/* 如果必须select *,可以尝试 */
SELECT * FROM users FORCE INDEX(PRIMARY) ORDER BY create_time DESC
2.10 数据分布不均匀
当索引列的值分布非常不均匀时(如90%都是同一个值),优化器可能选择全表扫描。
解决方案:
sql复制ANALYZE TABLE users; /* 更新统计信息 */
3. 高级场景与疑难问题排查
3.1 索引合并(Index Merge)的陷阱
MySQL有时会使用多个索引然后合并结果,但这种优化可能适得其反:
sql复制-- 可能比全表扫描更慢
EXPLAIN SELECT * FROM users
WHERE name LIKE '张%' OR age > 30;
解决方案:
sql复制-- 使用UNION ALL替代
SELECT * FROM users WHERE name LIKE '张%'
UNION ALL
SELECT * FROM users WHERE age > 30 AND name NOT LIKE '张%';
3.2 子查询优化
sql复制-- 失效
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE age > 30)
-- 优化为JOIN
SELECT o.* FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.age > 30
3.3 分页查询优化
典型问题:
sql复制-- 越往后越慢
SELECT * FROM articles ORDER BY id LIMIT 100000, 20
优化方案:
sql复制-- 方案1:使用覆盖索引
SELECT id FROM articles ORDER BY id LIMIT 100000, 20
-- 然后二次查询获取完整数据
-- 方案2:记住上一页最后一条记录的ID
SELECT * FROM articles
WHERE id > 100000
ORDER BY id LIMIT 20
4. 实战诊断工具与技巧
4.1 EXPLAIN详解
关键字段解读:
- type:从优到劣 system > const > eq_ref > ref > range > index > ALL
- key:实际使用的索引
- rows:预估需要检查的行数
- Extra:重要提示(Using index, Using filesort等)
4.2 性能分析工具
sql复制-- 开启性能分析
SET profiling = 1;
执行你的SQL;
SHOW PROFILE;
-- 查看索引使用情况
SELECT * FROM sys.schema_index_statistics
WHERE table_schema = 'your_db';
4.3 慢查询日志配置
ini复制# my.cnf配置
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
5. 索引设计与优化原则
- 单表索引数量不宜超过5个
- 优先考虑覆盖索引(包含所有查询字段)
- 字符串索引考虑前缀索引
sql复制ALTER TABLE users ADD INDEX idx_name(name(10)); - 定期使用
OPTIMIZE TABLE整理碎片 - 使用
FORCE INDEX提示时要谨慎
我在实际工作中发现,80%的性能问题都可以通过正确的索引设计解决。但记住:索引不是越多越好,每个额外的索引都会增加写入成本。最好的策略是结合业务查询模式和数据特点,设计最合适的索引方案。
