哪些操作会导致索引失效?(隐式类型转换、函数包裹列、!=、NOT IN、模糊查询前置%、OR 部分列无索引等)
What operations cause an index to not be used? (e.g., implicit type conversion, functions
考察要点
这道题面试官想看你是否真正理解 B+ 树索引的工作原理,以及 MySQL 查询优化器的决策逻辑。高分回答不仅仅是罗列失效场景,而是要从“为什么优化器无法或放弃使用索引”的角度出发,解释其背后的根本原因——即 SQL 写法是否破坏了索引的有序性和可搜索性(SARGable)。
核心概念
索引失效的本质是查询谓词(WHERE 条件)不具备“可搜索性”(Searchable Argument, SARG)。一个 SARGable 的谓词通常是 列 操作符 常量 的形式,且操作符能让优化器利用索引的有序性来快速定位数据范围。当谓词 non-SARGable 时,优化器无法有效利用 B+ 树的结构,可能退化为全索引扫描(index)或全表扫描(ALL)。
以下是导致索引失效的典型场景,及其原理解释:
| 场景 | 反例 (可能导致索引失效) | 正例 (可利用索引) | 原理解释 |
|---|---|---|---|
| 1. 对索引列使用函数 | WHERE DATE_FORMAT(create_time, '%Y-%m') = '2023-10' | WHERE create_time >= '2023-10-01' AND create_time < '2023-11-01' | B+ 树中存储的是 create_time 的原始值,并按其排序。对列使用函数后,MySQL 无法预知函数计算结果的顺序,必须对每一行都计算一次函数值,再与条件比较,这等同于全表扫描。 |
| 2. 隐式类型转换 | WHERE phone_number = 13800001111 (phone_number 是 VARCHAR) | WHERE phone_number = '13800001111' | MySQL 规则:当字符串和数字比较时,会将字符串转换为数字。上述查询会变成 WHERE CAST(phone_number AS SIGNED) = 13800001111,这又回到了场景 1,对索引列使用了函数。注意:如果列是数字类型,条件是字符串,如 WHERE user_id = '123',通常能走索引,因为 MySQL 会将常量字符串转为数字,而不是对列进行转换。 |
| 3. 左模糊查询或双向模糊查询 | WHERE name LIKE '%张' 或 WHERE name LIKE '%三%' | WHERE name LIKE '张%' | B+ 树的有序性是从左到右的。'张%' 可以定位到以“张”开头的第一条记录,然后向后扫描。而以通配符 % 开头,无法确定起始点,只能全表扫描。 |
4. 使用 OR 连接,且部分条件列无索引 | WHERE indexed_col = 'A' OR unindexed_col = 'B' | WHERE indexed_col = 'A' UNION ALL WHERE unindexed_col = 'B' (需评估性能) | 优化器需要找到满足任一条件的行。即使 indexed_col 能走索引,但 unindexed_col 必须全表扫描。优化器会认为,既然总要全表扫描一次,不如直接一次性全表扫描,同时判断两个条件,成本更低。例外:MySQL 5.0+ 引入了 index_merge 优化,若 OR 两边的列都有独立的索引,可能合并两个索引的扫描结果。 |
5. 使用 NOT IN 或 != | WHERE status NOT IN (1, 2) 或 WHERE status != 0 | WHERE status IN (0, 3, 4) 或 WHERE status > 0 | NOT IN 和 != 通常表示一个很大的、不连续的范围。优化器评估后,如果发现走索引扫描的成本(扫描大量索引页 + 大量回表)比直接全表扫描更高,就会放弃索引。这取决于数据的选择性(Selectivity)。如果 status != 0 排除的记录只占总数的 1%,那么很可能走索引;如果排除了 90%,则大概率全表扫描。 |
6. IS NULL / IS NOT NULL | WHERE indexed_col IS NULL | (无) | 早期 MySQL 版本中,NULL 值不进入索引。现代 InnoDB 中,NULL 值也会被索引(通常放在 B+ 树的最左侧)。因此 IS NULL 可以走索引。但和 != 类似,如果 NULL 值的占比非常高,优化器基于成本考虑也可能放弃索引。 |
| 7. 复合索引未使用最左前缀 | 复合索引 idx(a, b, c),查询 WHERE b = 2 AND c = 3 | WHERE a = 1 AND b = 2 | B+ 树对复合索引的排序规则是:先按 a 排序,a 相同再按 b 排序,以此类推。没有 a 作为查询的起始条件,优化器无法利用 (a, b, c) 的有序性来定位 b 和 c。 |
常见低分回答
- 绝对化结论:“用了
OR索引就一定失效”。这是错误的,忽略了index_merge优化的可能性,也忽略了OR两边是同一个索引列的情况(如id = 1 OR id = 100,可以转为id IN (1, 100),是典型的 range scan)。 - 函数误解:“
WHERE后面用了函数索引就失效”。这种说法不精确。应该是“对索引列本身使用函数”。如果函数用在常量上,如WHERE create_time > NOW() - INTERVAL 1 DAY,索引是完全可以正常使用的。 - 混淆
COUNT(*)和COUNT(列):当被问到索引优化时,错误地回答COUNT(col)会比COUNT(*)快,因为它不统计NULL。虽然语法语义正确,但这和索引失效场景无关。正确的关联是:当存在一个二级索引时,COUNT(*)可能会选择扫描这个更小的二级索引(Extra: Using index),而不是扫描主键索引或全表,这是一种优化,而非失效。 - 对隐式转换方向不敏感:只知道“类型转换会失效”,但说不清是“列类型向常量类型转换”还是反过来。高分回答必须点出是 MySQL 将列进行函数包装(
CAST)导致的问题。 - 背诵口诀,不解释原理:只说“左模糊不走索引”,但无法解释 B+ 树的有序性决定了必须从左侧开始匹配。面试官一追问“为什么”,就无法深入回答。
工程实践与优化
-
首选工具
EXPLAIN:任何关于索引是否生效的疑问,都应以EXPLAIN的结果为准。type:检查连接类型。从好到坏:const>eq_ref>ref>range>index>ALL。出现index(全索引扫描)或ALL(全表扫描)通常意味着索引利用不佳。key:实际使用的索引。如果为NULL,则表示没有使用索引。key_len:索引使用的字节数。可用于判断复合索引是否完全被利用。Extra:包含关键信息。Using index(覆盖索引)、Using where(在存储引擎层过滤后,服务层还需过滤)、Using filesort(无法利用索引排序)、Using temporary(使用了临时表)。
sql1-- 示例:分析隐式类型转换2EXPLAIN SELECT * FROM users WHERE phone_number = 13800001111;3-- 查看 Extra 字段,可能会看到 "Using where",type 可能是 "ALL" -
SQL 改写:
- 函数/转换:将对列的操作转移到对常量值的操作上。
WHERE col/10 = 100->WHERE col = 1000WHERE date_col < CURDATE()->WHERE date_col < '2023-10-27'(在应用层计算好) OR优化:如果OR的两边 selectivity 都很高,可以考虑拆成UNION ALL,让每个子查询都能走索引。但要注意UNION会引入去重开销。- 前缀模糊查询:如果业务确实需要,考虑使用全文索引(Full-Text Index)或引入外部搜索引擎如 Elasticsearch。
- 函数/转换:将对列的操作转移到对常量值的操作上。
-
索引设计:
- 函数索引 (MySQL 8.0+):如果业务逻辑无法避免对列使用函数,可以创建函数索引。
sql1-- 针对 WHERE JSON_EXTRACT(extra, '$.name') = '张三'2CREATE INDEX idx_extra_name ON user_info ((JSON_EXTRACT(extra, '$.name')));
- 复合索引:遵循最左前缀原则,将过滤性最好的列(Cardinality 高)放在最左边。
- 函数索引 (MySQL 8.0+):如果业务逻辑无法避免对列使用函数,可以创建函数索引。
常见追问与延伸题目
-
追问:如果
name LIKE '%keyword%'这种需求非常频繁且性能要求高,该如何优化?- 回答要点 1 (DB 内部方案):使用 MySQL 5.7+ 的 ngram 全文索引或 MySQL 8.0+ 的函数索引。ngram 会将文本拆分成固定长度的词组进行索引,可以支持任意位置的模糊匹配,但索引体积较大。
- 回答要点 2 (外部系统方案):引入专业的搜索引擎,如 Elasticsearch (ES) 或 OpenSearch。将需要搜索的数据同步到 ES 中,利用其强大的倒排索引和分词能力,可以实现毫秒级的复杂文本搜索,这是业界处理此类问题的标准方案。
- 回答要点 3 (折中方案):如果数据量不大,可以考虑创建一个反转列,如
name_rev,存储name列的反转字符串。查询LIKE '%keyword'就变成了LIKE REVERSE('keyword')%,可以走name_rev列的索引。这是一种 trick,维护成本较高。
-
追问:
EXPLAIN结果中type是index,这是好是坏?它和ALL有什么区别?- 回答要点 1 (定义):
type: index指的是“全索引扫描”(Full Index Scan)。它会遍历整个索引树的叶子节点,而不是全表。 - 回答要点 2 (与 ALL 的区别):它比
type: ALL(全表扫描)要快,因为索引通常比表数据本身小得多,扫描索引的 I/O成本更低。尤其是在二级索引上进行全索引扫描,I/O 会显著减少。 - 回答要点 3 (好坏判断):它通常不是一个好的信号,因为它意味着查询没有利用索引的有序性来快速定位数据,而是遍历了整个索引。例如
SELECT id FROM users;在id是主键时,如果users表很大,这个操作就会是type: index。但如果查询是COUNT(*)并且有一个很小的二级索引,此时优化器选择type: index来扫描这个小索引,则是一种非常高效的优化(Extra: Using index)。所以好坏取决于具体的查询场景。
- 回答要点 1 (定义):
-
追问:如果我建了
idx(a, b)复合索引,WHERE a > 10 AND b = 5这个查询能用上索引吗?能用上多少?- 回答要点 1 (范围查询的影响):这个查询能用上索引,但只能部分利用。MySQL 会使用索引
idx(a, b)来定位a > 10的部分。 - 回答要点 2 (最左前缀中断):一旦在
a列上进行了范围查找(如>、<、BETWEEN),B+ 树的有序性对于后续的列(b列)就失效了。因为满足a > 10的记录,其b列是无序的。 - 回答要点 3 (
EXPLAIN结果):EXPLAIN的结果会显示key是idx(a, b),但key_len只会包含a列的长度,表示优化器只用索引的第一部分来定位数据,然后在定位到的这些行中,再通过WHERE条件去过滤b = 5的数据(Extra中会出现Using where)。
- 回答要点 1 (范围查询的影响):这个查询能用上索引,但只能部分利用。MySQL 会使用索引