MySQL优化三:查询性能优化之基础部分

前面文章中介绍了如何设计最优的库表结构、如何建立最好的索引,这些对于高性能来说是比不可少的。但这些还不够;还需要合理的设计查询。如果查询写得很糟糕,即使库表结构在合理、索引在合适,也无法实现高性能。查询优化、索引优化、库表结构优化需要齐头并进。

下面我们一起来理解MySQL如何真正执行查询,并明白高效和低效的原因何在,这样能充分发挥MySQL的优势,并避开弱点。

一 为什么查询速度会慢

在尝试编写快速的查询之前,需要清除一点,真正重要是响应时间。如果把查询看作是一个任务,那么它由一系列子任务组成,每个子任务都会消耗一定的时间。如果要优化查询,实际上要优化其子任务,要么消除其中一些子任务,要么减少子任务的执行次数,要么让子任务运行更快。

MySQL在执行查询的时候有哪些子任务,哪些子任务运行的速度很慢?这很难给出一个完整的列表,通常来说,查询的生命周期大致可以按照顺序来看:从客户端,到服务器,然后在服务器上进行解析,生成执行计划,执行,返回给结果给客户端。其中执行可以认为是整个生命周期中最重要的截断,这其中包括了大量未了检索数据到存储引擎的调用以及调用后的数据处理,包括排序、分组等。

在完成这些任务的时候,查询需要在不同的地方话费时间,包括网路,CPU计算,生成统计信息和执行计划、锁等待等操作,尤其是向底层存储引擎检索数据的调用操作,这些调用需要在内存操作,CPU操作和内存不足时导致的I/O操作上消耗时间。根据存储引擎不同,可能还会产生大量的上下文切换以及系统调用。

在每一个消耗大量时间的查询案例中,我们都能看到一些不必要的额外操作、某些操作被额外的重复了很多次、某些操作执行的太慢等。优化查询的目的就是减少和消除这些操作所花费的时间。

对于一个查询的全部生命周期,上面列的并不完整。这里只是想说明链接查询的生命周期、清楚查询的时间消耗情况对于优化查询有很大的意义。有了这些概念,我们在来看如何优化查询。

二 慢查询基础:优化数据访问

查询性能地下最基本的原因是访问的数据太多。某些查询可能不可避免的需要筛选大量数据,但这并不常见。大部分性能地下的查询都可以通过减少访问的数据量的方式进行优化。对于低效的查询,我们发现通过以下两个步骤来分析更有效:

① 确认应用程序是否在检索大量超过需要的数据。这意味着访问了太多的行,有时候也可能是过多的列。

② 确认MySQL服务器是否在分析大量超过需要的数据行。

1 是否向数据库请求了不需要的数据

有些查询会请求超过实际需要的数据,然后这些多余的数据会被应用程序丢弃。这会给MySQL服务器带来额外的负担,并增加网络开销,另外也会消耗应用服务器的CPU和内存资源。

① 查询不需要的记录

一个常见的错误是尝尝会误以为MySQL会只返回需要的数据,实际上MySQL却是先返回全部结果集在进行计算。我们经常会看到一些了解其他数据库系统的人会设计出这类应用程序。这些开发者习惯使用者杨的技术,先使用select语句查询大量的结果,然后获取前面的N行后关闭结果集,例如取出100条只显示10条。他们认为MySQL会执行查询,并只返回他们需要的10条数据,然后停止查询。实际情况是MySQL会查询出全部的结果集,客户端的应用程序会结构全部的结果集数据,然后抛弃其中大部分数据。最简单有效的解决方法就是在这样的查询后面加上limit。

② :多表关联时返回全部列

由于返回了大量的无用数据会倒是浪费大量资源所以在查询时,最好只取出所需要的就好。

③ :总是取出全部列

每次看到类似select * 或者在MyBatis中使用sql片段查出所有列时,都要确认一下是不是真的需要返回全部的列?很可能是不必要的。取出全部列,会让优化器无法完成索引覆盖扫描这类优化。还会为服务器带来额外的I/O、内存和CPU的消耗。

当然,查询返回所有列也不总是坏事,这种浪费数据库资源的方式可以简化开发,因为能提高相同代码片段的复用性,如果清楚这样做的性能影响,那么这种做法也是值得考虑的。如果应用程序使用了某种缓存机制,或有其他考虑,获取超过所需列也可能有其他好处,但不要忘记这样做的代价是什么。获取并缓存所有的列的查询,相比多个独立的只获取部分列的查询可能就更有好处。

④ :重复查询相同的数据

如果你不太小心,很容易出现这样的错误;不断的重复执行相同的查询,然后每次都返回完全相同的数据。例如,在用户评论的地方需要查询用户桐乡的URL,那么用户多次评论的时候,可能就会反复查询这个数据。比较好的方案是使用缓存。

2 MySQL是否在扫描额外的记录

