暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

MySQL之单表查询成本

GrowthDBA 2021-12-24
841
这两天一直在忙ScyllaDB的集群环境,公众号文章未更几日有余。今天抽时间来学习一下MySQL单表查询成本相关的知识。
Oracle数据库中的优化器又叫查询优化器(Query Optimizer)。它是SQL分析和执行的优化工具,它负责生成、制定SQL的执行计划。Oracle的优化器有两种,基于规则的优化器(RBO:Rule-Based Optimization)与基于代价、成本的优化器(CBO:Cost-Based Optimization)。
CBO优于RBO是因为RBO是一种呆板、过时的优化器,它只认规则,对数据不敏感。毕竟规则是死的,数据是变化的,这样生成的执行计划往往不可靠。今天我们就来学习一下MySQL关于单表查询成本的相关知识。


成本概述



通过前面学习我们知道了MySQL优化器的作用:MySQL优化器会从生成的多个不同执行方案中,选择成本最低的方案去真正的执行查询。在MySQL中,一条查询语句的执行成本由两部分组成

  • I/O成本:查询数据时,需要把数据加载到内存中后再操作,从磁盘到内存这个加载的过程损耗的时间称之为I/O成本。

  • CPU成本:读取以及检测记录是否满足对应的搜索条件、对结果集进行排序等这些操作损耗的时间称之为CPU成本。

InnoDB存储引擎是以16KB大小的页(Page)作为磁盘和内存交互的基本单位,MySQL规定读取一个页(Page)花费的成本默认是1.0,读取以及检测一条记录是否符合搜索条件的成本默认是0.2

1.0、0.2这些数字称之为成本常数。注意:不管读取记录时需不需要检测是否满足搜索条件,其成本都算是0.2。




简单的成本计算



测试数据准备

我们还是先来准备一些测试数据:

--Create DatabaseCREATE DATABASE test_cost;--Create TableUSE test_cost;CREATE TABLE `table_query_cost` (    `id` INT(11) NOT NULL AUTO_INCREMENT,    `key1` VARCHAR(100),    `key2` INT(11) ,    `key3` VARCHAR(100),    `key_part1` VARCHAR(100),    `key_part2` VARCHAR(100),    `key_part3` VARCHAR(100),    `common_field` VARCHAR(100),    PRIMARY KEY (`id`),    KEY idx_key1 (`key1`),    UNIQUE KEY uq_key2 (`key2`),    KEY idx_key3 (`key3`),    KEY idx_key_part(`key_part1`, `key_part2`, `key_part3`)) Engine=InnoDB;--Create ProcedureDELIMITER ;;CREATE PROCEDURE cost_data()BEGIN    DECLARE i INT;    SET i=0;    WHILE i<10000 DO        INSERT INTO table_query_cost(key1,key2,key3,key_part1,key_part2,key_part3,common_field) VALUES(SUBSTRING(MD5(RAND()),1,2),i+1,SUBSTRING(MD5(RAND()),1,3),SUBSTRING(MD5(RAND()),1,4),SUBSTRING(MD5(RAND()),1,5),SUBSTRING(MD5(RAND()),1,6),SUBSTRING(MD5(RAND()),1,7));        SET i=i+1;    END WHILE;END;;DELIMITER ;--Call ProcedureCALL cost_data();--Query DataSELECT COUNT(*) FROM test_cost.table_query_cost;SELECT * FROM test_cost.table_query_cost LIMIT 20;

基于成本的计算过程

MySQL在真正执行查询前,优化器会找出执行SQL所有可能使用到的方案,对比之后选择执行成本最低的方案进行查询,这个成本最低的方案就是执行计划,确定好执行计划后,MySQL Server层就会调用InnoDB存储引擎提供的接口查询数据了,整个过程分为以下4步:
1、根据搜索条件,找出所有可能使用到的索引
2、计算全表扫描的代价
3、计算使用不同索引执行查询的代价

4、对比各个执行方案的代价,找出成本最低的那个进行查询

