为什么SQL语句走了索引,执行却很慢?

Last updated on February 20, 2025 pm

写在前面

我们都知道,业务开发涉及到数据库的SQL操作时,一定要review是否命中索引。否则,会走“全表扫描”,如果表数据量很大时,会慢的要死。

但假如命中了索引,就一定不会出现慢查询吗?

慢查询

MySQL的慢查询日志是MySQL提供的一种日志记录,用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。

Explain

其实,在explain中,我们不仅需要关注type字段,更重要的是看rows字段,它记录的是扫描行数。当一个表很大时,不仅要关注是否有索引,还要关注索引的过滤性是否足够好。

举例

现在有一张表,记录了中国14亿人的基本信息,有一个索引是姓名和年龄的组合索引。现在需要查出所有姓张,并且年龄是8岁的人的信息,

方案1:

1
select * from people where name like '张%' and age=8;

在MySQL5.5和之前的版本中,这个语句的执行流程是这样的:

  • 首先从联合索引上找到第1个姓张的记录,取出主键id,然后到主键索引树上,根据id取出整行的值;
  • 判断年龄字段是否等于8,如果是就作为结果集的一行返回,如果不是就丢弃。
  • 在联合索引上向右遍历,并重复做回表和判断的逻辑,直到碰到联合索引树上名字的第1个字不是张的记录为止。

假设全国姓张的人有8000万,那么这个过程就要回表8000万次。

在MySQL5.6版本,引入了索引下推的优化。我们来看看这个优化的执行流程:

  • 首先从联合索引树上,找到第1个姓张的记录,判断这个索引记录里面,年龄的值是不是8,如果是就回表,取出整行数据,作为结果集的一部分返回,如果不是就丢弃;
  • 在联合索引树上,向右遍历,并判断年龄字段后,根据需要做回表,直到碰到联合索引树上名字的第1个字不是张的记录为止;

假设全国姓张的人有8000万,其中只有100万个人年龄是8岁,那么这个查询过程中在联合索引里要遍历8000万次,而回表只需要100万次。

还有一个更方便有效的方法:

我们可以考虑把姓和年龄来做一个联合索引。这里可以使用MySQL5.7引入的虚拟列来实现。

首先在表上上创建一个字段叫name_first的虚拟列,然后给name_first和age上创建一个联合索引,并且,让这个虚拟列的值总是等于name字段的前两个字节,虚拟列在插入数据的时候不能指定值,在更新的时候也不能主动修改,它的值会根据定义自动生成,在name字段修改的时候也会自动修改。

有了这个新的联合索引,我们在找姓张,并且年龄为8的小朋友的时候,这个SQL语句就可以这么写:

1
select * from t_people where name_first='张' and age=8

这样这个语句的执行过程,就只需要扫描联合索引的100万行,并回表100万次,这个优化的本质是我们创建了一个更紧凑的索引,来加速了查询的过程。

总结

优化sql,并不只是看有没有走索引,而是要减少扫描行数。可能不同的场景下有不同的原因,重点是找出性能瓶颈所在,然后解决它。


为什么SQL语句走了索引,执行却很慢?
http://example.com/2025/02/20/为什么SQL语句走了索引,执行却很慢?/
Author
April
Posted on
February 20, 2025
Licensed under