背景
MySQL在执行一个查询时,可以有不同的执行方案,MySQL的执行成本由I/O和CPU成本两个方便组成。
- I/O成本
存储引擎将数据和索引存储在磁盘,当查询时,需要现将数据或索引加载到内存中,人后在进行操作。这个从磁盘到内存的加载过程损耗的时间成为I/O成本。 - CPU成本
读取记录以及检测记录是否满足对应的检索条件、对结果集进行排序等,这些操作损耗的时间成为CPU成本。
对InnoDB存储引擎来说,页是磁盘和内存进行交互的基本单位(MySQL默认页面大小为16KB)。
MySQL规定读取一个页面花费的成本默认为1.0,读取以及检测一条记录是够符合检索条件的成本默认为0.2。1.0和0.2这些数字成为成本常数。
计算成本流程
* 根据检索条件,找出所有可能使用的索引;
* 计算全表扫描的代价;
* 计算使用不同索引执行查询的代价;
* 对比各种执行方案的代价,找出成本最低的那个方案。
成本计算示例
数据库表示例和检索语句示例
数据库示例
CREATE TABLE `t_test_table` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`key1` varchar(20) NOT NULL COMMENT 'key1',
`key2` varchar(11) NOT NULL COMMENT 'key2',
`key3` varchar(200) DEFAULT NULL COMMENT 'key3',
`key4` varchar(32) NOT NULL COMMENT 'key4',
PRIMARY KEY (`id`)
KEY `idx_key1` (`key1`),
KEY `idx_key2` (`key2`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='测试表'
检索语句示例
select * from where t_test_table where key1 = '104' and key2 = '50' and key3 = '1';
根据检索条件,找出所有可能使用的索引
* key1 = '104' :存在二级索引“idx_key1”
* key2 = '50' : 存在二级索引“idx_key2”
* key3 = '1':不存在索引;
计算全表扫描的代价
全表扫描的意思吧局促索引中的记录都一次与给定的检索条件进行比较,并把符合检索条件的记录加入到检索结果集中。所以需要将居所索引的对用页面加载到内存中,注意检测记录是否符合检索条件。由于查询成本=I/O成本 + CPU成本,所以计算全表扫描的代价时需要聚簇索引占用的页面数及查询表中的记录数。
MySQL为每个表维护了一系列的统计信息,并提供了“show tables staus”语句来查看表的统计信息。如查看表t_biz_order的统计信息
参数说明:
- Rows:表中的记录条数;
- Data_length:表占用存储控件字节数,Data_length=页面数量*每个页面的大小;
根据上述信息,及表的页面默认大小为16KB,计算聚簇索引的页面数量:
聚簇索引的页面数量 = 491520 / (16 * 1024) = 30
MySQL在真正计算成本时会进行一些微调,这些微调至是直接硬编码到代码中。微调值十分小,不影响分析过程,本文计算过程中直接忽略,采用约等于(≈)方式展示
- I/O成本:30 * 1.0 + 微调值 ≈ 30
- CPU成本:1414 * 0.2 + 微调值 ≈ 282.8
- 总成本:30 + 282.8 ≈ 312.8
计算说明:上述计算过程中30为聚簇索引占用的页面数,1.0表示加载一个页面所用的成本常数,1414表示统计表中的记录数,0.2是指访问一条记录所需的成本常数,微调值则为微调常数。
计算使用不同索引执行查询的代价
- 前文已列举查询可能使用索引的全部情况,钱数查询可能使用到“idx_key1”和“idx_key2”两个索引。MySQL查询优化器先分析使用唯一二级索引的成本,在分析普通索引的成本。但本文示例中没有唯一二级索引,所以直接分析“idx_key1”索引的成本
“idx_key1”索引的成本
- 对于二级索引的查询时,方式为先查询二级索引再回表的方式。计算这种查询的成本时需要知道二级索引扫描区间数量及需要回表的记录数。
扫描区间数量
MySQL查询优化器认为读取索引的一个扫描区间的I/O成本与读取一个页面的I/O成本是相同的。本例中,使用“idx_key1”索引扫描区间只有一个[104, 104],所以相当于扫描这个区间的二级索引付出的I/O成本就是1 * 1.0 = 1.0
需要回表的记录数
二级索引扫描区间包含多少记录并非直接从记录的左侧直接遍历到区间的最右侧,计算具体数量过程如下:
- 找到检索条件的最左侧记录,常数级别,消耗忽略不计;
- 找到检索条件的最右侧记录,常数级别,消耗忽略不计;
- 区间最左侧记录和最右侧记录,页面号相隔不远的情况下,计算两个页面之间的页面记录总数并相加;
- 区间最左侧记录和最右侧记录,页面号相隔较远的情况下,根据B+树的特性,分别查找左侧记录和右侧记录的父节点,计算两个页面之间的记录数
根据记录的主键值到聚簇索引中执行回表操作
- MySQL认为每次回表操作都相当于访问一次页面,也就是说二级索引扫描区间有多少记录,就需要进行多少次回表操作,也就是要进行多少次页面I/O。所以回表操作带来的I/O成本就是301 * 1.0 = 301.0。
回表操作,获取完整记录,再检测其他条件是够成立
回表操作的本质就是通过二级索引记录的主键值到聚簇索引中找到完整的记录,然后再检测“unit_id = 147”这个搜索条件以外的其他条件是否都成立。已知满足的条件的二级索引记录是301条,所以读取并检测这些完整记录所需要的CPU成本为301 * 0.2 = 60.2 。
所以本例中使用“key1”索引查询的成本如下:
- I/O成本:1.0 + 301 * 1.0 = 302.0 (扫描区间的数量 + 预估的二级索引记录条数)
- CPU成本:301 * 0.2 + 301 * 0.2 = 120.4
- 总成本:302.0 + 120.4 = 422.4
计算说明:上述计算过程中301为满足检索条件的二级索引的记录数量,1.0表示加载一个页面所用的成本常数,0.2是指访问一条记录所需的成本常数。
“key2”索引的成本
“key2”索引与“idx_biz_order_unit”索引一样,均为普通二级索引,所以其计算索引成本与之前计算方式一致,这里直接给出结论。
- I/O成本:1.0 + 8 * 1.0 = 9.0 (扫描区间的数量 + 预估的二级索引记录条数)
- CPU成本:8 * 0.2 + 8 * 0.2 = 3.2
- 总成本:9.0 + 3.2 = 12.2
计算说明:上述计算过程中8为满足检索条件的二级索引的记录数量,1.0表示加载一个页面所用的成本常数,0.2是指访问一条记录所需的成本常数。
对比各种执行方案的代价,找出成本最低的那个方案。
- 全表扫描成本:312.8
- 使用索引“key1”成本:422.4
- 使用索引“key2”成本:12.2
很显然使用索引“key2”成本最低,所以选择“key2”索引执行查询。
使用explain查询执行计划
explain select * from where t_test_table where key1 = '104' and key2 = '50' and key3 = '1';
Intersection索引合并简介
- 细心的小伙伴已经发现,我们自己计算成本的结果与使用explain执行计划所得到的结果不一致。我们分析得到的成本为12.2,而explain执行计划得到的成本为9.6。这是因为MySQL优化器在优化SQL时使用了Intersection索引合并。
我们知道示例的SQL语句可能执行的索引有两个“key2”和“key1”。上述索引执行过程中均需要查询二级索引然后再根据二级索引记录的主键进行一次回表。Intersection索引合并指的是从不同索引中扫描到的记录的id值取交集,只为这些id值执行回表操作。
Intersection索引合并条件:每个索引中获取到的二级索引记录都是按主键值排序的。
不同索引命中的主键id如下:
- I/O成本:1.0 + 8 * 1.0 = 9.0 (扫描区间的数量 + 预估的二级索引记录条数)
- CPU成本:8 * 0.2 + 4 * 0.2 = 2.4
- 总成本:9.0 + 2.4 = 11.4
计算成本也不完全一致,猜测MySQL在针对索引合并优化时有其他的成本计算方式?