下面举个栗子🌰说明一下:比如我们有如下的SQL语句:
SELECT * FROM test_cost.table_query_cost WHERE     key1 IN ('e4', '2f', 'd7') AND     key2 > 99 AND key2 < 999 AND     key3 > key2 AND     key_part1 LIKE '%ead%' AND    common_field = 'a6c5a45';
1、根据搜索条件,找出所有可能使用到的索引
之前MySQL之单表访问方法我们知道,对于B+树索引来说,只要索引列和常数使用=、<=>、IN、NOT IN、IS NULL、IS NOT NULL、>、<、>=、<=、BETWEEN、!=(不等于也可以写成<>)或者LIKE操作符连接起来,就可以产生一个范围区间(LIKE匹配字符串前缀也行),这些搜索条件都可能使用到索引,MySQL把一个查询中可能使用到的索引称之为possible keys

分析上述SQL涉及到的搜索条件:

  • key1 IN ('e4', '2f', 'd7') ,可以使用到二级索引idx_key1

  • key2 > 99 AND key2 < 999,可以使用到二级(唯一)索引uq_key2

  • key3 > key2,搜索条件的索引列没有和常数比较,不能使用索引

  • key_part1 LIKE '%ead%',LIKE操作符和以通配符开头的字符串做比较,不能使用索引

  • common_field = 'a6c5a45',common_field字段无索引,不能使用索引

综上所述,SQL语句可能用到的索引possible keys只有idx_key1uq_key2

2、计算全表扫描的代价
InnoDB存储引擎的全表扫描就是把聚簇索引中的记录都依次和给定的搜索条件做比较,把符合搜索条件的记录加入到结果集。需要将聚簇索引对应的页面加载到内存中,然后再检测记录是否符合搜索条件。
查询成本=I/O成本+CPU成本,所以计算全表扫描的代价需要两个信息:
  • 聚簇索引总共占用的页面数

  • 表中的记录数

我们可以通过查询统计信息的方式得到:

SHOW TABLE STATUS LIKE 'table_query_cost'\G;

  • Rows对于MyISAM存储引擎的表来说,该值是准确的。对于InnoDB来说,该值是预估值。我们实际插入了10000行记录,但是查看表状态的行数信息却有10056行记录。
  • Data_length:表示表占用的存储空间字节数。MyISAM存储引擎,该值就是数据文件的大小。InnoDB存储引擎,该值就相当于聚簇索引占用的存储空间大小(单位:字节,B),也就是说可以这样计算该值的大小:
Data_length = 聚簇索引的页面数量 x 每个页面的大小

一个Page大小为16KB,由上面的公式可以得到聚簇索引的页面数量为97

聚簇索引的页面数量 = 1589248 ÷ 16 ÷ 1024 = 97

得到了聚簇索引占用的页面数量以及该表记录数的预估值,下面就可以计算全表扫描成本了,但是MySQL在真实计算成本时会进行一些微调,这些微调的值是直接硬编码到代码里的。

  • I/O成本

97 x 1.0 + 1.1 = 98.1
97是聚簇索引占用的页面数,1.0是加载一个页面的成本常数,1.1是微调值。
  • CPU成本
10056 x 0.2 + 1.0 = 2012.2
10056是统计数据中预估的表记录数,0.2是访问一条记录所需的成本常数,1.0是微调值。
  • 总成本
98.1 + 2012.2 = 2110.3
综上所述。table_query_cost表扫描所需的总成本就是2110.3

3、计算使用不同索引执行查询的代价

综上可知,可能用到的索引(possible keys)有idx_key1、uq_key2,需要分别分析单独使用这些索引执行查询的成本,最后还要分析是否可能使用到索引合并。MySQL查询优化器先分析使用唯一二级索引的成本,再分析使用普通索引的成本,所以我们也先分析uq_key2的成本,然后再看使用idx_key1的成本。

※计算使用uq_key2执行查询的成本


uq_key2的搜索条件:key2 > 99 AND key2 < 999,对应的范围区间是(99,999),因为uq_key2为二级索引,查询时会发生回表操作,对于使用二级索引 + 回表方式的查询,MySQL计算这种查询的成本依赖两个方面的数据:
  • 范围区间数量不论某个范围区间的二级索引到底占用了多少页面,查询优化器粗暴的认为读取索引的一个范围区间的I/O成本和读取一个页面是相同的。本例中使用uq_key2的范围区间只有一个:(99,999),所以相当于访问这个范围区间的二级索引付出的I/O成本就是:

