那什么样的Query 是更需要优化呢?
对于这个问题我们需要从对整个系统的影响来考虑。什么Query 的优化能给系统整体带来更大的收益,就更需要优化。一般来说,高并发低消耗(相对)的Query 对整个系统的影响远比低并发高消耗的Query 大。我们可以通过以下一个非常简单的案例分析来充分说明问题。
假设有一个Query 每小时执行10000 次,每次需要20 个IO。另外一个Query 每小时执行10 次,每次需要20000 个IO。我们先通过IO 消耗方面来分析。可以看出,两个Query 每小时所消耗的IO 总数目是一样的,都是200000 IO/小时。假设我们优化第一个Query,从20 个IO 降低到18 个IO,也就是仅仅降低了2 个IO,则我们节省了2 * 10000 = 20000 (IO/小时)。而如果希望通过优化第二个Query 达到相同的效果,我们必须要让每个Query 减少20000 / 10 = 2000 IO。我想大家都会相信让第一个Query 节省2 个IO远比第二个Query 节省2000 个IO 来的容易。其次,如果通过CPU 方面消耗的比较,原理和上面的完全一样。只要让第一个Query 稍微节省一小块资源,就可以让整个系统节省出一大块资源,尤其是在排序,分组这些对CPU 消耗比较多的操作中尤其突出。
最后,我们从对整个系统的影响来分析。一个频繁执行的高并发Query 的危险性比一个低并发的Query 要大很多。当一个低并发的Query 走错执行计划,所带来的影响主要只是该Query 的请求者的体验会变差,对整体系统的影响并不会特别的突出,之少还属于可控范围。但是,如果我们一个高并发的Query 走错了执行计划,那所带来的后可很可能就是灾难性的,很多时候可能连自救的机会都不给你就会让整个系统Crash 掉。曾经我就遇到这样一个案例,系统中一个并发度较高的Query 语句走错执行计划,系统顷刻间Crash,甚至我都还没有反应过来是怎么回事。
当重新启动数据库提供服务后,系统负载立刻直线飙升,甚至都来不及登录数据库查看当时有哪些Active 的线程在执行哪些Query。如果是遇到一个并发并不太高的Query 走错执行计划,至少我们还可以控制整个系统不至于系统被直接压跨,甚至连问题根源都难以抓到。
定位优化对象的性能瓶颈当我们拿到一条需要优化的Query 之后,
第一件事情是什么?是反问自己,这条Query 有什么问题?我为什么要优化他?
只有明白了这些问题,我们才知道我们需要做什么,才能够找到问题的关键。而不能就只是觉得某个Query 好像有点慢,需要优化一下,然后就开始一个一个优化方法去轮番尝试。这样很可能整个优化过程会消耗大量的人力和时间成本,甚至可能到最后还是得不到一个好的优化结果。这就像看病一样,医生必须要清楚的知道我们病的根源才能对症下药。如果只是知道我们什么地方不舒服,然后就开始通过各种药物尝试治疗,那这样所带来的后果可能就非常严重了。
所以,在拿到一条需要优化的Query 之后,我们首先要判断出这个Query 的瓶颈到底是IO 还是CPU。
到底是因为在数据访问消耗了太多的时间,还是在数据的运算(如分组排序等)方面花费了太多资源?
一般来说,在MySQL 5.0 系列版本中,我们可以通过系统自带的PROFILING 功能很清楚的找出一个Query 的瓶颈所在。当然,如果读者朋友为了使用MySQL 的某些在5.1 版本中才有的新特性(如Partition,EVENT 等)亦或者是比较喜欢尝试新事务而早早使用的MySQL 5.1 的预发布版本,可能就没办法使用这个功能了,因为该功能在MySQL5.1 系列刚开始的版本中并不支持,不过让人非常兴奋的是该功能在最新出来的MySQL 5.1 正式版(5.1.30)又已经提供了。而如果读者朋友正在使用的MySQL 是明确的优化目标
当我们定为到了一条Query 的性能瓶颈之后,就需要通过分析该Query 所完成的功能和Query 对系统的整体影响制订出一个明确的优化目标。没有一个明确的目标,优化过程将是一个漫无目的而且低效的过程,也很难达收到一个理想的效果。尤其是对于一些实现应用中较为重要功能点的Query 更是如此。
如何设定优化目标?
这可能是很多人都非常头疼的问题,对于我自己也一样。要设定一个合理的优化目标,不能过于理想也不能放任自由,确实是一件非常头疼的事情。
一般来说,我们首先需要清楚的了解数据库目前的整体状态,同时也要清楚的知道数据库中与该Query 相关的数据库对象的各种信息,而且还要了解该Query 在整个应用系统中所实现的功能。
了解了数据库整体状态,我们就能知道数据库所能承受的最大压力,也就清楚了我们能够接受的最悲观情况。把握了该Query 相关数据库对象的信
息,我们就应该知道实现该Query 的消耗最理想情况下需要消耗多少资源,最糟糕又需要消耗多少资源。
最后,通过该Query 所实现的功能点在整个应用系统中的重要地位,我们可以大概的分析出该Query 可以占用的系统资源比例,而且我们也能够知道该Query 的效率给客户带来的体验影响到底有多大。
当我们清楚了这些信息之后,我们基本可以得出该Query 应该满足的一个性能范围是怎样的,这也就是我们的优化目标范围,然后就是通过寻找相应的优化手段来解决问题了。如果该Query 实现的应用系统功能比较重要,我们就必须让目标更偏向于理想值一些,即使在其他某些方面作出一些让步与牺牲,比如调整schema 设计,调整索引组成等,可能都是需要的。而如果该Query 所实现的是一些并不是太关键的功能,那我们可以让目标更偏向悲观值一些,而尽量保证其他更重要的Query 的性能。
这种时候,即使需要调整商业需求,减少功能实现,也不得不应该作出让步。
现在,优化目标也已经明确了,自然是奥开始动手的时候了。我们的优化到底该从何处入手呢?
答案只有一个,从Explain 开始入手。为什么?因为只有Explain 才能告诉你,这个Query 在数据库中是以一个什么样的执行计划来实现的。
但是,有一点我们必须清楚,Explain 只是用来获取一个Query 在当前状态的数据库中的执行计划,在优化动手之前,我们比需要根据优化目标在自己头脑中有一个清晰的目标执行计划。只有这样,优化的目标才有意义。
一个优秀的SQL 调优人员(或者成为SQL performance Tuner),在优化任何一个SQL 语句之前,都应该在自己头脑中已经先有一个预定的执行计划,然后通过不断的调整尝试,再借助Explain 来验证调整的结果是否满足自己预定的执行计划。对于不符合预期的执行计划需要不断分析Query 的写法和数据库对象的信息,继续调整尝试,直至得到预期的结果。
当然,人无完人,并不一定每次自己预设的执行计划都肯定是最优的,在不断调整测试的过程中,如果发现MySQL Optimizer 所选择的执行计划的实际执行效果确实比自己预设的要好,我们当然还是应该选择使用MySQL optimizer 所生成的执行计划。
上面的这个优化思路,只是给大家指了一个优化的基本方向,实际操作还需要读者朋友不断的结合具体应用场景不断的测试实践来体会。当然也并不一定所有的情况都非要严格遵循这样一个思路,规则是死的,人是活的,只有更合理的方法,没有最合理的规则。
在了解了上面这些优化的基本思路之后,我们再来看看优化的几个基本原则。
永远用小结果集驱动大的结果集很多人喜欢在优化SQL 的时候说用小表驱动大表,个人认为这样的说法不太严谨。
为什么?因为大表经过WHERE 条件过滤之后所返回的结果集并不一定就比小表所返回的结果集大,可能反而更小。在这种情况下如果仍然采用小表驱动大表,就会得到相反的性能效果。其实这样的结果也非常容易理解,在MySQL 中的Join,只有Nested Loop 一种Join 方式,也就是MySQL 的Join 都是通过嵌套循环来实现的。驱动结果集越大,所需要循环的此时就越多,那么被驱动表的访问次数自然也就越多,而每次访问被驱动表,即使需要的逻辑IO 很少,循环次数多了,总量自然也不可能很小,而且每次循环都不能避免的需要消耗CPU ,所以CPU 运算量也会跟着增加。
所以,如果我们仅仅以表的大小来作为驱动表的判断依据,假若小表过滤后所剩下的结果集比大表多很多,结果就是需要的嵌套循环中带来更多的循环次数,反之,所需要的循环次数就会更少,总体IO 量和CPU 运算量也会少。而且,就算是非Nested Loop 的Join 算法,如Oracle 中的Hash Join,同样是小结果集驱动大的结果集是最优的选择。所以,在优化Join Query 的时候,最基本的原则就是“小结果集驱动大结果集”,通过这个原则来减少嵌套循环中的循环次数,达到减少IO 总量以及CPU 运算的次数。
尽可能在索引中完成排序只取出自己需要的Columns任何时候在Query 中都只取出自己需要的Columns,尤其是在需要排序的Query 中。
为什么?对于任何Query,返回的数据都是需要通过网络数据包传回给客户端,如果取出的Column 越多,需要传输的数据量自然会越大,不论是从网络带宽方面考虑还是从网络传输的缓冲区来看,都是一个浪费。
如果是需要排序的Query 来说,影响就更大了。在MySQL 中存在两种排序算法,
一种是在MySQL4.1 之前的老算法,实现方式是先将需要排序的字段和可以直接定位到相关行数据的指针信息取出,然后在我们所设定的排序区(通过参数sort_buffer_size 设定)中进行排序,完成排序之后再次通过行指针信息取出所需要的Columns,也就是说这种算法需要访问两次数据。
第二种排序算法是从MySQL4.1 版本开始使用的改进算法,一次性将所需要的Columns 全部取出,在排序区中进行排序后直接将数据返回给请求客户端。改行算法只需要访问一次数据,减少了大量的随机IO,极大的提高了带有排序的Query 语句的效率。但是,这种改进后的排序算法需要一次性取出并缓存的数据比第一种算法要多很多,如果我们将并不需要的Columns 也取出来,就会极大的浪费排序过程所需要的内存。
在MySQL4.1 之后的版本中,我们可以通过设置max_length_for_sort_data 参数大小来控制MySQL 选择第一种排序算法还是第二种排序算法。当所取出的Columns 的单条记录总大小
max_length_for_sort_data 设置的大小的时候,MySQL 就会选择使用第一种排序算法,反之,则会选择第二种优化后的算法。为了尽可能提高排序性能,我们自然是更希望使用第二种排序算法,所以在Query 中仅仅取出我们所需要的Columns 是非常有必要的。仅仅使用最有效的过滤条件很多人在优化Query 语句的时候很容易进入一个误区,那就是觉得WHERE 子句中的过滤条件越多越好,实际上这并不是一个非常正确的选择。其实我们分析Query 语句的性能优劣最关键的就是要让他选择一条最佳的数据访问路径,如何做到通过访问最少的数据量完成自己的任务