AI智能
改变未来

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

案例:

CREATE TABLE `t` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `a` (`a`),
KEY `b` (`b`)
) ENGINE=InnoDB;

然后,我们往表 t 中插入 10 万行记录,取值按整数递增,即:(1,1,1),(2,2,2),(3,3,3) 直到 (100000,100000,100000)。

1)10万条记录是怎么插进去的?

  • 存储过程

2)什么是存储过程?

  • 其实就是java中的函数

3)插入10万条记录的存储过程怎么写?

delimiter ;; //告诉mysql,该段命令是否结束
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();

4)执行select * from t where a between 10000 and 20000语句,选择是索引是谁?

  • a

5)执行下面图中的流程,那又是选择谁做索引?

不会再选择a做索引了。

5.1)为什么mysql会选错索引,不选择a做索引了?

  • MySQL 在判断扫描行数的时候出问题了

5.1.1)扫描行数是怎么判断的?

  • 根据统计信息来估算。

5.1.2)那么这个统计信息是啥呢?

  • 索引的“区分度“:一个索引上不同的值越多,这个索引的区分度就越好。一个索引上不同的值的个数,我们称之为“基数”。

5.1.3)MySQL 是怎样得到索引的基数的呢?

  • 采样统计(选N 个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数)

5.1.4)为什么要采样统计?

  • 整张表取出来一行行统计,代价太高

5.1.5)索引统计信息是固定的吗?

  • 不是,数据表变,他也跟着变,1/M就变,比如10万条,M就是10,那更新了10000条数据的时候就重新统计索引信息。

5.1.6)索引信息是放在哪里的?

  • innodb_stats_persistent=on,放在磁盘。N为20,M为10.

  • innodb_stats_persistent=off,放在内存。N为8,M为16。

索引统计只是一个输入,对于一个具体的语句来说,优化器还要判断,执行这个语句本身要扫描多少行。

5.1.7)优化器预估的,Q1和Q2的扫描行数是多少?

set long_query_time=0;
select * from t where a between 10000 and 20000; /*Q1*/
select * from t force index(a) where a between 10000 and 20000;/*Q2*/

  • Q1 的结果还是符合预期的,rows 的值是 104620。Q2 的 rows 值是 37116,而图 1 中我们用 explain 命令看到的 rows 是只有 10001 行,是这个偏差误导了优化器的判断。

6)我们上面说不再选择a作为索引,优化器为什么放着扫描 37000 行的执行计划不用,却选择了扫描行数是 100000 的执行计划呢?

  • 用索引a我每次都得回表一次。

  • 如果选择扫描 10 万行,是直接在主键索引上扫描的,没有额外的代价

  • 优化器会估算这两个选择的代价,从结果看来,优化器认为直接扫描主键索引更快。当然,从执行时间看来,这个选择并不是最优的。

7)mysql选错索引的根本原因是什么?

  • 没能准确地判断出扫描行数

8)当发现mysql选错索引了我们应该怎么办?

  • 使用analyze table t 命令,可以用来重新统计索引信息

9)假设现在有如下语句,假如你来选择索引,你会怎样选择?

  • 第一直觉当然是选a

 select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1;

先来看一下 a、b 这两个索引的结构图

  • 如果使用索引 a 进行查询,那么就是扫描索引 a 的前 1000 个值,然后取到对应的 id,再到主键索引上去查出每一行,然后根据字段 b 来过滤。显然这样需要扫描 1000 行。

  • 如果使用索引 b 进行查询,那么就是扫描索引 b 的最后 50001 个值,与上面的执行过程相同,也是需要回到主键索引上取值再判断,所以需要扫描 50001 行。

10)实际上上面的语句mysql选择谁做索引?

竟然选择了b做索引。

11)为什么会选择b来做索引?

  • 又选错了

12)为什么又选错了?

  • 扫描行数的估计值依然不准确;

大多数时候优化器都能找到正确的索引,但偶尔你还是会碰到我们上面举例的这两种情况:原本可以执行得很快的 SQL 语句,执行速度却比你预期的慢很多

13)选错索引我们应该怎么办呢?

  • 采用 force index 强行选择一个索引

  • 在数据库内部来解决

14)用 force index有什么缺点?

  • 不优美

  • 如果索引改了名字,这个语句也得改

  • 如果以后迁移到别的数据库的话,这个语法还可能会不兼容。

15)在数据库里面该怎样解决呢?

15.1)改变自己:

  • 优化器放弃了使用索引 a,说明 a 还不够合适。我们可以考虑修改语句,引导 MySQL 使用我们期望的索引。

  • 把“order by b limit 1” 改成 “order by b,a limit 1”

  • 优化器选择使用索引 b,是因为它认为使用索引 b 可以避免排序(b 本身是索引,已经是有序的了,如果选择索引 b 的话,不需要再做排序,只需要遍历),所以即使扫描行数多,也判定为代价更小。

  • 现在 order by b,a 这种写法,要求按照 b,a 排序,就意味着使用这两个索引都需要排序。因此,扫描行数成了影响决策的主要条件,于是此时优化器选了只需要扫描 1000 行的索引 a。

15.2)麻痹优化器

select * from  (select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 100)alias limit 1;
  • 用 limit 100 让优化器意识到,使用 b 索引代价是很高的。其实是我们根据数据特征诱导了一下优化器,也不具备通用性

15.3)新建一个更合适的索引,或删掉误用的索引。

16)前面我们在构造第一个例子的过程中,通过 session A 的配合,让 session B 删除数据后又重新插入了一遍数据,然后就发现 explain 结果中,rows 字段从 10001 变成 37000 多。而如果没有 session A 的配合,只是单独执行 delete from t 、call idata()、explain 这三句话,会看到 rows 字段其实还是 10000 左右。你可以自己验证一下这个结果。这是什么原因呢?

  • session A开启了一致性读,目的为了保证session A的可重复读,insert只能 另起炉灶,不能占用delete的空间。所以出现的情况就是delete虽然删除了,但是未释放空间,insert又增加了空间。导致统计信息有误

赞(0) 打赏
未经允许不得转载:爱站程序员基地 » MySQL为什么有时候会选错索引?