深入解读Mysql查询性能的优化
作者:阿柠xn
查询性能优化
在之前的文章中,我们介绍了如何设计最优的库表结构,如何建立最好的索引,这些对于高性能来说必不可少。但是这些还不够—你还需要设计合理的查询。如果查询写的很糟糕,即使库表结构再合理,索引再合适,也无法实现高性能。
为什么查询速度会慢
我们在想写一个快速的查询之前需要明白一个问题,真正重要的是响应时间。如果把查询看作是一个任务,那么它是由一系列子任务组成,每个子任务都会消耗一定的时间。如果想要优化查询,就需要优化其子任务,要么你就消除其中的一些子任务,要么就减少子任务的执行次数,要么就让子任务运行的更快。
通常来说呀,查询的生命周期大致可以按照顺序来看:从客户端,到服务器,然后在服务器上解析,生成执行计划,执行,并返回结果给客户端。
其中 执行 可以认为是整个生命周期最重要的阶段,其中包括了大量为了检索数据到存储引擎的调用以及调用后的数据处理,包括排序,分组等。
在完成这些任务的时候,查询需要在不同的地方花费时间,包括网络,CPU计算,生成统计信息和执行计划,锁等待等操作。
尤其是向底层存储引擎检索数据的调用操作,这些调用需要在内存操作,CPU操作和内存不足时导致IO操作上消耗时间。根据存储引擎不同,可能还会产生大量的上下文切换以及系统调用。
下面我们就来看看如何优化 查询。
慢查询基础:优化数据访问
查询性能低下的最基本的原因是访问的数据太多。某些查询可能不可避免的需要筛选大量数据,但这并不常见。
对于一些低效的查询,我们通常可以使用下面两个步骤来分析:
- 确认用用程序是否在检索大量超过你需要的数据。这通常意味着访问太多行,但有时候也可能是访问了太多列。
- 确认MySQL服务器是否在分析大量超过需要的数据行 是否向数据库请求了不需要的数据
有些查询会请求超过实际需要 的数据,然后这些多余的数据会被应用程序丢弃。这就会带来一些额外的很多负担,并增加网络开销。也会消耗应用服务器的cpu和内存资源。
这里有一些经典案例:
- 查询不需要的记录
很多人会以为MySQL只会返回需要的数据,实际上MySQL却是先返回全部结果集再进行计算。一些开发者会先使用select语句查询大量的结果,然后获取前面的N行后关闭结果集。
你以为mysql只返回了你需要的前几条信息,实际上MySQL是返回了全部结果集,然后丢弃了大部分的数据。最简单有效的解决方法就是加limit。
- 多表关联时返回全部列
比如说你想查询电影FLY 中出现的演员,你可千万千万不要像下面这样写:
select * FROM actor inner join film_actor using(actor_id) inner join film using(film_id) where film.title = 'FLY';
你这样写就把三个表的全部数据列都返回了,正确的方式是下面这么写,只取需要的列:
select actor.* FROM actor inner join film_actor using(actor_id) inner join film using(film_id) where film.title = 'FLY';
- 总是取出全部列
每次看到**SELECT ***的时候都需要仔细想想,是不是真的需要返回全部的列?很可能不是必需的。取出全部列,会让优化器无法完成索引覆盖扫描这类优化, 还会为服务器带来额外的I/O、内存和CPU的消耗。因此,一些DBA是严格禁止 SELECT *的写法的,这样做有时候还能避免某些列被修改带来的问题。何乐而不为呢?
当然,你话不能说死,查询返回查过需要的数据也不总是坏事。因为这种有点浪费数据库资源的方式是可以简化开发的,因为它能提高相同代码片段的复用性。
- 重复查询相同的数据
你比如说,一个用户多次评论的时候,你每次都要查询它的id,这就很不好,我们呢可以采取的一种方案是,初次查询的时候就将这个数据缓存起来,需要的时候从缓存中取出来,这样性能显然会更好。
MySQL是否在扫描额外的记录
我们上面讲的是确定查询只返回需要的数据,那么我们还要关注什么呢?
我们要去研究为了返回这个结果,有没有扫描过多的数据这一现象。
对于mysql,最简单的三个衡量查询开销的指标就下面这三哥们:
- 响应时间
- 扫描的行数
- 返回的行数
当然,没有哪个指标能够完美的说衡量出查询的开销,你只能通过这三指标去权衡罢了。
这三个指标都会记录到MySQL的慢日志中去,所以检查慢日志是找出扫描行数过多的查询的好办法。
1.响应时间
响应时间是两个部分之和:服务时间和排队时间。服务时间是指数据库处理这个査询真正花了多长时间。
排队时间是指服务器因为等待某些资源而没有真正执行査询,在那等资源所消耗的时间——可能是等I/O操作完成,也可能是等待行锁之类的。
但是上面所说的这些情况在实际情况下是更加复杂的情况,所以响应时间是没有什么一致的规律或者公式的。我们只能算个大致的时间然后去判断是不是一个合理的值。
2.扫描的行数和返回的行数
分析査询时,査看该査询扫描的行数是非常有帮助的。这在一定程度上能够说明该查询找到需要的数据的效率高不高。
当然,这个指标可能不够完美,因为并不是所有的行的访问代价都是相同的。较短的行的访问速度更快,内存中的行也比磁盘中的行的访问速度要快得多。
3.扫描的行数和访问类型
在评估查询的开销的时候,我们还需要考虑一下从表中找到一行数据的成本。因为MySQL有好几种访问方式可以査找并返回你想要的一行结果。
有些访问方式可能需要扫描很多行才能返回一行 结果,但是也有些访问方式可能无须扫描就能返回结果。
在EXPLAIN语句中的type列可以体现出你的访问类型。访问类型有很多种,如全表扫描,索引扫描,范围扫描,唯一索引査询,常数引用等。
这些访问的速度是从慢到快的,扫描的行数也是从小到大。当然我们是不需要记住这些访问类型。
如果你的查询没有办法找到合适的访问类型,俺么最好的解决方法就是增加一个合适的索引,这我们之前文章已经介绍了。
为什么索引岁查询性能的优化这么重要呢?索引让MySQL以最高效,扫描行数最少的方式找到你想要的结果。
一般MySQL能够使用如下的三种方式应用where条件,也是从好到坏的排序:
- 在索引中使用WHERE条件来过滤不匹配的记录。这是在存储引擎层完成的。
- 使用索引覆盖扫描(在Extra列中出现了 Using index)来返回记录,直接从索引中 过滤不需要的记录并返回命中的结果。这是在MySQL服务器层完成的,但无须再回表查询记录。
- 从数据表中返回数据,然后过滤不满足条件的记录(在Extra列中出现Using Where)这在MySQL服务器层完成,MySQL需要先从数据表读出记录然后过滤。这就很慢了,很糟糕了。
如果我们发现查询需要扫描大量的数据但是只是返回少数的行,那么我们通常可以尝试这些策略技巧去优化:
- 使用索引覆盖扫描,把所有需要用到的列都放到索引中。
- 改变库表结构。例如使用单独的汇总表
- 重写这个复杂的査询,让MySQL优化器能够以更优化的方式执行这个査询。
重构查询的方式
就是我们之前提到的一种优化方式,我们的SQL查询太慢,有时候是因为我们写的这个SQL太糟糕了。我们需要换一种方式去写SQL,但还是要返回一样的结果。
一个复杂的查询还是多个简单的查询
就如标题所言,我们在设计查询的时候,需要考虑的一个重要问题是:是否需要将一个复杂的查询变为多个简单的查询。
切分查询
有时候啊,我们需要把一个大查询“分而治之”,将大查询变为小查询,每个查询功能完全一样,只完成一小部分,每次只返回一小部分的结果。
你比如说,想要删除旧的数据,,如果说你用一个大的语句一次性完成的话 ,则可能需要一次性锁住很多数据,占满整个事务日志,耗尽系统资源,阻塞很多小的,但是重要的查询。所以将一个大的delete语句切分为多个较小的查询可以尽可能小地影响MySQL性能,同时还可以减少MySQL复制的延迟。
你比如说下面这个例子:
DELETE FROM messages WHERE created < DATE_SUB(NOW(),INTERVAL 3 MONTH);
我们就可以使用下面同样的方法来解决:
rows_affected = 0 do { rows_affected = do_query( "DELETE FROM messages WHERE created < DATE_SUB(NOW(),INTERVAL 3 MONTH) LIMIT 10000") } while rows_affected > 0
我们通过上面的语句,就实现了一次删除啊10000行数据这样一个限制,一次删除10000行数据是一个比较高效而且对服务器影响也是最小的做法。如果,每次删除数据后,都暂停一会再做下一次删除,这样也可以将服务器上原本一次性的压力分散到一个很长的时间段,也就大大的降低了对服务器的影响。
分解关联查询
很多高性能的应用都会对关联查询进行分解。简而言之就是,可以对每一个表进行一次单表查询,然后将结果在应用程序中进行关联。
你比如说说下面这个查询。
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'; SELECT * FROM tag_post WHERE tag_id=1234; SELECT * FROM post WHERE post.id in (123,456,567,9098,8904);
返回的结果一模一样,那么这样做有什么好处呢?
- 让缓存的效率更高。许多应用程序可以方便的缓存单表查询对应的结果对象。你比如说呀,如果第一行的tag已经被缓存了,那么应用就可以跳过第一个查询了,
- 我们分解之后,执行单个查询可以减少锁的竞争
- 在应用层做关联,可以更加容易对数据库进行拆分,更加容易做到高性能和可扩展
- 查询本身的效率也会提升,我们使用in代替关联查询,这比随机的关联是要更加高效的。
- 可以减少冗余记录的查询。在应用层做关联査询,意味着对于某条记录应用只需要査询一次,而在数据库中做关联査询,则可能需要重复地访问一部分数据。从这点看,这样的重构还可能会减少网络和内存的消耗。
到此这篇关于深入解读Mysql查询性能的优化的文章就介绍到这了,更多相关Mysql查询性能优化内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!