为什么就查了一行数据,执行那么慢?

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群版 2核4GB 100GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用版 2核4GB 50GB
简介: 今天主要介绍一下查了一行数据,为什么慢到人发慌。剖析一下MySQL的底层运行流程!

案例


登录校验


我不知道大家有没有遇到过这种情况。一个APP客户端在登录用户的时候,明明只查了一条数据,为什么那么慢呢?如何优化这类问题?如何解决这类问题呢?我们接下来分析一下!

分析之前先解决一下预备性问题。如下图,执行了SQL语句,按正常结论的话,应该是扫描全表的数量的。那这里为啥是3行呢?

explain SELECT * FROM `t_vip` where vipIphone='18360520588'

image.png

where提取

3行是因为where的缘故。介绍一下where后面SQL的那些事情。


Index Key

用于确定SQL查询在索引中的连续范围(起始范围+结束范围)的查询条件,被称之为Index Key。由于一个范围,至少包含一个起始与一个终止,因此Index Key也被拆分为Index First Key 和 Index Last Key,分别用于定位索引查找的起始,以及索引查询的终止条件。体现在Key Length。

image.png


Index First Key

用于确定索引查询的起始范围。

提取规则:从索引的第一个键值开始,检查其在where条件中是否存在,

1)若存在并且条件是=、>=,则将对应的条件加入Index First Key之中,继续读取索引的下一个键值,使用同样的提取规则;

2)若存在并且条件是>,则将对应的条件加入Index First Key中,同时终止Index First Key的提取;

3)若不存在,同样终止Index First Key的提取。

针对上面的SQL,应用这个提取规则,提取出来的Index First Key为(b >= 2, c > 1)。由于c的条件为 >,提取结束,不包括d。


Index Last Key

Index Last Key的功能与Index First Key正好相反,用于确定索引查询的终止范围。

提取规则:从索引的第一个键值开始,检查其在where条件中是否存在,

1)若存在并且条件是=、<=,则将对应条件加入到Index Last Key中,继续提取索引的下一个键值,使用同样的提取规则;

2)若存在并且条件是 < ,则将条件加入到Index Last Key中,同时终止提取;

3)若不存在,同样终止Index Last Key的提取。

针对上面的SQL,应用这个提取规则,提取出来的Index Last Key为(b < 8),由于是 < 符号,因此提取b之后结束。


Index Filter

image.png

在完成Index Key的提取之后,我们根据where条件固定了索引的查询范围,但是此范围中的项,并不都是满足查询条件的项。在上面的SQL用例中,(3,1,1),(6,4,4)均属于范围中,但是又均不满足SQL的查询条件

Index Filter的提取规则:同样从索引列的第一列开始,检查其在where条件中是否存在

1)若存在并且where条件仅为 =,则跳过第一列继续检查索引下一列,下一索引列采取与索引第一列同样的提取规则;

2)若where条件为 >=、>、<、<= 其中的几种,则跳过索引第一列,将其余where条件中索引相关列全部加入到Index Filter之中;

3)若索引第一列的where条件包含 =、>=、>、<、<= 之外的条件,则将此条件以及其余where条件中索引相关列全部加入到Index Filter之中;


Table Filter

image.png

Table Filter是最简单,最易懂,也是提取最为方便的。提取规则:所有不属于索引列的查询条件,均归为Table Filter之中。

同样,针对上面的用例SQL,Table Filter就为 e != ‘a’。


小结

SQL语句中的where条件,使用以上的提取规则,最终都会被提取到Index Key (First Key & Last Key),Index Filter与Table Filter之中。

Index First Key,只是用来定位索引的起始范围,因此只在索引第一次Search Path(沿着索引B+树的根节点一直遍历,到索引正确的叶节点位置)时使用,一次判断即可;

Index Last Key,用来定位索引的终止范围,因此对于起始范围之后读到的每一条索引记录,均需要判断是否已经超过了Index Last Key的范围,若超过,则当前查询结束;

Index Filter,用于过滤索引查询范围中不满足查询条件的记录,因此对于索引范围中的每一条记录,均需要与Index Filter进行对比,若不满足Index Filter则直接丢弃,继续读取索引下一条记录;