在确定查询只返回需要的数据后,接下来应该看看查询为了返回结果是否扫描了过多的数据。对于MySQL,最贱的衡量查询开销的有三个指标: 响应时间、扫描行数、返回行数。

没有那个指标能够完美的衡量查询的开销,但它们大致反应了MySQL在内部执行查询时需要访问多少数据,并可以大概推算出查询运行的时间。这三个指标都会记录到MySQL的慢日志中,所以检查慢日志记录是找出扫描函数过多的查询的好办法。

响应时间

要记住,响应时间只是一个表面上的值。这样说可能看起来和前面关于响应时间的说法并不矛盾,响应时间仍然是最重要的指标

响应时间是两个部分之和:服务时间和排队等待时间。服务时间是指数据库处理这个查询真正花费了多长时间。排队等待时间是指服务器因为等待某些资源而没有真正执行查询的时间;可能是等I/O操作完成,也可能是等待行锁等。遗憾的是,我们无法把响应时间细分到这部分,除非有什么办法能够逐个测量上面这些小号,不过很难做到。一般常见和重要的等待是I/O和锁等待,但实际情况会更加复杂。

所以在不同类型的应用压力下,响应时间并没有什么一直的规律或公示。注入存储引擎的锁、高并发资源竞争、硬件原因的因素都会影响响应时间。所以,响应时间即可能是一个问题的结果也可能是一个问题的原因,不同案例情况不同。

当你看到一个查询的响应时间的时候,首先需要问问自己,这个响应时间是否是一个合理的值。实际上可以使用快速上限估计法来估算查询的响应时间,这是由TapioLahdenmaki和Mike Leach编写的Relational Database Index Design and the Optimizers一书中提到的技术,概括的说,了解这个查询需要哪些索引以及它的执行计划是什么,然后计算大概需要多少个顺序和随机I/O,再用其乘以在具体硬件条件下一次I/O的消耗时间。最后把这些消耗加起来,就可以获得一个大概参考值来判断是不是一个合理的值。

扫描的行数和返回的行数

分析查询时,查看该查询扫描的行数是非常有帮助的。这在一定程度上能够说明该查询找到需要的数据的效率高不高。

对于找出那些糟糕的查询,这个指标可能还不够完美,因为并不是所有的行的访问代价都是相同的。较短的行的访问速度更快,内存中的行业比磁盘中的行的访问速度快很多。

理想情况下扫描的行数和返回的行数应该是相同的。但实际情况中这种理想情况并不多。例如一个关联查询,服务器必须要扫描多行才能生成结果集中的一行。扫描的行数对返回的行数的比例通常很小,一般在10% ~ 100%之间,不过有时候这个值也可能非常大。

扫描的行数和访问类型

在评估查询开销的时候,需要考虑一下从表中找到某一行数据的成本。MySQL有好几种方式可以朝赵并返回一行结果。有些访问方式可能需要扫描很多行才能返回一行结果,也有些访问方式可能无需扫描就能返回结果。

在EXPLAIN语句中type列反应了访问类型。访问类型有很多种,从全表扫描到索引扫描、范围扫描、唯一索引查询、常熟引用等。这里列的这些速度从慢到快,扫描的行数从大到小。不需要记住这些访问类型,但需要明白扫描表、扫描索引、范围访问和单值访问的概念。

如果查询没有办法找到合适的访问类型,那么解决的最好办法通常就是增加一个合适的索引,素银让MySQL以最搞笑、扫描行数最少的方式找到需要的记录。

一般MySQL能够使用一下三种方式应用where条件,从好到坏依次为:

① 在索引中使用where添加来过滤不匹配的记录。这是在存储引擎层完成的

② 使用索引覆盖扫描来返回记录,直接从索引中过滤不需要的记录并返回命中的结果。这是在MySQL服务层完成的,但无需在回表查询记录。

③ 从数据表中返回数据,然后过滤不返租条件的记录。这在MySQL服务器层完成,MySQL需要先从数据表独处记录然后过滤。

好的索引可以让查询使用合适的访问类型,尽可能的只扫描需要的数据行。但也不能说增加索引就能让扫描的行数等于返回的行数。

不幸的是,MySQL不会告诉我们生成结果实际上需要扫描多少行数据,而只会告诉我们生成结果时一共扫描了多少行数据。扫描的行数中的大部分都很可能是被where条件过滤掉的,对最终的结果集并没有贡献。理解一个查询需要扫描多少行和实际需要使用的行数需要先去理解这个查询背后的逻辑和思想。

如果发现查询需要扫描大量的数据单只返回少数的行,那么可以使用下面的技巧去优化它:

① 使用索引覆盖扫描,把所有需要用的列都放到索引中,这样存储引擎无需回表获取对应行就可以返回结果了。

② 改变库表结构。例如使用单独的汇总表。

③ 重写这个复杂的查询,让MySQL优化器能够以更优化的方式执行这个查询。

三 重构查询的方式