1 x 1.0 = 1.0
  • 需要回表的记录数优化器需要计算二级索引的某个范围区间到底包含多少条记录,计算过程如下:
  • 步骤1:根据key2 > 99这个条件访问一下uq_key2对应的B+树索引,找到满足条件的第一条记录,称之为区间最左记录
  • 步骤2:根据key2 < 999这个条件继续从uq_key2对应的B+树索引中找出最后一条满足这个条件的记录,称之为区间最右记录
在B+数树中定位一条记录的过程非常快,常数级别,上述两个步骤性能消耗可以忽略不计。
  • 步骤3:如果区间最左记录和区间最右记录相隔不太远(相隔不大于10个页面),那就可以精确统计出满足key2 > 99 AND key2 < 999条件的二级索引记录条数。否则只沿着区间最左记录向右读10个页面,计算平均每个页面中包含多少记录,然后用这个平均值乘以区间最左记录和区间最右记录之间的页面数量就可以了。如何估计区间最左记录和区间最右记录之间有多少个页面呢?还记得MySQL之B+树索引的二级索引树形结构图吧,回顾一下。

比如区间最左记录在页34中,区间最右记录在页40中,计算区间最左记录和区间最右记录之间的页面数量就相当于计算页34和页40之间有多少页面,而每一条目录项记录都对应一个数据页,所以计算页34和页40之间有多少页面就相当于计算它们父节点(也就是页42)中对应的目录项记录之间隔着几条记录不就可以了吗。如果页34和页40之前的页面太多(对应的目录项不在同一个父节点页面中),那就继续递归进行统计,这个统计过程在父节点页面进行,所以也不是很耗费性能。
根据上述算法测得uq_key2在区间(99, 999)之间大约有86条记录。读取这86条二级索引记录需要付出的CPU成本就是:
86 x 0.2 + 0.01 = 17.21

86是需要读取的二级索引记录条数,0.2是读取一条记录成本常数,0.01是微调。

  • 步骤4:根据这些记录里的主键值到聚簇索引中做回表操作。MySQL评估回表操作的I/O成本依旧很豪放,他们认为每次回表操作都相当于访问一个页面,也就是说二级索引范围区间有多少记录,就需要进行多少次回表操作,也就是需要进行多少次页面I/O。有86条记录需要回表,所以回表操作的I/O成本就是:

86 x 1.0 = 86.0
  • 步骤5:回表操作后得到的完整用户记录,然后再检测其他搜索条件是否成立。回表操作的本质就是通过二级索引记录的主键值到聚簇索引中找到完整的用户记录,然后再检测除key2 > 99 AND key2 < 999这个搜索条件以外的搜索条件是否成立。因为我们通过范围区间获取到二级索引记录共86条,也就对应着聚簇索引中86条完整的用户记录,读取并检测这些完整的用户记录是否符合其余的搜索条件的CPU成本如下:MySQL只计算这个查找过程所需的I/O成本,也就是我们上一步骤中得到的86.0,在内存中的定位完整用户记录的过程的成本是忽略不计的。在定位到这些完整的用户记录后,需要检测除key2 > 99 AND key2 < 999这个搜索条件以外的搜索条件是否成立,这个比较过程花费的CPU成本就是:

86 x 0.2 = 17.2

86是待检测记录的条数,0.2是检测一条记录是否符合给定的搜索条件的成本常数。

使用uq_key2执行查询的成本:

  • I/O成本

1.0 + 86 x 1.0 = 87.0 (范围区间的数量 + 预估的二级索引记录条数)
  • CPU成本
86 x 0.2 + 0.01 + 86 x 0.2 = 34.41 (读取二级索引记录的成本 + 读取并检测回表后聚簇索引记录的成本)

综上,使用uq_key2索引执行查询的总成本是

87.0 + 34.41 = 121.41

※计算使用idx_key1执行查询的成本


idx_key1的搜索条件是key1 IN ('e4', '2f', 'd7'),相当于3个单点区间:['e4', 'e4']、['2f', '2f']、['d7', 'd7']。

  • 范围区间数量:使用idx_key1执行查询有3个单点区间,访问这3个范围区间的二级索引付出的I/O成本就是:

