SQL面试之--明明建了索引为什么失效了?
背景
在数据库查询优化中,索引是提升查询性能的核心手段。但实际场景中,即使创建了索引,查询性能仍可能未达预期导致slow sql,这类问题但凡你在简历上写了个熟悉mysql或者PG这类的数据库,面试官可能都会问一下,以此判断你到底是简单的掌握了增删改查建表删表还是精通。
下面我们就来分析一下这个问题
索引失效意味着什么?
索引失效,说得明白点,通常索引失效的最终表现就是扫全表去了,比如Mysql的ALL,或者PG的Seq Scan(顺序扫描)
那如何验证是否触发全表扫描呢?
要看是哪种类型很简单,在sql前加一个EXPLAIN
--Mysql
EXPLAIN SELECT * FROM table_name WHERE name='test';
-- 若 type 列为 ALL,表示全表扫描 | 若 key 列为 NULL,表示未使用索引。--PG
EXPLAIN ANALYZE SELECT * FROM table_name WHERE name='test';
-- 若出现 Seq Scan on table_name,表示全表扫描。
特殊情况
至于为什么我之前说通常情况下,因为有些场景即使索引有效,数据库的优化器仍可能选择全表扫描(因为它觉得全表扫描比用索引更快,是的,优化器否定了你并朝你丢来一个鸡蛋😀):
场景 | 原因 |
---|---|
表数据量极小 | 小表(如 < 1000 行)的全表扫描可能比索引扫描更快(减少随机 I/O)。 |
索引选择性过低 | 索引列的值重复率高(如“性别”一共就三种枚举值-男/女/空),用索引不如全表扫描高效 |
索引失效的常见原因及案例
言归正传,在排查掉特殊情况后,我们开始分析索引失效的常见原因及案例
1. 查询写法问题
1.1 对索引列使用函数或计算
-- 示例:date_column是索引列,但查询时使用函数或者计算
SELECT * FROM orders WHERE YEAR(date_column) = 2023;
SELECT * FROM orders WHERE date_column+1 = 2023;-- 失效原因:索引存储原始值,无法匹配函数处理后的值--优化方案:改写查询条件,避免索引列参与计算:
SELECT * FROM orders
WHERE date_column BETWEEN '2023-01-01' AND '2023-12-31';
1.2 对索引列使用左模’%test’
-- 示例:name列有索引,但左模糊匹配 '%ie' 或者 '%ie%'
SELECT * FROM users WHERE name LIKE '%ie';
-- 失效原因:80%的现实情况下,我们建的索引都是默认B-tree索引,而B-tree是不支持左模的--因为它是基于最左前缀有序存储的,无法直接定位中间或结尾的字符。
这里顺便科普一下,哪些索引可支持模糊匹配
- Mysql专为文本搜索设计,支持任意位置的词项匹配的全文索引(Full-Text Index):
- PG(GIN)和 ES的倒排索引
额外知识:倒排索引
这里再灌点知识,什么是倒排索引?倒排索引是一种 “从词项到文档” 的索引结构,与传统的 正排索引(“从文档到词项”)相反,这么讲有点干巴,上例子。
传统的正排索引,类似小说的目录,我们通过目录去找对应的内容,每一个章节名就是一个文档id,你想查看第3章,直接翻到对应页码即可。
但你最喜欢的的角色是Kuromi,你只想看Kuromi出现的章节,这时候就难办了,我总不能一页一页去翻吧?
这时候聪明的你,想到了为角色建立章节的映射,记录角色出现的章节及位置。(也就是为词项建立倒排索引–第一次为全量)
"Kuromi" → [{章节: 1, 位置: [10, 25], 出现次数: 2}, -- 第1章第10、25行{章节: 3, 位置: [5], 出现次数: 1}, -- 第3章第5行{章节: 5, 位置: [30], 出现次数: 1} -- 第5章第30行
]
这时候你想看Kuromi是不是简单多了,无需阅读全文(遍历全表),直达跳转到对应章节,就算有新增章节(增量),我们也无需重新构建索引,只需更新Kuromi的倒排列表即可。
对了,这时候还有一种情况会导致索引失效,比如PG中一个jsonb类型的列,你建了一个B-tree的索引,这时候如果你要单独匹配对象中某个字段(如 data->>‘user’),则B-tree 索引将完全失效,除非你是要精准匹配整个jsonb对象
1.3 隐式类型转换(如列用字符串查询)。
索引列是数字类型,但查询用字符串:WHERE str_col = '123'
--失败原因:数据库需隐式转换类型,无法直接使用索引(如 str_col = 123 才能命中索引)。
1.4 使用OR 连接非索引列。
WHERE a = 1 OR b = 2(b 无索引)
-- 失败原因:优化器无法有效合并索引扫描,只能全表扫描。
**1.5 使用 NOT、!=、NOT IN
这个不是一定的,因为非等值查询可能无法高效使用索引(这具体取决于数据库优化器实现,所以不是一定的)
2 索引设计缺陷
2.1 覆盖索引未遵循最左前缀原则
所谓覆盖索引就是我们在建立索引时选择了多列
-- 示例:覆盖索引为 (name, age),但查询条件跳过name
SELECT * FROM table WHERE age = 22;
-- 失效原因:复合索引需从最左列开始匹配优化方案:
调整查询条件顺序或重建索引:
CREATE INDEX idx_b ON table(age); -- 单独为age建索引
2.2 索引未覆盖查询字段
索引未覆盖查询字段是指索引中未包含查询所需的所有字段,这其实严格意义上算是命中了索引的,不算严格意义上的索引失效,但是数据库需要回表查询(Bookmark Lookup)从数据页中获取额外的数据。这会增加 I/O 开销,降低查询性能。
-- 示例:索引仅包含id,但SELECT *需回表
SELECT * FROM products WHERE id > 100;
-- 失效原因:需要所有字段,但索引只有id--优化方案:建议建覆盖索引,比如你经常查name age city,那你就给这几个建一个覆盖索引
CREATE INDEX idx_covering ON users(name, age, city);