mysql执行计划成本分析和慢查解决方案

在用explain看某一条sql的执行计划的时候,有些索引的执行结果跟我们期望不一致,那不一致的原因是什么呢?mysql底层判断要走哪个索引的依据是什么?是基于什么成本考虑来走哪个索引?以及在了解这些原理之后,我们是否有一套针对mysql慢查治理的一般思路和解决方案?这就是本文存在的原因,也是自己在项目中总结的一些经验和沉淀。在讲执行计划成本和慢查解决方案之前,先对一些关系技术——mysql的索引基础知识做个简单概述。 

  索引是加快查询速度的核心技术,也是治理慢查的关键思路。但是有一些疑难点,很多人都是一知半解,特别是索引树上存储的数据内容比较模棱两可,现在针对这些疑难点做一个总结。

一 索引分类和索引树数据组成

索引,是基于B+数的数据结构存储数据的,主要有聚集索引,辅助索引和覆盖索引。

1-聚集索引

这里就是主键索引,每个所以都是按照主键ID进行索引的数据,在索引的叶子节点数据组成=主键ID+该行的所有数据。

比如订单表order_info的前两行记录为:

CREATE TABLE `order_info` (
  `id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL COMMENT '主键',
  `order_no` varchar(32) UNSIGNED NOT NULL DEFAULT '' COMMENT '订单号',
  `user_id` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '用户ID',
  `money` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '金额',
  PRIMARY KEY (`id`),
  UNIQUE KEY `order_no` (`order_no`),
  KEY `order_uid` (`order_no`,`user_id`)
) ENGINE=InnoDB CHARSET=utf8mb4 COMMENT='订单表';
id(主键ID)order_no(订单号)user_id(用户ID)money(金额)
1E20211111100
2E20211222200

此时主键ID包含的索引数据=id+(order_no+user_id+money),数据组成如下图所示

2-辅助索引

该索引的特点是,索引数据处理包含索引字段,还包含主键ID。比如上面订单表的辅助索引key =order_uid,除了包含其自身字段order_no和user_id外,还包含主键id,如下图所示:

3-覆盖索引

覆盖索引,是辅助索引中的一种,理解好“覆盖”二字是关键:SELECT时需要的数据,如果在索引数据里都存在,那么就不需要回表再查询其他字段信息,这样的索引叫做覆盖索引。

比如拿上面的订单表为例:

select order_no,user_id from order_info where order_no='E202111'

这时只需要查 order_no和user_id 的值,而 这两个值已经在 key=order_uid索引树上了,因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引 order_uid 已经“覆盖了”我们的查询需求,我们称为覆盖索引。

由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段

二-走上索引原则-最左匹配原则

B+ 树这种索引结构,可以利用索引的“最左前缀”,来定位记录。为了直观地说明这个概念,我们用(order_no,user_id)这个联合索引来分析。

可以看到,索引项是按照索引定义里面出现的字段顺序排序的。

 这样,比如下面的语句,因为没有走上最左的order_no的索引,最终没有走上key=order_uid的索引。原因也很简单,在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序。也就是说,如果我们想使用联合索引中尽可能多的列,查询条件中的各个列必须是联合索引中从最左边开始连续的列。如果我们仅仅按照第二列搜索,肯定无法走索引

select * from order_info where user_id=22 

三-不是所有针对索引列的查询都能用上索引

在上一个案例中,我创建了一个 order_no+user_id 的联合索引,仅搜索 order_no 时就能够用上这个联合索引。这就引出两个问题:

1)是不是建了索引一定可以用上?

2)怎么选择创建联合索引还是多个独立索引?
首先,建立一个新表student,如下:


CREATE TABLE `person` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `score` int(11) NOT NULL,
  `create_time` timestamp NOT NULL,
  PRIMARY KEY (`id`),
  KEY `create_time` (`create_time`),
  KEY `name_score` (`name`,`score`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

通过下面的存储过程循环创建 10 万条测试数据,我的机器的耗时是 140 秒(本文的例子均在 MySQL 5.7.26 中执行):


CREATE DEFINER=`root`@`%` PROCEDURE `insert_person`()
begin
    declare c_id integer default 1;
    while c_id<=100000 do
    insert into person values(c_id, concat('name',c_id), c_id+100, date_sub(NOW(), interval c_id second));
    set c_id=c_id+1;
    end while;
end

现在,我们通过几个案例来分析一下索引失效的情况。

第一,索引只能匹配列前缀。比如下面的 LIKE 语句,搜索 name 后缀为 name123 的用户无法走索引,执行计划的 type=ALL 代表了全表扫描:


EXPLAIN SELECT * FROM person WHERE NAME LIKE '%name123' LIMIT 100

把百分号放到后面走前缀匹配,type=range 表示走索引扫描,key=name_score 看到实际走了 name_score 索引: 

原因很简单,索引 B+ 树中行数据按照索引值排序,只能根据前缀进行比较。如果要按照后缀搜索也希望走索引的话,并且永远只是按照后缀搜索的话,可以把数据反过来存,用的时候再倒过来。

 第二,条件涉及函数操作无法走索引。比如搜索条件用到了 LENGTH 函数,肯定无法走索引:


EXPLAIN SELECT * FROM person WHERE LENGTH(NAME)=7

同样的原因,索引保存的是索引列的原始值,而不是经过函数计算后的值。如果需要针对函数调用走数据库索引的话,只能保存一份函数变换后的值,然后重新针对这个计算列做索引。

第三,联合索引只能匹配左边的列。也就是说,虽然对 name 和 score 建了联合索引,但是仅按照 score 列搜索无法走索引:


EXPLAIN SELECT * FROM person WHERE SCORE>45678

 原因也很简单,在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序。也就是说,如果我们想使用联合索引中尽可能多的列,查询条件中的各个列必须是联合索引中从最左边开始连续的列.

三 执行计划选择成本分析

通过前面的案例,我们可以看到,查询数据可以直接在聚簇索引上进行全表扫描,也可以走二级索引扫描后到聚簇索引回表。看到这里,你不禁要问了,MySQL 到底是怎么确定走哪种方案的呢。其实,MySQL 在查询数据之前,会先对可能的方案做执行计划,然后依据成本决定走哪个执行计划。

这里说的成本,包括IO成本和CPU成本:

  • O 成本,是从磁盘把数据加载到内存的成本。默认情况下,读取数据页的 IO 成本常数是 1(也就是读取 1 个页成本是 1)

  • CPU 成本,是检测数据是否满足条件和排序等 CPU 操作的成本。默认情况下,检测记录的成本是 0.2。

基于此,我们分析下全表扫描的成本。

全表扫描,就是把聚簇索引中的记录依次和给定的搜索条件做比较,把符合搜索条件的记录加入结果集的过程。那么,要计算全表扫描的代价需要两个信息:

  • 聚簇索引占用的页面数,用来计算读取数据的 IO 成本;
  • 表中的记录数,用来计算搜索的 CPU 成本。

那么,MySQL 是实时统计这些信息的吗?其实并不是,MySQL 维护了表的统计信息,可以使用下面的命令查看:


SHOW TABLE STATUS LIKE 'person'

 可以看到:

  • 数据长度=4734976字节,每页大小为16K,则大概有289页,所以IO成本=289*1=289
  • 总行数=100086行,所以CPU成本是100086*0.2=20017左右。

所以,全表扫描的总成本=IO成本+CPU成本=289+20017= 20306 左右。

接下来,我还是用 person 表这个例子,和你分析下 MySQL 如何基于成本来制定执行计划。现在,我要用下面的 SQL 查询 name>‘name84059’ AND create_time>‘2020-01-24 05:00:00’


EXPLAIN SELECT * FROM person WHERE NAME >'name84059' AND create_time>'2020-01-24 05:00:00'

执行计划的结果是全表扫描:

只要把 create_time 条件中的 5 点改为 6 点就变为走索引了,并且走的是 create_time 索引而不是 name_score 联合索引,主要原因还是改为6点后扫描的行数少了,成本变低了。

我们可以得到两个结论:

  • MySQL 选择索引,并不是按照 WHERE 条件中列的顺序进行的
  • 即便列有索引,甚至有多个可能的索引方案,MySQL 也可能不走索引

其原因就是,MySQL 并不是猜拳决定是否走索引的,而是根据成本来判断的。虽然表的统计信息不完全准确,但足够用于策略的判断了。不过,有时会因为统计信息的不准确或成本估算的问题,实际开销会和 MySQL 统计出来的差距较大,导致 MySQL 选择错误的索引或是直接选择走全表扫描,这个时候就需要人工干预,使用强制索引了。

四-慢查解决方案

出现慢查的主要原因,一般都是建立索引不当或者sql的条件不满足索引匹配规则的问题。所以针对这些问题,总结出慢查问题的一般解决方案:

1)检查建立索引是否合理,索引的顺序是否合理

  • 索引的顺序很重要,辅助索引的字段区分度应该是从左到右,呈递增趋势。最理想的情况是一个sql只需检索出一条数据,这样即使回表也只会一次。最差的情况,就是索引有跟没有一个样,就是通过这个索引根本区分不出想要的数据,比如索引:country_sex,用性别做左右字段,只能查出一半的数据,命中率很低。
  • 索引的字段大小不要太大,不然会占用过多的内存。

2)尽量使用覆盖索引,避免回表成本。

  • 如果要查询的数据都在索引树里,就可以避免根据辅助索引上的主键ID来回表查其他数据。

3)涉及到order by 的,尽量走上索引,还有查询的字段越少越好。

 order by 这个动作,可能在内存中完成,也可能需要使用外部排序,这取决于排序所需的内存和参数 sort_buffer_size。

sort_buffer_size,就是 MySQL 为排序开辟的内存(sort_buffer)的大小。如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。

所以,如果你查询的字段比较多,占用的内存>sort_buffer_size,这样就会导致sort_buffer_size空间不够,最后需要额外的磁盘临时文件来辅助排序。如果查询的字段的内存<sort_buffer_size,则只需要在内存排序即可,速度比较快。

另外,如果能够走上索引,索引树的数据天然有序,不需要再排序,固然速度是最快的。

以上就是本文的分享,也是自己的一些总结和沉淀,希望通过记录的方式能够让自己学过的更深刻,更扎实;同时,也希望看过此文的博友也能有所收获,谢谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值