1. 面试复盘:那些年我们踩过的索引失效坑
上周刚结束狗东的数据库开发岗面试,面试官连环追问"索引失效"的场景让我印象深刻。作为MySQL性能优化的核心知识点,索引失效问题在实际开发中几乎每天都会遇到。今天我就把面试中讨论的10个典型场景整理出来,结合8年MySQL调优经验,从原理到实战帮你彻底搞懂这个高频考点。
索引就像图书馆的目录系统,能帮我们快速定位数据位置。但当索引失效时,数据库就不得不进行全表扫描(好比在图书馆里逐本书翻找),性能会呈指数级下降。根据我的统计,生产环境中约60%的慢查询都与索引失效有关。下面这些场景,有些是新手容易忽略的陷阱,有些甚至是老司机都会翻车的隐蔽情况。
2. 索引失效的10种典型场景
2.1 违反最左匹配原则
这是联合索引最常见的失效场景。假设我们有个商品表建立了(category_id, brand_id, price)的联合索引:
-- 有效索引查询 SELECT * FROM products WHERE category_id = 1 AND brand_id = 5; -- 失效查询(缺少最左字段category_id) SELECT * FROM products WHERE brand_id = 5;原理说明:联合索引的存储结构是按照索引字段顺序构建的B+树。跳过最左字段时,数据库无法利用索引的有序性,就像跳过了字典的字母索引直接按页码查找。
实战建议:
- 设计联合索引时,将区分度高的字段放在左边
- 无法避免时要考虑单独建立索引或使用索引覆盖
2.2 对索引列使用函数或运算
-- 失效案例(使用函数) SELECT * FROM orders WHERE DATE_FORMAT(create_time,'%Y-%m') = '2023-01'; -- 失效案例(使用运算) SELECT * FROM users WHERE age + 1 > 20;优化方案:
-- 改为范围查询 SELECT * FROM orders WHERE create_time >= '2023-01-01' AND create_time < '2023-02-01';2.3 隐式类型转换
当字段类型与查询条件类型不一致时:
-- user_id是varchar类型,但用数字查询 SELECT * FROM users WHERE user_id = 10086; -- 实际执行等价于(导致索引失效) SELECT * FROM users WHERE CAST(user_id AS signed int) = 10086;避坑技巧:
- 使用
EXPLAIN查看执行计划时注意type列 - 出现
ALL或index往往说明索引失效
2.4 使用不等于(!= / <>)查询
-- 全表扫描 SELECT * FROM products WHERE status != 'online';替代方案:
-- 改为IN查询 SELECT * FROM products WHERE status IN ('draft', 'offline', 'deleted');2.5 LIKE以通配符开头
-- 失效查询 SELECT * FROM articles WHERE title LIKE '%优化%'; -- 有效查询(能使用索引) SELECT * FROM articles WHERE title LIKE '性能%';特殊场景处理:
- 必须使用
%xxx%时考虑全文索引 - 数据量大时可使用Elasticsearch等专业搜索工具
2.6 OR条件使用不当
-- 索引失效案例 SELECT * FROM orders WHERE user_id = 1001 OR amount > 1000; -- 优化方案(使用UNION) SELECT * FROM orders WHERE user_id = 1001 UNION ALL SELECT * FROM orders WHERE amount > 1000;2.7 索引列参与IS NULL判断
-- 可能失效(取决于数据分布) SELECT * FROM customers WHERE phone IS NULL;优化建议:
- NULL值较少时可考虑
WHERE phone IS NOT NULL反转查询 - 重要字段建议设置NOT NULL约束并设置默认值
2.8 范围查询后的条件失效
-- 只有category_id和price能用索引,color失效 SELECT * FROM products WHERE category_id = 1 AND price > 100 AND color = 'red';索引设计技巧:
- 将等值查询字段放在联合索引左侧
- 范围查询字段尽量放在右侧
2.9 使用NOT IN条件
-- 全表扫描 SELECT * FROM products WHERE category_id NOT IN (1, 2, 3);替代方案:
-- 使用NOT EXISTS SELECT * FROM products p WHERE NOT EXISTS ( SELECT 1 FROM categories c WHERE c.id IN (1,2,3) AND c.id = p.category_id );2.10 数据量过少时优化器放弃索引
当表中数据量很少(如小于全表10%)时,优化器可能认为全表扫描比索引更快。
应对策略:
- 使用
FORCE INDEX强制使用索引 - 通过
ANALYZE TABLE更新统计信息
3. 诊断索引失效的实用技巧
3.1 EXPLAIN执行计划分析
重点关注以下字段:
type:ALL表示全表扫描key:实际使用的索引rows:预估扫描行数Extra:Using filesort或Using temporary需要警惕
3.2 开启慢查询日志
配置参数:
slow_query_log = 1 slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 1 log_queries_not_using_indexes = 13.3 使用性能分析工具
推荐工具:
- Percona Toolkit的
pt-index-usage - MySQL Enterprise Monitor
- 阿里云的DAS诊断报告
4. 索引设计的最佳实践
三星索引原则:
- 一星:WHERE条件匹配索引列
- 二星:ORDER BY匹配索引列
- 三星:SELECT列被索引覆盖
索引选择策略:
- 高选择性字段优先建索引
- 避免过度索引(每个索引都有维护成本)
- 定期使用
pt-index-usage清理无用索引
联合索引设计口诀:
- 等值查询放左边
- 范围查询放右边
- 排序字段放最后
- 分组字段要前置
5. 真实案例:电商系统索引优化
去年优化过一个日均百万订单的电商系统,通过索引优化将结算页响应时间从2.3秒降到400毫秒。主要措施:
- 将
(user_id, status)联合索引改为(user_id, status, create_time) - 为支付时间字段添加函数索引
(DATE(pay_time)) - 将
ORDER BY create_time DESC改为ORDER BY id DESC(利用主键索引)
优化后效果:
- 索引命中率从65%提升到92%
- 数据库CPU使用率下降40%
- 慢查询数量减少85%
6. 面试加分技巧
当被问到索引失效问题时,可以这样展示深度:
从存储结构解释: "MySQL的InnoDB引擎使用B+树索引,当查询条件不能利用树的有序性时..."
结合优化器原理: "优化器会根据统计信息选择执行计划,当预估索引扫描行数超过阈值..."
引用实际案例: "在我们订单系统中曾遇到...通过...方案解决了..."
延伸讨论:
- 索引下推优化(ICP)
- MRR多范围读取优化
- 覆盖索引与回表代价
最后分享一个排查索引问题的黄金法则:当发现查询变慢时,先看执行计划,再看索引设计,最后考虑SQL重写。记住,好的索引设计应该像精心规划的交通网络,让数据查询永远走"快速路"。