从一次查询的全流程看SQL优化

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

概述

上一篇对外面平台的订单业务的下单流程做了个简单的介绍,通过产生的订单数据分析了一下如果系统遇到性能瓶颈所进行的系统架构演进。

订单业务中不管是用户下单以及用户查询订单列表的操作的过程中涉及到的业务和流程都不是很复杂,难点在于当数据量变的很大的时候不能影响用户的使用体验,在用户下单、查询订单列表、修改订单数据的时候尽量保证系统的响应时效。

一下几篇主要针对之前提出的优化方案进行展开分析,下面先从SQL优化方案开始进行分析

一次查询的全过程

首先分析一下一次查询订单请求的全部过程。用户发起请求,后台应用服务接收到查询请求,WEB容器Tomcat开启一个线程处理本次查询请求,调用编写的应用代码到数据库中执行查询SQL语句,返回查询结果。

  • 业务流程图:

image.png

  • MySQL查询SQL的执行流程:

image-20220724145021044.png

image-20220724150548470.png

InnoDB存储引擎的数据存储的Buffer Pool最小单位是chunk,每个chunk的默认大小是128M,默认只有一个chunk。

查询buffer_pool的大小:SELECT @@innodb_buffer_pool_size/1024/1024;

查询buffer_pool的实例数:SELECT @@innodb_buffer_pool_instances;

调用接口执行SQL后,所有操作都是基于内存进行操作的,但是MySQL的数据是持久化到磁盘上的,所以MySQL的数据存在的地方有内存和磁盘,内存中是部分最近操作的或正在操作的数据,磁盘中几乎是全部的最新数据,之所以说是几乎,是因为可能内存中存在新写的数据,但是还未及时刷入到磁盘中。因此如果查询的数据在内存中不存在,还有一个从磁盘加载数据的过程。

image-20220724154653283.png

查询流程小结

一次查询请求首先是通过网络将请求发送到后端服务器,交给对应的WEB应用,WEB容器开启一个线程处理请求,调用业务代码,业务代码执行SQL查询,应用与MySQL服务建立连接,成功建立连接后,MySQL会分配一个线程处理应用的操作,应用将需要执行的SQL发送给MySQL服务,MySQL服务通过对SQL的解析、优化得到执行计划,然后调用功能执行SQL的接口,到MySQL的存储引擎中查询数据,存储引擎到自己的buffer pool中查询数据,如果buffer pool中没有数据,将会到磁盘中加载对应表空间的数据到buffer pool中,然后将数据返回,最终将数据返回给用户。

磁盘IO为什么这么慢

通过对查询流程的分析,发现其实大部分流程都是基于内存的操作,只有最后一步有可能会发生磁盘IO,磁盘IO会影响整个的执行效率。那么磁盘IO为什么这么慢呢,进一步分析一下磁盘IO慢的原因。

硬盘内部主要部件为磁盘盘片、传动手臂、读写磁头和主轴马达。实际数据都是写在盘片上,读写主要是通过传动手臂上的读写磁头来完成。实际运行时,主轴让磁盘盘片转动,然后传动手臂可伸展让读取头在盘片上进行读写操作。磁盘物理结构如下图所示:

image.png

影响硬盘性能的因素:

  • 寻道时间 Tseek是指将读写磁头移动至正确的磁道上所需要的时间。寻道时间越短,I/O操作越快,目前磁盘的平均寻道时间一般在3-15ms。
  • 旋转延迟 Trotation是指盘片旋转将请求数据所在的扇区移动到读写磁盘下方所需要的时间。旋转延迟取决于磁盘转速,通常用磁盘旋转一周所需时间的1/2表示。比如:7200rpm的磁盘平均旋转延迟大约为60*1000/7200/2 = 4.17ms,而转速为15000rpm的磁盘其平均旋转延迟为2ms。
  • 数据传输时间 Ttransfer是指完成传输所请求的数据所需要的时间,它取决于数据传输率,其值等于数据大小除以数据传输率。目前IDE/ATA能达到133MB/s,SATA II可达到300MB/s的接口数据传输率,数据传输时间通常远小于前两部分消耗时间。简单计算时可忽略。

衡量性能的指标: 机械硬盘的连续读写性能很好,但随机读写性能很差,这主要是因为磁头移动到正确的磁道上需要时间,随机读写时,磁头需要不停的移动,时间都浪费在了磁头寻址上,所以性能不高。衡量磁盘的重要主要指标是IOPS和吞吐量。

MySQL索引的形成

MySQL在磁盘上的存储单位是一个一个的数据页,数据页中包含38个字节的文件头、56字节的数据页头、26字节的最大最小记录、数据区、空闲区、数据页目录、8自己的文件也尾部,如图:

image-20220724211238797.png

数据区中保存的是一行一行的数据,展开后的结果如图:

image-20220724211343875.png

为了提高数据的查找效率,数据页中有两个重要的结构最大记录最小记录数据页目录,MySQL查找数据的时候,先根据要查找记录的主键ID根据数据页的最大记录最小记录找到数据页,在根据数据页的数据页目录查找具体的一行数据的地址。数据页之间是双向链表、数据页中的数据行是单项链表的记录,这也提高了数据的查询效率。

image-20220724213619254.png

当数据页变的很多的时候,在查找数据页的过程中也会降低效率,针对这种情况,MySQL增加了一个特殊的数据页,这种数据页中储存的不是具体的业务数据,而是其他数据页中数据行的主键ID,查询数据的时候先通过特殊的数据页查询到具体的数据页,然后在到具体的数据页中查询具体的数据。这种特殊的数据页就是索引页。

image-20220724214715468.png

最终就会形成一个倒着的树一样的结构,这也就是MySQL中的索引树:

image-20220724215516489.png

SQL优化流程

  • 业务分析优化流程

根据关联查询字段、where条件过了字段、聚合函数字段、排序字段等进行分析建立合适的索引。

image-20220724223645912.png

其中where条件、聚合函数、排序字段比较简单容易分析,需要注意的是join字段的索引建立分析,涉及到一些MySQL特性相关的算法。

mysql中使用join语句关联2张表的话,比如执行这条sql:

select t1.order_no, t2.product_id from order_info t1 left join order_item_detail t2 on t1.order_no = t2.order_no

这个时候,join关联查询的过程是什么样子的呢?其实,这个就取决于当前join语句用到的算法了,join语句一共有3种算法,最基础的是Simple nested loop算法

join语句优化算法

  • Simple nested loop算法:相当于双重for循环

  • Block nested loop算法

MySQL中提供了一个join buffer的内存空间,但是join buffer的默认大小是256kb,内存有限,可以通过join buffer size调节join buffer的大小。

image-20220725075044418.png

  • Index nested loop算法

原来的匹配次数为:驱动表行数 * 被驱动表行数,而现在变成了:驱动表行数 * 被驱动表索引的高度,这样就极大的减少了被驱动表的匹配次数,极大地提升了join的性能。

image.png

如果join关联查询能使用到索引的话,MySQL就会使用Index nested loop算法,如果无法使用Index nested loop算法,MySQL默认会使用Block nested loop算法。

总结

上面从一次查询的流程分析,带入了MySQL索引的形成,到如何根据索引结构的特性实现SQL优化,以及优化的流程。解决问题的前提是发现问题,只有发现问题后才能找到对应的办法解决问题。业务的SQL往往会经过两个阶段的优化,业务初期根据实际业务中用到的SQL根据优化流程创建对应的索引,第二个阶段是系统运行期,通过系统监控发现慢SQL然后进行优化。

猜你喜欢

转载自juejin.im/post/7125241277683073055