【MySQL数据库】一条SQL语句为什么执行这么慢?

面试高频题目,一条SQL语句为什么执行这么慢? 这其中涉及的知识也是各种各样,今天就让我们来完全剖析这个问题。

一、执行偶尔变慢

有的时候,明明执行的是同一条语句,之前执行还挺快的,但忽然某一次就像“卡住了一样”需要很久才能返回结果,甚至是长时间不返回。出现这种情况呢,就需要考虑以下两种情况:

1. 刷脏页,写磁盘

首先说明脏页的概念:当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。
通常情况下,我们对于数据的操作都是将数据从磁盘中加载到内存中后,在内存中进行修改的。而修改后也并不是直接就将内存中的脏页刷回到磁盘中。什么情况下会进行刷脏页的操作呢?

  1. redo log写满了,要flush脏页,前面我们讲过,MySQL操作时会写redo log,而且redo log 的大小时有限的,那么就会出现写满了的情况,此时,就不得不将redo log中的数据刷回到磁盘中。这种情况是InnoDB要尽量避免的。因为出现这种情况的时候,整个系统就不能再接受更新了,所有的更新都必须堵住。
  2. 内存不够用了,要先将脏页写到磁盘,这种情况其实是常态。InnoDB用缓冲池(buffer pool)管理内存,缓冲池中的内存页有三种状态:
    第一种是,还没有使用的;
    第二种是,使用了并且是干净页;
    第三种是,使用了并且是脏页。
    而当要读入的数据页没有在内存的时候,就必须到缓冲池中申请一个数据页。如果内存不够,就要把最久不使用的数据页从内存中淘汰掉,如果是刚好是脏页呢,就必须将脏页先刷到磁盘,变成干净页后才能复用。
  3. 系统处于空闲时刻,在系统处于空闲时刻时,就会将刷脏页操作安排起来。
  4. MySQL正常关闭时刻,在关闭时刻,系统会将内存中的脏页都刷回磁盘,保证下次使用可以直接使用数据。

对于第三四种情况,明显不是在执行SQL语句导致慢的原因,重点影响效率的就是第一种和第二种情况。
📌所以如果在执行SQL语句时,发生了刷脏页操作,那就一定会影响整体效率!

如果对redo log 还不是很理解的,推荐另一篇文章——>认识MySQL中重要的 bin log、redo log 日志系统

2. 还在等锁

这个相信是比较好理解的原因吧:当我们在执行一条SQL语句,需要获得行锁又或者是表锁时,刚好这个表或者行上的锁已经被别的线程持有,此时,我们要执行的语句只能等待别的事务提交后释放锁才能继续执行。
在排查问题时,可以使用show processlist命令,来查看当前的状态,然后进行响应的调整。

📌所以如过SQL语句操作需要等待获取锁,就会导致执行变慢 !

3. 回滚日志过多

还有可能出现的问题就是有很多的undo log回滚日志,由于MySQL默认的隔离级别是可重复读,也就是一致性读,所以就有可能出现事务A在事务B之前开启事务并进行查询字段a的操作,而事务B进行了大量修改字段a的操作。
所以,当事务A查到字段a的最新值时,由于隔离级别是可重复读,它查询出的字段a的值不应该是事务B修改后的。而事务A读到的却a是最新的值,但它并不会返回这个结果,而是会从a当前的值多次使用回滚日志,最终查询到未被事务B修改时候的值

📌由于重复进行了大量对查询值的回滚,也会导致一条SQL语句变慢!

推荐自己的关于隔离级别的文章——>谈谈事务的隔离级别有什么?

二、执行总是很慢

另一种情况呢,就是无论怎么执行这条语句,总是很慢。

1. 没有索引

首先想到的,就是在SQL语句中操作的字段上没有索引,执行语句只能进行全表扫描。
如果整个表的数据很多,显然进行全表扫描会导致整体的效率十分低。为了解决这个问题,就要考虑在字段上加上索引,当然加索引也要选择最适合的索引。比如是否可以根据最左前缀的原则使用联合索引,又或者是根据是否需要回表添加主键索引或普通索引,还有根据业务逻辑是否需要建立唯一索引。总之要选择最适合的索引来增加效率。

这里也可以参考我之前的文章:
关于MySQL索引的基础知识
普通索引和唯一索引的区别?

📌所以,如果字段上没有索引,执行起来当然慢 !

2. 对字段进行函数操作

当你发现,查询的字段明明是有索引的呀,怎么执行起来还是这么慢呀?
这时候检查一下你的SQL语句是否对字段进行了函数操作,比如:

select * from student where id + 1 = 10000

看起来很正常,而且id这个字段上也是有索引的,按理说这个搜索是该走索引的。但是实际上,MySQL对这个语句还是进行了全表扫描,原因是它只认识id的索引,并不认识id+1,他认为对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。所以这个语句由于没有用到索引,而是进行全表扫描导致执行变慢。看起来确实有点不智能,我们只能将SQL语句修改为:

select * from student where id = 10000 - 1

这样写,虽然语义没变,但这次就可以走id 索引来查询了。

📌所以,如果对索引字段做函数操作,执行时就不会继续选择这个索引,这样也会导致效率低下 !

3. 选错索引

看过上面的原因,你仔细核查了一下,即有索引,而且也没有对索引字段做函数操作呀,怎么查询还是慢吞吞的。这时候就考虑一下,是不是MySQL给我们选错索引了。 你可能有点惊讶,MySQL竟然还会选错索引嘛?! 是的,那就来一起看看这种情况吧:

首先,对于一个查询语句而言,选择是否使用索引和使用哪一个索引时优化器的事情,优化器判断时会根据扫描行数是否排序等问题来进行选择一个他认为最合理的索引,或者是不使用索引。你可能会说,怎么会不使用索引呢?使用索引是快的呀。

还记得之前说的回表操作嘛,如果是一个普通索引,他需要先查询到叶子节点中存放的主键索引的值,再到主键索引树在找出相应的数据。那么就有可能因为回表的问题导致优化器觉得使用索引还不如全表扫描呢。
优化器首先会预判出,使用这个索引字段会需要扫描多少行,扫描的行数越少越好。而它预估的方式就是索引的区分度,而一个索引上不同的值的个数,我们称之为基数(cardinality)。也就是说,这个基数越大,索引的区分度越好。

而优化器得到的基数也并不是很准确的,因为它并不是将整张表都拿出来一个个的统计,毕竟这样代价很大,所以它实际上时通过采样来实现的。,InnoDB默认会选择N个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。这种方式也使得基数实际上是不是很准确的。
我们可以通过show index方法,看到一个索引的基数。也可以通过explain命令查看语句的执行情况。

所以,如果执行一个语句时,发现这个索引的区分度很小时,就会让优化器觉得符合条件的数据会很多,也会进行很多次的回表,所以他就会放弃走索引,而使用全表扫描。

如果我们发现这个问题,怎么解决呢?

  1. 可以使用force index命令来强行选择一个索引。
  2. 可以修改语句,引导MySQL使用我们期望的索引。
  3. 可以新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引。

📌总之,如果MySQL自己选错了索引,也会导致整个语句执行变慢!

唠唠叨叨:
好啦,这就是我总结的使得一条SQL语句为执行慢的原因了。我总结了六条,当然还会有各种各样的场景使得整体效率变慢,本人知识广度有限,希望还知道其它情况的大佬多多指点。本文参考极客时间课程《MySQL实战45讲》,老师讲的非常好,如果感兴趣建议大家去看。文章如果有什么问题欢迎留言指正,另外如果对你有帮助也欢迎小伙伴们点赞关注一起进步!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章