Table Filter,则是最后一道where条件的防线,用于过滤通过前面索引的层层考验的记录,此时的记录已经满足了Index First Key与Index Last Key构成的范围,并且满足Index Filter的条件,回表读取了完整的记录,判断完整记录是否满足Table Filter中的查询条件,同样的,若不满足,跳过当前记录,继续读取索引的下一条记录,若满足,则返回记录,此记录满足了where的所有条件,可以返回给前端用户。

在MySQL 5.6之前,并不区分Index Filter与Table Filter,统统将Index First Key与Index Last Key范围内的索引记录,回表读取完整记录,然后返回给MySQL Server层进行过滤。

而在MySQL 5.6之后,Index Filter与Table Filter分离,Index Filter下降到InnoDB的索引层面进行过滤,减少了回表与返回MySQL Server层的记录交互开销,提高了SQL的执行效率


回到案例


介绍了大概where的提取规则,我们再回到案例的SQL进行验证。

一目了然可以发现where条件使用到了 vipIphone 字段。根据提取规则。这个是命中了index first key。


查询时间长


如果说一条SQL查询时间过于长的话就说明肯定是出现问题了。当我们执行explain的时候,也同样的命中了索引。执行时间也是很正常化 十几毫秒。那么为什有时那么慢?

一般第一种情况就是表被锁住了。排查思路就是先执行以下SQL看看当前语句处于什么状态。然后我们再针对每种状态,去分析它们产生的原因、如何复现,以及如何处理。

show processlist


等MDL锁

返回结果Waiting for table metadata lock。这个状态就表示现在有一个线程正在表请求或者持有 MDL 写锁,把 select 语句堵住了。 下面我们复现以下image.png

sessionA先对表t加了一个写锁。sessionB要准备执行对表t的读取操作。读取一个表是需要获取MDL读锁的。这样为了防止脏读。所以发现了锁等待现象。

唯一的解决方案就是把这个写锁释放掉。通过kill命令干掉写锁。这样读锁就恢复了。


等flush

另一个情况就是返回结果为Waiting for table flush。根据这个返回结果介绍两个关于flush的指令

flush tables t with read lock;   -- flush表t
flush tables with read lock;  -- flush所有表

这个结果就表示,在查询的时候刚好被MySQL的内部机制刷数据页碰撞了。但是这两个语句执行都是比较快的,应该不会阻塞查询那么长时间啊。唯一的可能就是一个修改的语句被flush阻塞了。刚好这个查询语句又被修改阻塞了。现在我们复现一下过程。image.png

  • sessionA开始一个对表的睡眠操作。参数为秒级。 因为这里是全表扫描。表里有10万数据。所以大概要睡眠10万秒
  • sessionB开启一个flush表B的操作。flush之前要对表t进行关闭。而表t又在sessionA中睡眠。session就被sessionA堵住了。
  • sessionC开始执行查询的操作。sessionA没有执行完毕,sessionB又被sessionA堵住了。sessionC在执行的时候又被sessionB堵住了。


等行锁

上文中,MDL读写锁已经描述完了。这里就介绍已经进来的场景。现在正在处理行锁。

执行如下SQL,我先说一下当初我学习的疑点。lock in share mode 这个是共享锁的意思。具体介绍在扩展中

select * from t where id=1 lock in share mode; 

由于要访问id=1这条数据时要加读锁,如果在这之前这条记录上已经持有一个写锁,那么我们的select语句将被堵塞。我们复现一下过程。

image.png

sessionA开启一个事务还没有提交。sessionB便开始查询并且加读锁。导致 session B 被堵住。


扩展

提到了lock in share mode 我们可以扩展聊一聊for update排他锁,就是行锁

for update:如果事务对数据加上排他锁之后,则其他事务不能对该数据加任何的锁。获取排他锁的事务既能读取数据,也能修改数据。

lock in share mode:如果事务对某行数据加上共享锁之后,可进行读写操作;其他事务可以对该数据加共享锁,但不能加排他锁,且只能读数据,不能修改数据。 某个事物想进行修改数据操作,那他必须等其他事物的共享锁都释放完毕才能进行修改操作