在优化有问题的查询时,目标应该是找到一个更优的方法获得市级需要的结果;而不一定总是需要从MySQL获取一模一样的结果集。有时候,可以将查询转换一种写法让其返回一样的结果,但是性能更好。但也可以通过修改应用代码,用另一种方式完成查询,最终达到一样的目的。

1 一个复杂查询还是多个简单查询

设计查询的时候一个需要考虑的重要问题是,是否需要将一个复杂的查询分成多个简单的查询。在传统实现中,总是强调需要数据库层完成尽可能多的工作,这样做的逻辑在于以前总是人文网络通信、查询解析和 优化是一件代价很高的事情。

但是这样的想法对于MySQL并不使用,MySQL从设计上让链接和断开链接都很轻量级,在返回一个小的查询结果方面很搞笑。现代的网络速度比以前要快很多,无论是带宽还是延迟。在某些版本的MySQL上,即使在一个通用服务器上,也能够运行每秒超过10万的查询,即使是一个千兆玩咖也能够轻松满足每秒超过2000次的查询。所以运行多个小查询现在已经不是大问题了。

MySQL内部每秒能够扫描内存中上百万行数据,相比之下,MySQL响应数据给客户端就慢得多了。在其他条件都相同的时候,使用尽可能少的查询当然是更好的。但是有时候,将一个大查询分解为多个小查询是很有必要的。别害怕这样做,好好衡量一下这样做是不是会减少工作量。

不过,在应用设计的时候,如果一个查询能够胜任时还写成多个独立查询时不明智的。

例如,我们看到有些应用对一个数据表做10此独立的查询来返回10行数据,每个查询返回一条结果,查询10次~~

for(int i = 0;i < userIdList.size();i++){
    User user = userMapper.findUserById(userIdList.get(i));
}

这是一个新手经常使用的方法,会占用更多的数据库连接,并且效率很低。

2 切分查询

有时候对于一个大查询我们需要分而治之,将大查询切分成效查询,每个查询功能完全一样,只完成一小部分,每次只返回一小部分查询结果。

删除旧的数据就是一个很好的例子。定期的清理大量数据时,如果用一个大的语句一次性完成的话,则可能需要一次锁住很多数据、占满整个事物日志、好近系统资源、阻塞很多小的但重要的查询。将一个大的delete语句切分成多个较小的查询可以尽可能小的影响MySQL性能,同时还可以减少MySQL赋值的延迟。例如,我们需要每个月运行一次下面的查询

delete from messages where created < date_sub(now(),interval 3 month);

那么可以用类似下面的办法来完成同样的工作;

rows_affected = 0
do{
    rows_affected = do_query(
        "delete form messages where created < dete_sub(now(),interval 3 month limit 10000)"
    )
}while rows_affected > 0;

一次删除一万航数据一般来说是一个比较高效而且对服务器影响也最小的做法,如果是服务型引擎,很多时候小事物能够更高效。同时,需要注意的是,如果每次删除数据后,都暂停一会在做下一次删除,这样也可以将服务器上原本一次性的压力分散到一个很长的时间段中,就可以大大降低对服务器的影响,还可以大大减少删除时 锁的持有时间。

3 分解关联查询

很多高性能的应用都会对关联查询进行分解。简单的,可以对每一个表进行一次单表查询,然后将结果在应用程序中进行关联。例如下面这个查询:

select * from tag
join tag_post on tag_post.tag_id = tag.id
join post on tag_post.post_id = post.id
where tag.tag = "mysql";

可以分解成下面这些查询来代替

select * from tag where tag = "mysql";(假设结果为1234)
select * from tag_post where tag_id = 1234;(假设结果为123,456,789)
select * from post where post.id in (123,456,789);

为什么这样做?乍一看这么做并没有什么好处,原本一条查询,这里却变成多条查询,返回的结果又是一模一样的。事实上,用分解关联查询的方式重构查询有一下优势:

① 让缓存的效率更高。许多应用程序可以方便的缓存单表查询对应的结果对象,对于MySQL的查询缓存来说,如果关联中的某个表发生了变化,那么久无法使用查询缓存了,而拆分后,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存结果了。

② 将查询分解后,执行单个查询可以减少锁的竞争。

③ 在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。

④ 查询本身效率也可能会有所提升。使用in()替代关联查询,可以让MySQL按照ID顺序进行查询,这可能比随机的关联要更高效

⑤ 可以减少冗余记录的查询。在应用层做关联查询,一位置对于某条记录应用值需要查询一次,而在数据库中做关联查询,则可能需要重复的访问一部分数据。从这点看,这样的重构还可能会减少网络和内存的消耗。

⑥ 更进一步,这样做相当于在应用中实现了哈希关联,而不是使用MySQL的嵌套循环关联。某些场景哈希关联的效率要高很多

在很多场景下,通过重构查询将关联放到应用程序中将会更加高效。

查询执行过程详细部分太多了,所以提到下一章单独一篇

猜你喜欢

转载自blog.csdn.net/yongqi_wang/article/details/86495017