索引失效的场景有哪些?

本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~

关于SQL语句的面试题中,有两个被面试官问到的频率特别高,一个是SQL优化,另外一个则是索引失效。

接下来我们就来一一进行盘点,索引失效的场景到底有哪些。

准备工作

我们先来创建一张订单表,并为该表生成100w+的数据,这样SQL语句所对应的执行计划会更加准确,表结构如下:

SQL 复制代码
CREATE TABLE `tony_order`  (
  `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单ID',
  `product_id` int NOT NULL COMMENT '商品ID',
  `user_id` int NOT NULL COMMENT '用户ID',
  `status` tinyint NOT NULL COMMENT '状态',
  `discount_amount` int NOT NULL COMMENT '总金额',
  `total_amount` int NOT NULL COMMENT '打折金额',
  `payable_amount` int NOT NULL COMMENT '实际支付金额',
  `receiver_name` varchar(255) DEFAULT NULL COMMENT '收件人名称',
  `receiver_phone` varchar(255) DEFAULT NULL COMMENT '收件人手机号',
  `receiver_address` varchar(255) DEFAULT NULL COMMENT '收件人地址',
  `note` varchar(255) DEFAULT NULL COMMENT '备注',
  `payment_time` datetime NULL DEFAULT NULL COMMENT '支付时间',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id` DESC) USING BTREE,
  INDEX `idx_product_id`(`product_id` ASC) USING BTREE,
  INDEX `idx_user_id_total_amount`(`user_id` ASC, `total_amount` ASC) USING BTREE,
  INDEX `idx_create_time`(`create_time` ASC) USING BTREE,
  INDEX `idx_update_time`(`update_time` ASC) USING BTREE,
  INDEX `idx_status`(`status` ASC) USING BTREE,
  INDEX `idx_receiver_phone`(`receiver_phone` ASC) USING BTREE,
  INDEX `idx_receiver_name`(`receiver_name` ASC) USING BTREE,
  INDEX `idx_receiver_address`(`receiver_address` ASC) USING BTREE
  ) ENGINE = InnoDB CHARACTER SET = utf8mb4 ROW_FORMAT = Dynamic;

接下来我们来一一验证下索引失效的场景。

索引失效场景

1、不遵循最左前缀原则

SQL 复制代码
SELECT * FROM tony_order WHERE total_amount = 100;

我们从执行计划中可以看到,这条SQL语句走的是全表扫描,即使创建了索引idx_user_id_total_amount也没有生效。

但由于其total_amount字段没有在联合索引的最左边,不符合最左前缀原则。

SQL 复制代码
SELECT * FROM tony_order WHERE user_id = 4323 AND total_amount = 101;

当我们把user_id这个字段补上之后,果然就可以用上索引了。

在MySQL 8.0 版本以后,联合索引的最左前缀原则不再那么绝对了,其引入了Skip Scan Range Access Method机制,可对区分度较低的字段进行优化。

感兴趣的同学可以去看下,本文中就不过多展开描述了。

2、LIKE百分号在左边

SQL 复制代码
SELECT * FROM tony_order WHERE receiver_address LIKE '%北京市朝阳区望京SOHO';

SELECT * FROM tony_order WHERE receiver_address LIKE '%北京市朝阳区望京SOHO%';  

执行上面这两条SQL语句,结果都是一样的,走了全表扫描。

接下来我们将SQL语句改为%在右边,再执行一次看看。

SQL 复制代码
SELECT * FROM tony_order WHERE receiver_address LIKE '北京市朝阳区望京SOHO%';  

这个原理很好理解,联合索引需要遵循最左前缀原则,而单个索引LIKE的情况下,也需要最左边能够匹配上才行,否则就会导致索引失效。

3、使用OR关键字

有一种说法,只要使用OR关键字就会导致索引失效,我们来试试。

SQL 复制代码
SELECT * FROM tony_order WHERE receiver_name = 'Tony学长' OR user_id = 41323;

从结果中我们可以看到,索引并没有失效,聪明的查询优化器将receiver_name和user_id两个字段上的索引进行了合并。

接下来我们再换个SQL试试。

SQL 复制代码
SELECT * FROM tony_order WHERE receiver_phone = '13436669764' OR user_id = 4323;

这次确实索引失效了,由于receiver_phone这个字段上并没有创建索引,所以无法使用索引合并操作了,只能走全表扫描。

有的同学会问,那为什么user_id上的索引也失效了呢?

因为一个字段走索引,另一个字段走全表扫描是没有意义的,反而不如走一次全表扫描查找两个字段更有效率。

