查看原文
其他

数据库中 SQL 语句使用索引,还是很慢?可能是这几点原因

Java精选 2022-08-09
>>号外:关注“Java精选”公众号,回复“2021面试题”关键词,领取全套500多份Java面试题文件。
慢查询归纳起来大概有这么几种情况
1)全表扫描;
2)全索引扫描;
3)索引过滤性不好;
4)频繁回表的开销。
全索引扫描的不足
那如果我们在更深层次的看这个问题,其实他还潜藏了一个问题需要澄清,就是什么叫做使用了索引。
我们都知道,InnoDB是索引组织表,所有的数据都是存储在索引树上面的。比如上面的表t,这个表包含了两个索引,一个主键索引和一个普通索引。在InnoDB里,数据是放在主键索引里的。
如图所示:
可以看到数据都放在主键索引上,如果从逻辑上说,所有的InnoDB表上的查询,都至少用了一个索引,所以现在我问你一个问题,如果你执行select from t where id>0,你觉得这个语句有用上索引吗?
我们看上面这个语句的explain的输出结果显示的是PRIMARY。其实从数据上你是知道的,这个语句一定是做了全面扫描。但是优化器认为,这个语句的执行过程中,需要根据主键索引,定位到第1个满足ID>0的值,也算用到了索引。
所以即使explain的结果里写的KEY不是NULL,实际上也可能是全表扫描的,因此InnoDB里面只有一种情况叫做没有使用索引,那就是从主键索引的最左边的叶节点开始,向右扫描整个索引树。
也就是说,没有使用索引并不是一个准确的描述。
你可以用全表扫描来表示一个查询遍历了整个主键索引树;也可以用全索引扫描,来说明像select a from t;这样的查询,他扫描了整个普通索引树;而select * from t where id=2这样的语句,才是我们平时说的使用了索引。他表示的意思是,我们使用了索引的快速搜索功能,并且有效的减少了扫描行数。
索引的过滤性要足够好
根据以上解剖,我们知道全索引扫描会让查询变慢,接下来就要来谈谈索引的过滤性。
假设你现在维护了一个表,这个表记录了中国14亿人的基本信息,现在要查出所有年龄在10~15岁之间的姓名和基本信息,那么你的语句会这么写
select * from t_people where age between 10 and 15
你一看这个语句一定要在age字段上开始建立索引了,否则就是个全面扫描,但是你会发现,在你建立索引以后,这个语句还是执行慢,因为满足这个条件的数据可能有超过1亿行。
我们来看看建立索引以后,这个表的组织结构图:
这个语句的执行流程是这样的:
从索引上用树搜索,取到第1个age等于10的记录,得到它的主键id的值,根据id的值去主键索引取整行的信息,作为结果集的一部分返回;
在索引age上向右扫描,取下一个id的值,到主键索引上取整行信息,作为结果集的一部分返回;
重复上面的步骤,直到碰到第1个age大于15的记录;
你看这个语句,虽然他用了索引,但是他扫描超过了1亿行。所以你现在知道了,当我们在讨论有没有
使用索引的时候,其实我们关心的是扫描行数。
对于一个大表,不止要有索引,索引的过滤性还要足够好。
像刚才这个例子的age,它的过滤性就不够好,在设计表结构的时候,我们要让所有的过滤性足够好,也就是区分度足够高。
回表的代价
那么过滤性好了,是不是表示查询的扫描行数就一定少呢?
我们再来看一个例子:
如果你的执行语句是
select * from t_people where name='张三' and age=8
t_people表上有一个索引是姓名和年龄的联合索引,那这个联合索引的过滤性应该不错,可以在联合索引上快速找到第1个姓名是张三,并且年龄是8的小朋友,当然这样的小朋友应该不多,因此向右扫描的行数很少,查询效率就很高。
但是查询的过滤性和索引的过滤性可不一定是一样的,如果现在你的需求是查出所有名字的第1个字是张,并且年龄是8岁的所有小朋友,你的语句会怎么写呢?
你的语句要怎么写?
很显然你会这么写:
select * from t_people where name like '张%' and age=8;
在MySQL5.5和之前的版本中,这个语句的执行流程是这样的:
首先从联合索引上找到第1个年龄字段是张开头的记录,取出主键id,然后到主键索引树上,根据id取出整行的值;
判断年龄字段是否等于8,如果是就作为结果集的一行返回,如果不是就丢弃。
在联合索引上向右遍历,并重复做回表和判断的逻辑,直到碰到联合索引树上名字的第1个字不是张的记录为止。
这个过程跟上面的差别,是在遍历联合索引的过程中,将年龄等于8的条件下推到所有遍历的过程中,减少了回表的次数,假设全国名字第1个字是张的人里面,有100万个是8岁的小朋友,那么这个查询过程中在联合索引里要遍历8000万次,而回表只需要100万次。
虚拟列
可以看到这个优化的效果还是很不错的,但是这个优化还是没有绕开最左前缀原则的限制,因此在联合索引你还是要扫描8000万行,那有没有更进一步的优化方法呢?
我们可以考虑把名字的第一个字和age来做一个联合索引。这里可以使用MySQL5.7引入的虚拟列来实现。对应的修改表结构的SQL语句:
alter table t_people add name_first varchar(2) generated (left(name,1)),add index(name_first,age);
我们来看这个SQL语句的执行效果:
CREATE TABLE `t_people`(
`id` int(11) DEFAULT NULL,
`name` varchar(20) DEFAUT NULL,
`name_first` varchar(2) GENERATED ALWAYS AS (left(`name`,1)) VIRTUAL,
KEY `name_first`(`name_first`,'age'))
ENGINE=InnoDB DEFAULT CHARSET=utf8;
首先他在people上创建一个字段叫name_first的虚拟列,然后给name_first和age上创建一个联合索引,并且,让这个虚拟列的值总是等于name字段的前两个字节,虚拟列在插入数据的时候不能指定值,在更新的时候也不能主动修改,它的值会根据定义自动生成,在name字段修改的时候也会自动修改。
有了这个新的联合索引,我们在找名字的第1个字是张,并且年龄为8的小朋友的时候,这个SQL语句就可以这么写:
select * from t_people where name_first='张' and age=8
这样这个语句的执行过程,就只需要扫描联合索引的100万行,并回表100万次,这个优化的本质是我们创建了一个更紧凑的索引,来加速了查询的过程。 

作者:以前你的名字

blog.csdn.net/qq_29374433/article/details/113735535

往期精选  点击标题可跳转

Spring Boot 框架中实现跨域访问的五种解决方案,你懂了吗?

面试官问:导致 Spring 事务失效的场景有哪些,如何解决失效问题?

推荐 IntelliJ IDEA 特别“养眼”的主题插件,这几款超赞,总有一款适合你!

京东面试官问:LEFT JOIN 关联表中用 ON 还是 WHERE 跟条件有什么区别?

IDEA 超全面的设置及功能优化,效率那是杠杠的!

还在用 System.out.println("") 输出日志到控制台吗?那么太 Low 了!

Intellij IDEA 2020.3 如何配置注释模板,让你瞬间提高出一个逼格?

推荐 2021 年 Java 超经典的 25 道 MyBatis 面试题,值得收藏!

高并发场景下基于 Spring Boot 框架来实现 MySQL 读写分离(附源码)

有没有搞错?Java 对象不使用时,为什么要赋值 null?

面试官问:Java 中如何处理含有泛型的 JSON 反序列化问题?

MySQL 数据库中 52 条 SQL 语句性能优化方法,干货必收藏!

点个赞,就知道你“在看”!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存