优化器选错索引,导致线上瘫痪

比较喜欢的一段话:不经一番寒彻骨,怎得梅花扑鼻香,学习是枯燥的请大家坚持! 这篇文章的是向丁奇老师学习的。不懂的自己搜一下哈! 阅读这篇文章大概需要35分钟!

大家好前面我们大概了解了索引如何选择以及利弊问题。今天介绍一下强大的MySQL为什么会选错索引!如果索引选对了事半功倍!

先模拟一下数据,做一些准备工作, 创建表,插入10万条数据

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `a` (`a`),
  KEY `b` (`b`)
) ENGINE=InnoDB;
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分析一下

select * from t where a between 10000 and 20000;

根据索引,这条语句肯定没有问题走了a索引

image.png

图 1 使用 explain 命令查看语句执行情况

从图1看,结果是没问题的,也和我们理想中的是一致的。

接下来,我们执行一下以下SQL

img

图 2 session A 和 session B 的执行流程

在模拟的时候可以开两个会话,这里就是sessionA使用一致快照启动事务,sessionB删除数据+重新插入数据+查询执行计划,最后sessionA再提交事务。

这时候,session B 的查询语句 select * from t where a between 10000 and 20000 就不会再选择索引 a 了。我们可以通过慢查询日志(slow log)来查看一下具体的执行情况。

为了验证我们猜测的准确性。

  1. 首先修改慢查询日志的时间 当前的意思就是0秒之后的所有查询都将记入慢查询日志中。这里有个坑设置完之后必须重启会话,否则无效 set long_query_time=0;
  2. 查询当前设置是否成功 show variables like 'long%';
  3. 查询确认当前慢查询日志是否正常开启 show variables like '%slow_query_log%';

以上就是准备工作了。我们采用了一组对照操作。然后观察执行结果。

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

img

我先声明一下,上面那个图不是我的,其他都是我的,我的慢查询日志显示不出来。奇怪的很

可以看到,Q1 扫描了 10 万行,显然是走了全表扫描,执行时间是 40 毫秒。Q2 扫描了 10001 行,执行了 21 毫秒。也就是说,我们在没有使用 force index 的时候,MySQL 用错了索引,导致了更长的执行时间。

这个例子对应的是我们平常不断地删除历史数据和新增数据的场景。这时,MySQL 竟然会选错索引,是不是有点奇怪呢?今天,我们就从这个奇怪的结果说起吧。

优化器干了哪些事

上述文章中我们了解过一条SQL的执行流程,介绍过优化器主要负责选择索引的操作。那么优化器这里到底是怎么做的呢?我们一步一步深挖底层原理。

优化器选择索引的目的,是找到一个最优的执行方案,并用最小的代价去执行语句。在数据库里面,扫描行数是影响执行代价的因素之一。扫描的行数越少,意味着访问磁盘数据的次数越少,消耗的 CPU 资源越少。

当然,扫描行数并不是唯一的判断标准,优化器还会结合是否使用临时表、是否排序等因素进行综合判断。

我们这个简单的查询语句并没有涉及到临时表和排序,所以 MySQL 选错索引肯定是在判断扫描行数的时候出问题了。

MySQL的扫描行数是怎么判断的?

数据库在真正的执行之前是不可能知道有多少行数据的,而且也不可能把数据都查一遍然后根据数量选择对应的索引。那么到底是怎么实现的呢?

索引的区分度

显然,一个索引上不同的值越多,这个索引的区分度就越好。而一个索引上不同的值的个数,我们称之为“基数”(cardinality)。也就是说,这个基数越大,索引的区分度越好。我们可以通过show index from 表名 看到一个索引的基数。

以下Cardinality列的值并不代表行数。

image.png

索引的基数

上面简单的介绍了基数是啥,基数能影响哪些东西。以下着重介绍一下MySQL内部是怎么得到索引基数的。采样统计

采样统计的时候,innodb默认会选择N个数据页,统计这些界面上的不同值,得到一个平均值,然后乘以这个索引的页面数。这个数就是索引的基数!

数据表会持续更新的,索引的统计信息也不会固定一变。所以当变更的数据行数超过1/M的时候,会自动触发重新做一下索引统计。也就类似于上文提到的 analyze table 表名

介绍一下索引统计,索引统计在MySQL中有两种存储方式,可以通过以下代码查看。

show variables like 'innodb_stats_persistent';