所以,有时候索引失效未必是坏事,而是查询优化器做出的最优解。

4、索引列上有函数

SQL 复制代码
SELECT * FROM tony_order WHERE ABS (user_id) = 4323; 
SQL 复制代码
SELECT * FROM tony_order WHERE LEFT (receiver_address, 3) 

这个不用过多解释了,就是MySQL的一种规范,违反就会导致索引失效。

5、索引列上有计算

SQL 复制代码
SELECT * FROM tony_order WHERE  user_id + 1 = 4324; 

这个也不用过多解释了,还是MySQL的一种规范,违反就会导致索引失效。

6、字段隐式转换

SQL 复制代码
SELECT * FROM tony_order WHERE receiver_phone = 13454566332;

手机号字段明明是字符类型,却在SQL中不慎写成了数值类型而导致隐式转换,最终导致receiver_phone字段上的索引失效。

SQL 复制代码
SELECT * FROM tony_order WHERE receiver_phone = '13454566332';

当我们把手机号加上单引号之后,receiver_phone字段的索引就生效了,整个天空都放晴了。

SQL 复制代码
SELECT * FROM tony_order WHERE product_id = '12345';

我们接着尝试,把明明是数值型的字段写成了字符型,结果是正常走的索引。

由此得知,当发生隐式转换时,把数值类型的字段写成字符串类型没有影响,反之,但是把字符类型的字段写成数值类型,则会导致索引失效。

7、查询记录过多

SQL 复制代码
SELECT * FROM tony_order WHERE product_id NOT IN (12345,12346);

那么由此得知,使用NOT IN关键字一定会导致索引失效?先别着急下结论。

SQL 复制代码
SELECT * FROM tony_order WHERE status NOT IN (0,1);

从执行计划中可以看到,status字段上的索引生效了,为什么同样使用了NOT IN关键字,结果却不一样呢?

因为查询优化器会对SQL语句的查询记录数量进行评估,如果表中有100w行数据,这个SQL语句要查出来90w行数据,那当然走全表扫描更快一些,毕竟少了回表查询这个步骤。

反之,如果表中有100w行数据,这个SQL语句只需要查出来10行数据,那当然应该走索引扫描。

SQL 复制代码
SELECT * FROM tony_order WHERE status IN (0,1);

同样使用IN关键字进行查询,只要查询出来的记录数过于庞大,都会通过全表扫描来代替索引扫描。

SQL 复制代码
SELECT * FROM tony_order WHERE status = 0;

甚至我们不使用IN、NOT IN、EXISTS、NOT EXISTS这些关键字,只使用等号进行条件筛选同样会走全表扫描,这时不走索引才是最优解。

8、排序顺序不同

SQL 复制代码
SELECT * FROM tony_order ORDER BY user_id DESC,total_amount ASC 

我们可以看下,这条SQL语句中的user_id用了降序,而total_amount用了升序,所以导致索引失效。

SQL 复制代码
SELECT * FROM tony_order ORDERBY user_id ASC,total_amount ASC

而下面这两条SQL语句中,无论使用升序还是降序,只要顺序一致就可以使用索引扫描。

相关推荐
DoraBigHead4 分钟前
手写 `new`、`call`、`apply`、`bind` + V8 函数调用机制解密
前端·javascript·面试
ai小鬼头13 分钟前
AIStarter教你快速打包GPT-SoVITS-v2,解锁AI应用市场新玩法
前端·后端·github
paopaokaka_luck1 小时前
基于SpringBoot+Vue的汽车租赁系统(协同过滤算法、腾讯地图API、支付宝沙盒支付、WebsSocket实时聊天、ECharts图形化分析)
vue.js·spring boot·后端·websocket·算法·汽车·echarts
giao源1 小时前
Spring Boot 整合 Shiro 实现单用户与多用户认证授权指南
java·spring boot·后端·安全性测试
【本人】1 小时前
Django基础(四)———模板常用过滤器
后端·python·django
豌豆花下猫2 小时前
Python 潮流周刊#111:Django迎来 20 周年、OpenAI 前员工分享工作体验(摘要)
后端·python·ai
汤姆大聪明2 小时前
Mysql定位慢查询
数据库·mysql
LaoZhangAI2 小时前
ComfyUI集成GPT-Image-1完全指南:8步实现AI图像创作革命【2025最新】
前端·后端
LaoZhangAI2 小时前
Cline + Gemini API 完整配置与使用指南【2025最新】
前端·后端