在一条单表查询语句真正执行之前,<font color="#c41230">MySQL</font>的查询优化器会找出执行该语句所有可能使用的方案,对比之后找出成本最低的方案,这个成本最低的方案就是所谓的<font color="#c41230">执行计划</font>,之后才会调用存储引擎提供的接口真正的执行查询
举一个栗子
表结构
查询语句
1. 根据搜索条件,找出所有可能使用的索引
只有<font color="#c41230">索引列和常数</font>使用运算符连接起来,产生一个范围区间,才可能使用到该索引,possible keys
key1 IN ('a', 'b', 'c')
单点区间
key2 > 10 AND key2 < 1000
连续区间
key3 > key2
Key3与非常数连接
key_part1 LIKE '%hello%'
非前缀模糊匹配
所以可能用到的索引列为 key1 和 Key2
2. 计算全表扫描的代价
全表扫描即将聚簇索引从对应的页面加载到内存,然后检测记录是否满足条件
如何计算
查看表统计信息
SHOW TABLE STATUS LIKE 'single_table'\G
Rows: 9693
表示表中的记录条数(预估值)
Data_length: 1589248
表示表占用的存储空间字节数<br>对于使用<font color="#c41230">InnoDB</font>存储引擎的表来说,该值就相当于聚簇索引占用的存储空间大小<br>
聚簇索引叶子节点占用的页面数量计算
Data_length / 16KB (也就是/16 / 1024 ) = 97
因此IO成本为
97(聚簇索引页面数) x 1.0 (加载一个页面数量) + 1.1(微调值) = 98.1
因为CPU成本为
9693(预估记录数) x 0.2(条件过滤) + 1.0(微调值) = 1939.6
总成本为
IO成本 + CPU成本 = 2037.7
3.1 计算使用不同索引【idx_key2】执行查询的代价
计算二级索引成本的大步骤为<br>1. 计算搜索二级索引树中满足条件的记录<br>2. 回表查询
搜索二级索引树代价计算
范围区间占用页面数量(用于计算IO成本)
查询优化器粗暴的认为读取索引的<font color="#c41230">一个范围区间的I/O成本</font>和<font color="#c41230">读取一个页面</font>是相同的
需要回表的记录数(用于计算CPU成本)
如何计算idx_key2在(10, 1000)这个范围区间中包含多少二级索引记录
【IF】当左边界记录和右边界记录相隔较小时
直接遍历这些页面的PAGE HEADER中的PAGE_N_RECS字段(记录该页面有多少条数据)<br>可以获得精确值
【IF】当左边界记录和右边界记录相隔较大时
从左边界所在页面向右读取10个页面,计算平均每个页面中包含多少记录。再乘以左边界和右边界之间的页面数量即可
左边界和右边界之间的页面数量可以从B+树中的父节点获取(若跨越太多页面则需要递归)
假设区间中拥有95条记录,则成本计算为
IO成本
1(范围区间页面数量) x 1.0 = 1.0
CPU成本
95(范围区间记录数量) x 0.2 + 0.01 = 19.01
总成本 = IO成本 + CPU成本 = 1.0 + 19.01 = 20.01
回表查询代价计算
回表查询聚簇索引需加载的页面数量(用于计算IO成本)
设计MySQL的大叔评估回表操作的I/O成本依旧很豪放,他们认为<font color="#c41230">每次回表操作都相当于访问一个页面<br></font>
回表查询定位页面后,需要定位记录并且判断其他过滤条件(用于计算CPU成本)
成本计算规则
IO成本
95(回表记录数量) * 1.0 = 95
CPU成本
95(回表记录数量) * 0.2 = 19
总成本
总成本的IO成本+ CPU成本 =95 + 19 = 114
使用索引【idx_key2】执行查询的代价
IO总成本
搜索二级索引树IO成本 + 回表查询IO成本 = 1 + 95 = 96
CPU总成本
搜索二级索引树CPU成本 + 回表查询CPU成本
95 x 0.2 + 0.01 + 95 x 0.2 = 38.01
总成本
IO总成本 + CPU总成本 = 96.0 + 38.01 = 134.01
3.2 计算使用不同索引【idx_key2】执行查询的代价
步骤同上
3.3 是否会使用到合并索引,因为上面索引列和常数都不是等值匹配,所以就不涉及啦
4. 对比各种执行方案的代价,找出成本最低的那个
基于索引统计数据的成本计算
有时候使用索引执行查询时会有许多单点区间,比如使用<font color="#c41230">IN</font>语句就很容易产生非常多的单点区间
SELECT * FROM single_table WHERE key1 IN ('aa1', 'aa2', 'aa3', ... , 'zzz');
设计MySQL的大叔把这种通过直接访问索引对应的B+树来计算某个范围区间对应的索引记录条数的方式称之为<font color="#c41230">index dive</font>
如果上面的单点区间很多,那么如果都执行index dive操作,则成本会非常高,比全表扫描还恐怖得多
以上问题如何解决
使用统计数据<br>统计使用二级索引树定位到记录后(由于不唯一,所以可能对应多条记录),然后在回表后可能对应多少条记录<br>也就是统计一个值的重复次数<br>
一个值的重复次数 ≈ Rows(总记录数) ÷ Cardinality (索引列不重复值得个数)<br>9693 ÷ 968 ≈ 10(条)<br>
Rows 可从SHOW TABLE STATUS,统计表数据信息中获取
Cardinality 可从SHOW INDEX,统计索引数据信息中获取
所以按照上面的计算,加入IN语句里面有2000个单点区间,那么就需要回表 2000*10次
至于什么时候用统计数据,由eq_range_index_dive_limit环境变量决定,默认是200