MySQL 10 MySQL为什么有时候会选错索引?

场景引入

我们知道,MySQL中一张表可以支持多个索引。但是写SQL语句时,并没有主动指定使用哪个索引,而是由MySQL来确定。而有时候,MySQL会选错索引,导致执行速度变得很慢。

举个例子,假设一张表里有(id,a,b)三个字段,并分别建立索引。然后往表中插入10万行记录,取值依次递增,即数据从(1,1,1)一直到(100000,100000,100000)

插入过程用了一个存储过程:

sql 复制代码
delimiter ;;
create procedure idata()
begin
  declare i int;
  set i=1;
  while(i<=100000)do
    insert into t values(i, i, i);
    set i=i+1;
  end while;
end;;
delimiter ;
call idata();

接下来分析一条SQL语句:

sql 复制代码
select * from t where a between 10000 and 20000;

显然,该语句能用上索引a。对该语句进行EXPLAIN,查看执行情况:

接着,在表t上做如下操作:

如上,session A开启了一个事务,随后session B删除所有数据,又调用存储过程插入数据,并进行查询。

但是,这条查询语句没有选择索引a。使用如下三条语句进行实验:

sql 复制代码
set long_query_time=0;
select * from t where a between 10000 and 20000; 
select * from t force index(a) where a between 10000 and 20000;/*对照作用*/

慢查询日志如下:

可以发现,session B的查询语句走的是全表扫描,即MySQL用错了索引。

这个场景其实很常见,对应平时不断删除数据和新增数据的场景。因此,本文要讲清为什么会选错索引。

优化器的逻辑

在MySQL 01里,我们已经介绍过了,选择索引是由MySQL的优化器完成的。优化器选择索引的目的是找到一个最优的执行方案,并用最小代价执行语句。

在数据库里,判断执行代价的标准有很多。直观的就是扫描行数,扫描行数越少,意味着访问磁盘次数越少,消耗的CPU资源越少。除此之外,优化器还会结合是否使用临时表、是否排序等因素进行综合判断。

在前面的例子中,没有涉及临时表和排序,那么就是在判断扫描行数是判断错误了。因此,我们需要知道,MySQL如何判断扫描行数。

MySQL在执行语句之前,并不能精确知道满足条件的记录有多少条,只能根据统计信息进行估算。这个统计信息指的是索引的区分度。一个索引上不同的值(我们称之为基数)越多,索引的区分度就越好。

可以使用show index方法来看索引的基数。我们查看例子中表t的基数,其结果如下:

可以发现,尽管三个字段插入的数据都是相同的,但MySQL统计的基数都不同,且都不准确。

由于取每行进行统计代价太高,MySQL在统计基数时使用的是采样统计的方法:采样统计时,InnoDB默认选择N个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,计算出这个索引的基数。

数据表会持续更新,因此索引统计信息也会不断变化。当变更的数据行数超过总行数的1/M,会自动触发重新做一次索引统计。在MySQL中,索引统计有两种存储方式,可以通过设置参数innodb_stats_persistent的值进行选择:

  • 设为on,表示统计信息持久化存储,此时默认N=20,M=10

  • 设为off,表示统计信息只存在内存,此时默认N=8,M=16

尽管基数统计不是完全准确,但从show index的结果看,大体还是接近的,因此选错索引还有其他原因。

除了进行基数统计,优化器还会判断执行语句本身要扫描多少行。对于例子的语句,优化器预估的扫描行数为:

rows字段可以看出,没用上a索引的预计扫描行数为104620,而强制使用a索引的预计扫描行数为37116。

此时,你或许有疑问,既然用上索引a的扫描行数少,且该语句可以用索引a,为什么优化器不使用呢?

这是因为,如果使用索引a,在select *时,需要先从索引a得到id,再回到主键索引找出整行数据,优化器会计算这个代价;如果不使用索引a,是直接在主键索引上扫描并获得数据。在这个例子中,优化器认为直接扫描主键索引更快,尽管该判断是错误的。

因此选错索引的本质原因还在于没有准确判断出扫描行数

既然是统计信息有误,就需要进行修正。可以使用analyze table t来重新统计索引信息:

可以发现这次判断正确。因此,当发现explain的结果和实际情况差距较大,可以先使用analyze进行重新统计。

基于相同的表t,来看另外一个语句:

sql 复制代码
select * from t where (a between 1 and 1000)  and (b between 50000 and 100000) order by b limit 1;

从条件来看,这个查询返回的是空。那么在索引的选择上,该语句会怎么选择呢?

按照我们自己的分析:

  • 若使用索引a,会先扫描索引a的前1000个值,取到对应的id后进行回表,然后根据字段b进行过滤,这样需要扫描1000行。

  • 若使用索引b,会先扫描索引b的最后50001个值,取到对应的id后进行回表,然后根据字段b进行过滤,这样需要扫描50001行。

对该语句explain的结果:

可以看到,优化器选择的是索引b,预估扫描行数为50198,即MySQL又选错了索引。

索引选择异常和处理

遇到上面例子中选错索引的情况,处理办法主要有三种:

(1)采用force index强制选择索引

在不强制选择索引时,MySQL会根据词法解析结果分析出可能使用的索引,然后依次判断每个索引需要扫描多少行。而强制选择后,MySQL会直接选择这个索引。

比如对于例子2,假设使用force index强制选择索引a:

可以看到,使用合理的索引,速度快了很多。

不过强制选择也有缺点:

  • 如果索引改名,该语句也得修改;

  • 如果以后迁移到其他数据库,该语法不一定兼容;

  • 变更不及时,往往是等出现选错索引的问题时才会去强制选择。

(2)修改语句

比如在例子2中,将order by b limit 1改为order by b,a limit 1,语义逻辑不变。但之前优化器使用索引b是因为认为使用索引b可以避免排序,而修改后使用两个索引都需要排序,扫描行数成了影响决策的主要条件,此时优化器选择扫描行数较少的a。

这种方法的缺点就是不通用,需要根据不同语句做不同修改。

(3)新建更合适的索引

在有些场景下,可以新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引。

相关推荐
逻极25 分钟前
数据分析项目:Pandas + SQLAlchemy,从数据库到DataFrame的丝滑实战
python·mysql·数据分析·pandas·sqlalchemy
luoluoal37 分钟前
基于python的英汉电子词典软件(源码+文档)
python·mysql·django·毕业设计·源码
UCH1HA1 小时前
MySQL主从复制与读写分离
linux·mysql·集群
自燃人~1 小时前
为什么MySQL用b+不用B数
数据库·mysql
warton882 小时前
ubuntu24.04 安装mysql8.0.36
linux·运维·mysql
小Mie不吃饭3 小时前
Oracle vs MySQL 全面对比分析
数据库·mysql·oracle
扶尔魔ocy4 小时前
【linux C】在mysql中增加自定义的C动态库
linux·运维·mysql
kabcko4 小时前
CentOS安装Mysql
mysql·adb·centos
ruleslol4 小时前
MySQL 工具使用指南
mysql
爱学java的ptt4 小时前
mysql的存储引擎
数据库·mysql