阿里四面:为何MySQL没有使用建立的索引?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了阿里四面:为何MySQL没有使用建立的索引?相关的知识,希望对你有一定的参考价值。
参考技术A mysql选了个不恰当索引而导致的慢查询。某晚收到了线上数据库的频繁报警,数据库突然大量慢查询,导致每个数据库连接执行一个慢查询都要耗费很久。这还导致突然过来的很多查询需要让MySQL开辟更多连接,因此报警也告诉我们,数据库的连接剧增,而且每个连接都打满,每个连接都要执行一个慢查询。
接着DB的连接全部打满,无法开辟新连接,但还持续的有新的查询请求,导致DB无法处理新查询,很多查询发到DB直接就阻塞然后超时,导致商品系统频繁的报警,出现大量DB查询超时报错的异常。
这意味着商品数据库及商品系统濒临崩溃,大量慢查询耗尽DB连接资源,而且一直阻塞在数据库里执行,数据库没法执行新的查询,商品数据库无法执行查询,用户没法使用商品系统,也就没法查询和筛选电商网站里的商品了。
报警时机又正是晚高峰,虽说商品数据有多级缓存架构,但下单过程中,还是会大量请求商品系统,所以晚高峰时,商品系统本身TPS大致几千。因此发现数据库的监控里显示每min的慢查询超过10w+:商品系统大量的查询都变成了慢查询。
慢查询主要就是如下语句:
该语句执行的商品表里大致1亿左右数据量,该量级已稳定很长时间,主要也就是这么多商品,但上面语句居然一执行就是几十s!基本上数据库的连接全部被慢查询打满,一个连接要执行几十s的SQL,然后才能执行下一个SQL,此时数据库基本就废了,没法执行什么查询。所以商品系统本身也报警查询数据库的超时异常。
经常用到的查询字段肯定都建了索引,即index_category(catetory,sub_category)肯定存在。因为如果你一旦用上了品类索引,按品类和子类去在索引里筛选:
理论上执行速度很快,即使表有亿级数据,但也不应超过1s。但跑了几十秒,说明肯定没用那个索引,看执行计划:
possible_keys=index_category的,key=PRIMARY,Extra=Using where
就是在扫描主键索引,还用where条件里的两个字段做筛选,所以这么扫描就会耗费几十s。
为快速解决问题,使用force index语法,强制改变MySQL自动选择不恰当聚簇索引进行扫描的行为:
再次执行SQL,仅耗费100多ms。
所以若MySQL使用了错误的执行计划,那就force index语法改变它。
但案例还有问题:
该表是个亿级数据量大表,那index_category二级索引也比较大,所以此时MySQL觉得如果从index_category二级索引查找符合where条件的一波数据,接着还得回表。因为要select *,所以必然涉及回表,但在回表前,必然要做完order by id desc limit xx,xx操作。
举个例子,根据where category='xx' and sub_category='xx',从index_category二级索引里查找出一波数据,假设几万条,
因为二级索引包含主键id,就得按order by id desc,对这几万条数据基于临时磁盘文件进行filesort磁盘排序,排序后,再按limit xx,xx语法将指定位置的几条数据拿出来,假设limit 0,10,那么就是把10条数据拿出来。拿出来10条数据之后,再回到聚簇索引根据id查,把这10条数据的完整字段都查出来,这就是MySQL认为如果你使用index_category的话,可能会发生的一个情况。
所以他担心,你根据
从index_category二级索引里查出来的数据太多了,还得在临时磁盘里排序,可能性能很差,因此MySQL就把这种方式判定不太好。
因此他选择直接扫描主键的聚簇索引,因为聚簇索引按id值有序,所以扫描时,直接按order by id desc倒序得顺序扫描即可,然后因为他知道你是
也就知道你仅仅只要拿到10条数据就行了。所以他在按序扫描聚簇索引时,就会对每条数据都采用Using where,跟
条件进行比对,符合条件的就直接放入结果集里去,最多就是放10条数据进去就可以返回了。
此时MySQL认为,按顺序扫描聚簇索引,拿到10条符合where条件的数据,应该很快,很可能比使用index_category二级索引更快,因此此时他就采用了扫描聚簇索引的这种方式。
这SQL之前在线上系统运行一直没问题,即之前在线上系统而言,即使采用扫描聚簇索引,该SQL也确实运行不慢,最起码是不会超过1s。
为何突然大量报慢查询,耗时几十s?因为之前
条件通常有返回值,即根据条件里的取值,扫描聚簇索引,通常都是很快就能找到符合条件的值并返回,所以之前其实性能也没啥问题。
但后来可能是商品系统里的运营人员,在商品管理的时候加了几种商品分类和子类,但是这几种分类和子类的组合其实没有对应的商品,导致很多用户使用这种分类和子类去筛选商品
条件实际上是查不到任何数据的!所以扫描聚簇索引时,怎么都扫不到符合条件的结果,一下就把聚簇索引全部扫了一遍,等于上亿数据全表扫描一遍,都没找到符合where category='新分类' and sub_category='新子类'这个条件的数据。
正因如此,才导致这个SQL语句频繁的出现几十秒的慢查询,进而导致MySQL连接资源打满,商品系统崩溃!
SQL调优并不太难,核心是看懂SQL执行计划,理解慢的原因,然后想法解决,本案例就得通过force index语法来强制某个SQL用我们指定的索引。
为何阿里不推荐MySQL使用join?
- DBA禁用join
- 若有两个大小不同的表做join,用哪个表做驱动表?
今天这篇文章,我就先跟你说说join语句到底是怎么执行的,然后再来回答这两个问题。
示例表:
- 往表t2里插入了1000行数据
- 在表t1里插入的是100行数据
可见,两表都有一个主键索引id和一个索引a
Index Nested-Loop Join
select *
from t1 straight_join t2 on (t1.a = t2.a);
若直接使用join语句,MySQL优化器可能会选择表t1或t2作为驱动表,这会影响我们分析SQL语句的执行过程。为便于分析执行过程中的性能,改用straight_join让MySQL使用固定的连接方式执行查询,这样优化器只会按照我们指定的方式去join。所以,该语句里:
-
t1 是驱动表
-
t2是被驱动表
-
使用索引字段join的 explain结果
t2的字段a上有索引,join过程用了该索引,因此该语句执行流程:
- 从t1读入一行数据 R
- 从数据行R中,取出a字段到t2里查找
- 取出t2中满足条件的行,跟R组成一行,作为结果集一部分
- 重复执行步骤1到3,直到t1的末尾循环结束
这个过程是先遍历t1,然后根据从t1中取出的每行数据中的a值,去t2中查找满足条件的记录。形式上和我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引,称之为“Index Nested-Loop Join”,NLJ。
- Index Nested-Loop Join算法的执行流程
TODO
该流程:
- 对驱动表t1做了全表扫描,需扫描100行
- 对于每一行R,根据a字段去t2查找,这是树搜索。由于构造数据一一对应,因此每次搜索过程都只扫描一行,共扫描100行
- 所以,整个执行流程,总扫描行数是200
所以能不能使用join?
假设不使用join,那就只能用单表查询:
select * from t1
查出t1所有数据,这里有100行。
循环遍历这100行数据:
- 从每一行R取出字段a的值$R.a
- 执行select * from t2 where a=$R.a
- 把返回的结果和R构成结果集的一行
该查询过程,也扫描了200行,但共执行了101条语句,比join多了100次交互。而且客户端还要自己拼接SQL语句和结果。
这性能还不如直接join。
怎么选择驱动表?
该示例中,驱动表t1走全表扫描,被驱动表t2走树搜索。
假设被驱动表行数M。每次在被驱动表查一行数据,要先搜索索引a,再搜索主键索引。每次搜索一棵树的时间复杂度log2M
,所以在被驱动表上查一行的时间复杂度是 2*log2M
。
假设驱动表行数N,执行过程就要扫描驱动表N行,然后对每一行,到被驱动表上匹配一次。
因此整个执行过程,时间复杂度是 N + N*2*log2M
。N扩大1000倍,扫描行数就会扩大1000倍;而M扩大1000倍,扫描行数扩大不到10倍。
可见,N严重影响扫描行数,应该让小表做驱动表。
小结
- 使用join语句,性能比强行拆成多个单表执行SQL语句的性能要好
- 如果使用join语句的话,需要让小表做驱动表。
这些结论的前提是“可以使用被驱动表的索引”。
若被驱动表用不上索引呢?
Simple Nested-Loop Join
select * from t1 straight_join t2 on (t1.a=t2.b);
t2的b无索引,所以每次到t2去匹配时,就要做一次全表扫描。
但这样,该SQL就要扫描t2 100次,共扫描100*1000=10万行。若t1和t2都是10万行的表,就要扫描100亿行!
当然,MySQL也没有使用这个Simple Nested-Loop Join算法,而使用“Block Nested-Loop Join”算法,BNL。
Block Nested-Loop Join
被驱动表无可用索引时的算法流程:
- 把t1的数据读入线程内存join_buffer中,由于我们这个语句中写的是select *,因此是把整个表t1放入了内存
- 扫描t2,把t2中的每一行取出来,对比join_buffer数据,满足join条件的,作为结果集的一部分返回。
-
BNL执行流程
TODO -
不使用索引直接join的执行计划
t1、t2都做了次全表扫描,因此总扫描行数1100。由于join_buffer是以无序数组组织,因此对t2中的每一行,都要做100次判断,总共需要在内存中做的判断次数是:100*1000=10万次。
若使用SNL算法查询,扫描行数也是10万行。因此,时间复杂度一样的。但BNL算法的这10万次判断是内存操作,速度上会快很多,性能较好。
那么此时哪个表做驱动表呢?
假设小表的行数是N,大表的行数是M,则在该算法里:
- 两个表都做一次全表扫描,总扫描行数:
M+N
- 内存中判断次数
M*N
所以调换M和N无差异,所以选择哪个做驱动表,执行耗时都一样。
- 若表t1是个大表,join_buffer放不下咋办?
join_buffer的由参数join_buffer_size设定,默认256k。若放不下t1的所有数据,就会分段放。
把join_buffer_size改成1200,再执行:
select *
from t1 straight_join t2 on (t1.a = t2.b);
执行过程如下:
- 扫描t1,顺序读取数据行放入join_buffer,放完第88行join_buffer满了,继续第2步
- 扫描t2,把t2中的每一行取出来,跟join_buffer中的数据做对比,满足join条件的,作为结果集的一部分返回
- 清空join_buffer
- 继续扫描t1,顺序读取最后的12行数据放入join_buffer中,继续执行第2步
step4、5,表示清空join_buffer再复用。这也能看出该算法的确是分块join。
此时由于t1被分成两次放入join_buffer,导致t2会被扫描两次。虽然分成两次放入join_buffer,但判断等值条件的次数不变,依然是(88+12)*1000=10万次。
此时如何选择驱动表?
假设,驱动表数据行数N,需分K(K不是常数,N越大K就会越大,因此把K表示为λ*N,显然λ的取值范围是(0,1))段完成,被驱动表数据行数M。
所以,该算法执行过程:
- 扫描行数
N+λNM - 内存判断
N*M次
显然,内存判断次数是不受选择影响。观察扫描行数,在M和N确定时,N越小,结果越小。
所以应该让小表当驱动表。
在N+λ*N*M
中,λ才是影响扫描行数的关键因素,越小越好。
N越大,分段数K越大。那么,N固定时,什么会影响K呢?( 即λ的大小)答案是join_buffer_size:
join_buffer_size越大,一次可放入行越多,分段数越少,被驱动表全表扫描次数越少
所以若你的join很慢,就把join_buffer_size加大。
综上:
能不能使用join
若使用INL,当可以用被驱动表的索引,是没问题的。
若使用BNL,扫描行数就会过多。尤其是在大表上的join,这样可能要扫描被驱动表很多次,会占用大量的系统资源。所以这种join禁用。
所以判断要不要使用join,就是看explain结果里面,Extra字段里面有没有出现“Block Nested Loop”。
若使用join,大表or 小表做驱动表?
- INL:
选择小表做驱动表 - BNL:
- 在join_buffer_size足够大时,一样
- 在join_buffer_size不够大时(常见情况),选择小表做驱动表
所以,该问题最终结论:永远使用小表做驱动表。
什么叫“小表”?
若加上 where t2.id<=50
:
select *
from t1 straight_join t2 on (t1.b = t2.b)
where t2.id <= 50;
select *
from t2 straight_join t1 on (t1.b = t2.b)
where t2.id <= 50;
使用 b 是为了让被驱动表都用不上索引。
但若用第二个语句,join_buffer只需放入t2的前50行,显然更好。所以这里“t2的前50行”是那个相对小的表,即“小表”。
再看个例子:
select t1.b, t2.*
from t1 straight_join t2 on (t1.b = t2.b)
where t2.id <= 100;
select t1.b, t2.*
from t2 straight_join t1 on (t1.b = t2.b)
where t2.id <= 100;
该例中,t1、t2都只有100行参与join。但这俩语句每次查询放入join_buffer的数据不同:
- t1只查字段b,因此若把t1放到join_buffer,只需放入b值
- t2需要查所有字段,若把t2放到join_buffer,就要放入所有字段
所以应该选择t1作为驱动表。该例中,“只需要一列参与join的t1”是相对的小表。
在决定哪个表做驱动表时,应该是两个表按各自条件过滤,过滤完后,计算参与join的各个字段的总数据量,数据量小的那个表,就是“小表”,将其作为驱动表。
以上是关于阿里四面:为何MySQL没有使用建立的索引?的主要内容,如果未能解决你的问题,请参考以下文章