2022-10-27 379
回忆一下查询成本
对于一个查询来说,有时候可以通过不同的索引或者全表扫描来执行它,MySQL优化器会通过事先生成的统计数据,或者少量访问B+树索引的方式来分析使用各个索引时都需要扫描多少条记录,然后计算使用不同索引的查询成本,最后选择成本最低的那个来执行查询。
小贴士:我们之前称那种通过少量访问B+树索引来分析需要扫描的记录数量的方式称为index dive,不知道大家还有没有印象。
一个很简单的思想就是:使用某个索引执行查询时,需要扫描的记录越少,就越可能使用这个索引来执行查询。
创建场景
假如我们现在有一个表t,它的表结构如下所示:
CREATETABLEt( idINTNOTNULLAUTO_INCREMENTPRIMARYKEY, key1VARCHAR(100), common_fieldVARCHAR(100), INDEXidx_key1(key1) )ENGINE=InnoDBCHARSET=utf8;
这个表包含3个列:
现在该表中共有10000条记录:
mysql>SELECTCOUNT(*)FROMt; +----------+ |COUNT(*)| +----------+ |10000| +----------+ 1rowinset(2.65sec)
其中key1列为’a’的记录有2310条:
mysql>SELECTCOUNT(*)FROMtWHEREkey1='a'; +----------+ |COUNT(*)| +----------+ |2310| +----------+ 1rowinset(0.83sec)
key1列在’a’到’i’之间的记录也有2310条:
mysql>SELECTCOUNT(*)FROMtWHEREkey1>'a'ANDkey1<'i'; +----------+ |COUNT(*)| +----------+ |2310| +----------+ 1rowinset(1.31sec)
现在我们有如下两个查询:
查询1:SELECT*FROMtWHEREkey1='a'; 查询2:SELECT*FROMtWHEREkey1>'a'ANDkey1<'i';
按理说上边两个查询需要扫描的记录数量是一样的,MySQL查询优化器对待它们的态度也应该是一样的,也就是要么都使用二级索引idx_key1执行它们,要么都使用全表扫描的方式来执行它们。不过现实是貌似查询优化器更喜欢查询1,而比较讨厌查询2。查询1的执行计划如下所示:
#查询1的执行计划 mysql>EXPLAINSELECT*FROMtWHEREkey1='a'\G ***************************1.row*************************** id:1 select_type:SIMPLE table:t partitions:NULL type:ref possible_keys:idx_key1 key:idx_key1 key_len:303 ref:const rows:2310 filtered:100.00 Extra:NULL 1rowinset,1warning(0.04sec)
查询2的执行计划如下所示:
#查询2的执行计划 mysql>EXPLAINSELECT*FROMtWHEREkey1>'a'ANDkey1<'i'\G ***************************1.row*************************** id:1 select_type:SIMPLE table:t partitions:NULL type:ALL possible_keys:idx_key1 key:NULL key_len:NULL ref:NULL rows:9912 filtered:23.31 Extra:Usingwhere 1rowinset,1warning(0.03sec)
很显然,查询优化器决定使用idx_key1二级索引执行查询1,而使用全表扫描来执行查询2。
为什么?凭什么?同样是扫描相同数量的记录,凭什么我range访问方法就要比你ref低一头?设计MySQL的大叔,你为何这么偏心…
解密偏心原因
世界上没有无缘无故的爱,也没有无缘无故的恨。这事儿还得从索引结构说起。比方说idx_key1二级索引结构长这样:
原谅我们把索引对应的B+树结构弄了一个极度精简版,我们忽略掉了页的结构,只保留了叶子节点的记录。虽然极度精简,但是我们还是保留了一个极其重要的特性:B+树叶子节点中的记录是按照索引列的值从小到大排序的。对于二级索引idx_key1来说:
也就是说,对于所有key1值为’a’的二级索引记录来说,它们都是按照id列的值进行排序的。对于查询1:
查询1:SELECT*FROMtWHEREkey1='a';
由于查询列表是* ,也就是说我们需要通过读取到的二级索引记录的id值执行回表操作,到聚簇索引中找到完整的用户记录(为了去获取common_field列的值)后才可以将记录发送到客户端。对于所有key1列值等于’a’的二级索引记录,由于它们是按照id列的值排序的,所以:
综上所述,执行语句1时,回表操作带来的性能开销较小。
而对于查询2来说:
查询2:SELECT*FROMtWHEREkey1>'a'ANDkey1<'i';
由于需要扫描的二级索引记录对应的id值是无序的,所以执行回表操作时,需要访问的聚簇索引记录所在的数据页很大可能就是无序的,这样会造成很多随机I/O。所以如果使用idx_key1来执行查询1和查询2,执行查询1的成本很显然会比查询2低,这也是设计MySQL的大叔更钟情于ref而不是range的原因。
MySQL的内部实现
MySQL优化器在计算回表的成本时,在使用二级索引执行查询并且需要回表的情境下,对于ref和range是很明显的区别对待的:
比方对于查询2来说,需要回表的记录数是2310,因为回表操作而计算的I/O成本就是2310。
doubleworst_seeks;
这个上限值的取值是从下边两个值中取较小的那个:
比方对于查询1来说,回表的记录数是2310,按理说计算因回表操作带来的I/O成本也应该是2310。但是由于对于ref访问方法,计算回表操作时带来的I/O成本时存在天花板,会从全表记录的十分之一(也就是9912/10=991,9912为估计值)以及聚簇索引所占页面的3倍(本例中聚簇索引占用的页面数就是97,乘以3就是291)选择更小的那个,本例中也就是291。
小贴士:在成本分析的代码中,range和index、all是被分到一类里的,ref是亲儿子,单独分析了一波。不过我们也可以看到,设计MySQL的大叔在计算range访问方法的代价时,直接认为每次回表都需要进行一次页面I/O,这是十分粗暴的,何况我们的实际聚簇索引总共才97个页面,它却将回表成本计算为2310,这也是很不精确的。当然,由于目前的算法无法预测哪些页面在内存中,哪些不在,所以也就将就将就用吧~
原文链接:https://77isp.com/post/9967.html
=========================================
https://77isp.com/ 为 “云服务器技术网” 唯一官方服务平台,请勿相信其他任何渠道。
数据库技术 2022-03-28
网站技术 2022-11-26
网站技术 2023-01-07
网站技术 2022-11-17
Windows相关 2022-02-23
网站技术 2023-01-14
Windows相关 2022-02-16
Windows相关 2022-02-16
Linux相关 2022-02-27
数据库技术 2022-02-20
抠敌 2023年10月23日
嚼餐 2023年10月23日
男忌 2023年10月22日
瓮仆 2023年10月22日
簿偌 2023年10月22日
扫码二维码
获取最新动态