注:普通 select 语句默认不加锁,而CUD操作默认加排他锁。


查询慢

首先举一个例子

mysql> select * from t where c=50000 limit 1;

由于字段 c 上没有索引,这个语句只能走 id 主键顺序扫描,因此需要扫描 5 万行。用时大概11.5毫秒。你可能会说,不是很慢呀,11.5 毫秒就返回了,我们线上一般都配置超过 1 秒才算慢查询。但你要记住:坏查询不一定是慢查询。我们这个例子里面只有 10 万行记录,数据量大起来的话,执行时间就线性涨上去了。

下面看一下只扫描1行数据就用时就非常大的例子。

select * from t where id=1;

是不是有点奇怪呢,我们可以查一下show log的信息

image.png

我们再看一个图对比一下

image.png

第二个图是需要加锁的。时间应该更长才对啊。以上两张图是借助丁奇老师的。文章是总结学习扩展的。

我们把上述结果继续复现一下。

image.png

  • sessionA开启一个事务,并且查询id=1 这条记录,并且查询id=1 并且加读锁查询。
  • sessionB在sessionA开启一个事务之后执行更新操作

我来解读一下。sessionA中的两个查询是不一样的。第一个是一致性读,第二个是当前读。所以第二个是非常快的,直接通过B+树找到对应的节点数据。而第一个的内部流程是1000001 开始,依次执行 undo log,执行了 100 万次以后,才将 1 这个结果返回。


总结


今天主要介绍了几种常见的查询慢的原因。以及一开始介绍了where条件后面的 index key机制问题。


相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
1月前
|
SQL 存储 关系型数据库
原本可以执行得很快的 SQL 语句,执行速度却比预期的慢很多,原因是什么?如何解决?
原本可以执行得很快的 SQL 语句,执行速度却比预期的慢很多,原因是什么?如何解决?
|
11月前
|
SQL
为什么两个执行很快的SQL,union之后特别慢
UNION获取两个数据集并返回唯一重叠。换句话说,它花费时间去除重复。 所以如果只是想取两个SQL的结果,改成使用union all,不会有去重操作 如果还是想去重,可以在union all之后再包一层查询,使用distinct
|
10月前
|
SQL 缓存 NoSQL
执行SQL响应比较慢,你有哪些排查思路?
如果面试问你,执行SQL响应慢,你有哪些排查思路和解决方案?这是一位去某里面试的小伙伴跟我分享的面试真题,那今天我给大家来分享一下我的思路。
80 1
|
9月前
|
中间件
简单记录服务器耗时
简单记录服务器耗时
50 0
|
11月前
|
SQL Arthas druid
MyBtais 批量插入慢排查及分析(后续)
MyBtais 批量插入慢排查及分析(后续)
129 0
|
SQL XML Java
线上sql执行慢,分享3个优化案例
线上sql执行慢,分享3个优化案例
183 0
|
开发工具 开发者
慢与快
我经常跟我的学员这样说:对于学习做游戏这件事情,大家要放平心态,不用着急,我给大家提供一年时间的服务,所以你大可以慢慢来。如果你的时间充足的话,就制定一个为期较短的计划。如果你工作比较忙,时间不够的话,那么就制定一个比较宽松的计划。最重要的是不要停,不要半途而废,不要就此放下。想一下,如果你能够在一年的时间里学会并掌握一门新技能,那也是相当不错的了。
64 0
|
Java
这4种方式,统计代码执行耗时,才足够优雅!
这4种方式,统计代码执行耗时,才足够优雅!
288 0
|
Java 调度
taskfactory默认执行慢的问题
Task.Factory.StartNew不是直接创建线程,创建的是任务,它有一个任务队列,然后通过任务调度器把任务分配到线程池中的空闲线程中,如果任务的数量比线程池中的线程多,线程池的线程数量还没有到达上限,就会创建新线程执行任务。如果线程池的线程已到达上限,没有分配到线程的任务需要等待有线程空闲的时候才执行。
232 0
|
存储 自然语言处理 NoSQL
为啥查询那么慢?
为啥查询那么慢?
244 0
为啥查询那么慢?