SQL面试之--明明建了索引为什么失效了?

背景

在数据库查询优化中,​索引是提升查询性能的核心手段。但实际场景中,即使创建了索引,查询性能仍可能未达预期导致slow sql,这类问题但凡你在简历上写了个熟悉mysql或者PG这类的数据库,面试官可能都会问一下,以此判断你到底是简单的掌握了增删改查建表删表还是精通。

下面我们就来分析一下这个问题

索引失效意味着什么?

索引失效,说得明白点,通常索引失效的最终表现就是扫全表去了,比如Mysql的ALL,或者PG的Seq Scan(顺序扫描)

那如何验证是否触发全表扫描呢?​​

要看是哪种类型很简单,在sql前加一个EXPLAIN

bash 复制代码
--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 对索引列使用函数或计算​
bash 复制代码
-- 示例: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'
bash 复制代码
-- 示例:name列有索引,但左模糊匹配 '%ie' 或者 '%ie%'
SELECT * FROM users WHERE name LIKE '%ie';
-- 失效原因:80%的现实情况下,我们建的索引都是默认B-tree索引,而B-tree是不支持左模的--因为它是基于最左前缀有序存储的,无法直接定位中间或结尾的字符。

这里顺便科普一下,哪些索引可支持模糊匹配​

  1. Mysql专为文本搜索设计,支持任意位置的词项匹配的全文索引(Full-Text Index):
  2. PG(GIN)和 ES的倒排索引
额外知识:倒排索引

这里再灌点知识,什么是倒排索引?倒排索引是一种 ​​"从词项到文档"​​ 的索引结构,与传统的 正排索引​("从文档到词项")相反,这么讲有点干巴,上例子。

传统的正排索引,类似小说的目录,我们通过目录去找对应的内容,每一个章节名就是一个文档id,你想查看第3章,直接翻到对应页码即可。

但你最喜欢的的角色是Kuromi,你只想看Kuromi出现的章节,这时候就难办了,我总不能一页一页去翻吧?

这时候聪明的你,想到了为角色建立章节的映射,记录角色出现的章节及位置。(也就是为词项建立倒排索引--第一次为全量

bash 复制代码
"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 ​隐式类型转换​(如列用字符串查询)。
bash 复制代码
索引列是数字类型,但查询用字符串:WHERE str_col = '123'
--失败原因:数据库需隐式转换类型,无法直接使用索引(如 str_col = 123 才能命中索引)。
1.4 使用​OR 连接非索引列。
bash 复制代码
WHERE a = 1 OR b = 2(b 无索引)
-- 失败原因:优化器无法有效合并索引扫描,只能全表扫描。
**1.5 使用 NOT、!=、NOT IN
bash 复制代码
这个不是一定的,因为非等值查询可能无法高效使用索引(这具体取决于数据库优化器实现,所以不是一定的)
2 ​索引设计缺陷​
2.1 覆盖索引未遵循最左前缀原则​

所谓覆盖索引就是我们在建立索引时选择了多列

bash 复制代码
-- 示例:覆盖索引为 (name, age),但查询条件跳过name
SELECT * FROM table WHERE age = 22;
-- 失效原因:复合索引需从最左列开始匹配

优化方案:
调整查询条件顺序或重建索引:
CREATE INDEX idx_b ON table(age);  -- 单独为age建索引
2.2 索引未覆盖查询字段

索引未覆盖查询字段是指​索引中未包含查询所需的所有字段,这其实严格意义上算是命中了索引的,不算严格意义上的索引失效,但是数据库需要​回表查询(Bookmark Lookup)​​从数据页中获取额外的数据。这会增加 I/O 开销,降低查询性能。

bash 复制代码
-- 示例:索引仅包含id,但SELECT *需回表
SELECT * FROM products WHERE id > 100;
-- 失效原因:需要所有字段,但索引只有id

--优化方案:建议建覆盖索引,比如你经常查name age city,那你就给这几个建一个覆盖索引
CREATE INDEX idx_covering ON users(name, age, city);
相关推荐
Lee川10 小时前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
倔强的石头_11 小时前
kingbase备份与恢复实战(二)—— sys_dump库级逻辑备份与恢复(Windows详细步骤)
数据库
Lee川14 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i16 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有16 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有16 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫17 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫17 小时前
Handler基本概念
面试
Wect18 小时前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼19 小时前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试