面试冲刺:18---一条SQL语句执行很慢的原因

这种问题属于开放性的问题,网上搜了些资料,总结了一下

一、MySQL内部因素

线程原因

  • MySQL线程参阅:https://blog.csdn.net/qq_41453285/article/details/104083744
  • MySQL是多线程的模型,其在后台有多个不同的后台线程,负责不同的任务
  • 后台线程有:
    • Master Thread:主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲(INSERT BUFFER)、UNDO页的回收等
    • IO Thread:在InnoDB中大量使用了AIO(Async IO)来处理写IO请求,这样可以极大提高数据库的性能。而IO Thread的工作主要负责这些IO请求的回调(call back)处理
    • Purge Thread:事务被提交后,其所使用的undo log可能不再需要,因此需要Purge Thread来回收已经使用并分配的undo页
    • Page Cleaner Thread:InnoDB 1.2.x版本引入的,作用是对脏页的刷新。在之前版本,脏页的刷新是在Master Thread中完成的,InnoDB 1.2.x版本之后单独放到这个线程中进行处理了
  • 因此:当MySQL执行的任务比较多的时候,例如IO操作、脏页较多等,那么可能导致MySQL的这些后台线程执行缓慢
  • 解决方法可以适当改变与这些线程相关的MySQL配置参数

MySQL内存池原因

  • MySQL内存池请参阅:https://blog.csdn.net/qq_41453285/article/details/104083744
  • MySQL重做日志请参阅:https://blog.csdn.net/qq_41453285/article/details/104115914
  • MySQL的内存结构包含:
    • 缓冲池:InnoDB存储引擎是基于磁盘存储的,并且数据是按照页的方式进行存储的。我们知道CPU与磁盘之间会有速度的读写差异,MySQL为了弥补磁盘与CPU之间数据读写的差异,设置了缓冲池。当读取数据时,将内容读取到缓冲池中,下次读取时直接从缓冲池中取;当写入数据时(此时就会产生脏页),会更新缓冲池中的内容,并且定期的更新到磁盘上
    • 重做日志缓冲:我们知道当MySQL的事务提交之后会产生重做日志信息,但是这些信息不回立即提交到重做日志中(undo log),而是先放到重做日志缓冲中
    • 其他的内存池:不做了解
  • 因此:当缓冲池、重做日志缓冲不够用时,或者存储空间变小时,也会是导致MySQL执行慢的原因

  • MySQL对数据的访问实惠加锁的,因此当你的SQL语句设计到一张表时,如果这条数据被别的事务加锁了,那你就就必须等到锁的释放,因此这也可能是一种因素
  • 当然,对于MySQL的内部因素还有很多很多:
    • 例如重做日志文件的大小可能也会影响到性能,请参阅:https://blog.csdn.net/qq_41453285/article/details/104115914
    • Checkpoint技术:https://blog.csdn.net/qq_41453285/article/details/104091059
  • MySQL的内部因素会影响SQL的性能,但是可能不是关键性的因素。如果当你的MySQL运行良好的情况下,SQL语句还是执行慢,那么就需要考虑下面的相关设计因素了

二、MySQL设计因素

你的表字段设计索引了吗?

  • MySQL的索引可以:
    • 缩短数据检索的时间
    • 加快表与表之间的连接
    • 为用来排序或分组的字段添加索引可以加快排序或分组的速度
  • 因此,如果你的SQL语句如果没有设计索引,那么SQL语句就会进行全表扫描,但是如果添加了索引,那么就会在指定的区间内进行查找
  • 例如,如果下面的id字段没有添加索引,那么进行查询时,就会进行全表的扫描,然后筛选出符合条件的数据
select * from student where id > 100 and id < 1000;

设计了索引,但是你的索引被用到了吗?

  • 在另一篇文章中我们介绍了“MySQL的索引何时会失效?”,请参阅:https://blog.csdn.net/qq_41453285/article/details/107747602
  • 因此,即使你的表设计了索引,如果由于SQL语句书写的不恰当,那么会当时索引没有被使用上
  • 演示案例:下面我们想要查询id为999的学生信息,但是由于=运算符的左边是999了,而不是id了,因此索引不会被使用到
select * from student where id - 1 = 1000;
  • 正确的做法是下面的样子:
select * from student where id - 1 = 1000;

SQL语句无误,但是你确定MySQL使用了你的索引吗?

  • 上面介绍过,如果SQL语句编写的不好,那么你的索引不会被使用,因此需要编写正确的SQL语句。但是即使你的SQL语句编写的良好,MySQL也不一定会用到你的索引
  • 假设现在我们有下面这样一张表,其中:
    • id是主键
    • age不是主键,假设其有一个索引
create table student (
    id int not null,
    age int not null,
    primary key(id)
) ENGINE = InnoDB;
  • 现在我们执行下面的SQL语句,扫描指定范围内的数据:
select * from student where age > 18 and id < 25;
  • 在理想的情况下:当我们执行上面的SQL语句时,回先根据age这个字段的索引找到主键的值,然后再根据聚集索引找到整行数据的值
    • 关于聚集索引请参阅:https://blog.csdn.net/qq_41453285/article/details/104208974
  • 但是,MySQL可能不会按照你预期的那样去执行,为什么呢?
    • 系统在执行这条SQL语句的时候,会判断是直接让age走索引的行数少,还是让其直接全表扫描的行数少(因为操作的行数也少,那么IO的次数就会越少)
    • 假设上面那条SQL语句会返回n行符合条件的行数,那么:
      • 如果全表扫描的话,直接操作的就是这个表的总行数
      • 如果走索引的话,需要先通过age索引找到主键,然后再通过聚集索引找到数据,因此这种情况下,需要扫描n行数据,并且每一行数据走两次索引
      • 那么:MySQL在执行SQL之前,回先预测一下这条SQL语句大概需要扫描多少行,如果需要扫描的行数很多(例如几乎要扫描全表),那么MySQL就执行扫描全表了就行了,根本不走索引了
  • 那么MySQL是如何预测要操作多少行数据的呢?
    • MySQL是通过索引的区分度来判断的,一个索引上不同的值越多,意味着出现相同数值的索引越少,意味着索引的区分度越高。我们把区分度称之为基数,即区分度越高,基数越大。所以呢,基数越大,意味着符合“age > 18 and id < 25”这个条件的行数越少
    • 所以呢,一个索引的基数越大,意味着走索引查询越有优势
    • 那么如何知道这个索引的基数呢?系统当然是不会遍历全部来获得一个索引的基数的,代价太大了,索引系统是通过遍历部分数据,也就是通过采样的方式,来预测索引的基数的
  • 因此:MySQL是通过采用的方式来预测索引的技术,因为是采样,因此就会有产生误差的时候,那么age这个索引的技术实际上很大,但是采样的时候把这个基数预测的很小。例如你采样的那一部分数据刚好基数很小,然后就误以为索引的基数很小。那么MySQL最终没有使用索引,而是走了全表扫描
  • 当然,我们可以在SQL语句执行的时候强制让其走索引的方式来查询。例如:
select * from student force index(id) where age > 18 and id < 25;
  • 也可以通过下面的命令来查询索引的基数和实际是否符合
show index from student;
  •  如果和实际很不符合的话,我们可以重新来统计索引的基数,命令如下:
analyze table student;

三、总结

  • 这个问题很广泛,答案是不唯一的,面试官想看你对MySQL的理解,总之,随意发挥吧

你可能感兴趣的:(面试冲刺,一条SQL语句执行很慢的原因)