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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群版 2核4GB 100GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用版 2核4GB 50GB
简介: 大家好前面我们大概了解了索引如何选择以及利弊问题。今天介绍一下强大的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

image.png

图 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*/

image.png

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

可以看到,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影响

image.png

图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截断。


相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
20天前
|
关系型数据库 分布式数据库 数据库
PolarDB产品使用合集之优化器对索引的阈值一般是多少
PolarDB是阿里云推出的一种云原生数据库服务,专为云设计,提供兼容MySQL、PostgreSQL的高性能、低成本、弹性可扩展的数据库解决方案,可以有效地管理和优化PolarDB实例,确保数据库服务的稳定、高效运行。以下是使用PolarDB产品的一些建议和最佳实践合集。
|
8月前
|
存储 自然语言处理 关系型数据库
MySQL学习笔记-优化器选索引的因素
MySQL学习笔记-优化器选索引的因素
136 0
|
存储 SQL 关系型数据库
如何优雅的给字段加索引,能引导优化器走索引?
大家好前面我们大概了解了MySQL为什么会选错索引。今天介绍一下如何巧妙的给字符串字段加索引提高查询性能。
如何优雅的给字段加索引,能引导优化器走索引?
|
关系型数据库 MySQL 索引
MySQL · 捉虫动态 · order by limit 造成优化器选择索引错误
问题描述 bug 触发条件如下: 优化器先选择了 where 条件中字段的索引,该索引过滤性较好; SQL 中必须有 order by limit 从而引导优化器尝试使用 order by 字段上的索引进行优化,最终因代价问题没有成功。 复现case 表结构 create table t
7661 0
|
算法 关系型数据库 C语言
PostgreSQL 当有多个索引可选时,优化器如何选择
标签 PostgreSQL , 索引 , 复合索引 , 选择 , 成本 , 优化器 背景 当一个表有很多索引时,并且一个QUERY可以使用到其中的多个索引时,数据库会如何做出选择?最终选择哪个,或者哪几个索引呢? 《PostgreSQL 多查询条件,多个索引的选择算法与问题诊断方法》 选择单个索引时,PATH可以选择index scan , index only scan, bitmap scan。
2941 0
|
SQL 关系型数据库 索引
PgSQL · 源码分析 · PG 优化器中的pathkey与索引在排序时的使用
概要 SQL在PostgreSQL中的处理,是类似于流水线方式的处理,先后由: 词法、语法解析,生成解析树后,将其交给语义解析 语义解析,生成查询树,将其交给Planner Planner根据查询树,生成执行计划,交给执行器 执行器执行完成后返回结果 数据库优化器在生成执行计划的时候,优化器会考虑是否需要使用索引,而使用了索引之后,则会考虑如何利用索引已经排过序的特点,来优化相关的排序,比如ORDER BY / GROUP BY等。
1662 0
|
SQL 关系型数据库 索引
PgSQL · 源码分析 · PG 优化器中的pathkey与索引在排序时的使用
概要 SQL在PostgreSQL中的处理,是类似于流水线方式的处理,先后由: 词法、语法解析,生成解析树后,将其交给语义解析 语义解析,生成查询树,将其交给Planner Planner根据查询树,生成执行计划,交给执行器 执行器执行完成后返回结果 数据库优化器在生成执行计划的时候,优化器会考虑是否需要使用索引,而使用了索引之后,则会考虑如何利用索引已经排过序的特点,来优化相
1543 0