image.png

  • 设置为 on 的时候,表示统计信息会持久化存储。这时,默认的 N 是 20,M 是 10
  • 设置为 off 的时候,表示统计信息只存储在内存中。这时,默认的 N 是 8,M 是 16。

SQL影响

img

图3 explain执行计划

根据图3,我们可以分析出扫描行数差距非常大。行数的原因影响已经分析过了。下面从另一个方面介绍一下。

possible_keys:表示查询时,可能使用的索引

key:表示实际使用的索引

根据以上两个提示得出,可能使用的索引与实际使用的索引不一致,导致索引失效,无效增大查询性能。

那么为什么会这样呢。上文的测试数据,通过sessionA和sessionB的两个会话的数据操作。导致统计数据不准。为什么放着扫描 37000 行的执行计划不用,却选择了扫描行数是 100000 的执行计划呢?

这是因为,如果使用索引 a,每次从索引 a 上拿到一个值,都要回到主键索引上查出整行数据,这个代价优化器也要算进去的。

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

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

使用普通索引,把回表的代价也算了进去。根据图1我们得到我们这样的SQL策略并没有 什么问题。所以冤有头债有主

选错索引的原因有哪些

  • 索引的区分度判断有误 (基数)
  • 回表操作导致索引选择有误

解决方案

  • 利用force index 强行选择一个索引
  • 利用analyze table t重新统计索引信息
  • 新增一个更合适的索引
  • 编写SQL,引导优化器选择索引

为什么必须依靠sessionA的事务

为什么没有session A,session B扫描的行数是1W

顶部的准备工作,我相信很多小伙伴都非常疑惑。我当初一开始接触的时候也是碰壁好久。为什么session的删除,插入,执行的时候要依靠sessionA的一致性事务呢?

如果没有sessionA。sessionB一个会话是否能完成这篇文章的所有工作呢?错误,完成不了

这里要了解delete的实现原理。

delete: 这里的删除并不是真正意义上从磁盘中删除,而是直接删除了当前的引用标记。

因为delete删除的特性。如果delete与存储过程中的insert语句间隔时间较短,purge线程还没有来得及清理该记录数据。如果主键相同的情况下,MySQL会自动使用之前的记录空间。

由于相同的数据量,以及表大小,索引的统计信息没有变化,最终导致扫描行数等一系列的问题的出现。

为什么开启了session A,session B扫描行数变成3W

由于是一致性事务也可以说是一致性读,目的是保证sessionA的可重复读。insert只能另起炉灶,把数据插入到其他地方。然后时间一到,purge就会清空之前的删除数据。

purge还没有执行的时候,就会出现表大小不一致,空间不一致等一系列问题,最终导致索引统计信息有误。

purge线程干了啥

innodb_purge_batch_size

set innodb_purge_batch_size=X

用来设置每次purge操作需要清理的undo log page的数量。【默认300,表示每次清理300个page,支持动态修改】

设置的越大,表示每次回收的页也就越多,可供重用的undo page也就越多,就能减少磁盘存储空间与分配的开销。不过该参数设置得太大,则每次需要purge处理更多的undo page,从而导致CPU和磁盘IO过于集中于对undo log的处理,使性能下降。普通用户不建议调整这个参数

image.png

innodb_purge_threads

set innodb_purge_threads=X

当有很多的表进行DML操作时候, 增大 innodb_purge_threads 能提高purge的效率(清理掉MVCC机制导致的老旧数据)。

现在的MySQL版本中。purge线程已经从master线程中独立出来,使用单独的线程提高了可伸缩性。

从MySQL5.7.8开始,这个参数默认是4,最大可以设置为32.【老版本里面这个值默认是1】

image.png
这两个一般不需要修改,这里就不过多阐述了

set innodb_max_purge_lag = X
set innodb_max_purge_lag_delay = X

作用

  • 清理del flag标签的记录
  • 清理undo的历史版本
  • 如果需要进行undo tablespace截断。

文章持续更新【公众号】

大概介绍了MySQL为什么会选错索引,优化器做了哪些事情,索引选择的依据,引导走更优的索引,delete原理,一致性读导致索引不准等问题。

最后在公众号中列举了一些以上文章的掌握程度的题目,帮个忙来个关注!感谢!

image.png

猜你喜欢

转载自blog.csdn.net/weixin_44907128/article/details/120064872