3 x 1.0 = 3.0
  • 需要回表的记录数:使用idx_key1时有3个单点区间,所以每个单点区间都需要查找一遍对应的二级索引记录数:
  • 查找单点区间['e4', 'e4']对应的二级索引记录数:方法同上述连续范围区间计算方法,不再赘述。最后得到的单点区间['e4', 'e4']对应的二级索引记录数是:42。
  • 查找单点区间['2f', '2f']对应的二级索引记录数:计算得到本单点区间对应的记录数是:39。
  • 查找单点区间['d7', 'd7']对应的二级索引记录数:计算得到本单点区间对应的记录数是:34。
这三个单点区间总共需要回表的记录数就是:
42 + 39 + 34 = 115

读取这些二级索引记录的CPU成本就是:

115 x 0.2 + 0.01 = 23.01
  • 根据这些记录里的主键值到聚簇索引中做回表操作所需的I/O成本就是:

115 x 1.0 = 115.0
  • 回表操作后得到的完整用户记录,然后再比较其他搜索条件是否成立。此步骤对应的CPU成本就是:

115 x 0.2 = 23.0
使用idx_key1执行查询的成本:
  • I/O成本
3.0 + 115 x 1.0 = 118.0 (范围区间的数量 + 预估的二级索引记录条数)
  • CPU成本
115 x 0.2 + 0.01 + 115 x 0.2 = 46.01 (读取二级索引记录的成本 + 读取并检测回表后聚簇索引记录的成本)

综上,使用idx_key1索引执行查询的总成本是

118.0 + 46.01 = 164.01

※是否有可能使用索引合并(Index Merge)

有关key1和key2的搜索条件是使用AND连接起来的,而对于idx_key1和uq_key2都是范围查询,也就是说查找到的二级索引记录并不是按照主键值进行排序的,并不满足使用Intersection索引合并的条件,所以并不会使用索引合并。
4、对比各执行方案的代价,找出成本最低的那个进行查询
  • 全表扫描的成本:2110.3
  • 使用uq_key2的成本:121.41
  • 使用idx_key1的成本:164.01
可以直观看到,使用uq_key2的成本最低,所以就使用uq_key2来执行查询。



小结



今天学习了MySQL基于CBO生成执行计划的过程——基于单表查询成本的分析和计算过程。下面来总结一下:
  • 一条SQL语句的查询成本=I/O成本+CPU成本。
  • 计算查询成本的元素包括:成本常数、微调值。
1、对于InnoDB引擎来说,加载一个页面(Page)花费的I/O成本默认是1.0、读取以及检测一条记录是否符合搜索条件的CPU成本默认是0.2,1.0、0.2这些数字称之为成本常数。
2、微调的值是直接硬编码到MySQL源码里的,因没有注释,且取值都非常小不影响最终分析结果,这个概念了解即可。
  • 计算通过全表扫描的代价的元素包括:I/O成本元素(聚簇索引的页面数量、Data_length = 聚簇索引的页面数量 x 每个页面的大小)、CPU成本元素(该表中的记录数、Rows为表行数的预估值)。全表扫描成本计算公式
全表扫描的总成本 = ((Data_length ÷ 16 ÷ 1024) × 1.0 + 1.1) + (Rows × 0.2 + 0.1)
  • 计算通过二级索引的查询代价的元素包括:I/O成本元素(范围区间的数量 + 预估的二级索引记录条数)、CPU成本元素(读取二级索引记录的成本 + 读取并检测回表后聚簇索引记录的成本)。二级索引成本计算公式

二级索引查询的总成本 = (范围区间的数量 × 1.0 + 预估的二级索引记录条数 × 1.0) + (预估的二级索引记录条数 × 0.2 + 读取并检测回表后聚簇索引记录条数 × 0.2 + 0.01)

今天就到这里吧,我们下篇见。站在巨人肩膀上,每天进步一点点。




 参考资料 




  • 小孩子4919《MySQL是怎样运行的:从根儿上理解MySQL》


end


文章转载自 GrowthDBA,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论