查询性能优化
如何设计最优的库表结构、如何建立最好的索引,这些对于高性能来说是必不可少的。但这些还不够, 还需要合理的设计查询。如果查询写得很糟糕,即使库表结构再合理、索引再合适,也无法实现高性能。
1、为什么查询会慢
如果把一条sql查询看做是一个完整的任务,那么它就会由一系列子任务组成,每个子任务都需要消耗一定的时间,我们对于sql的优化,其实就是对于它子任务的优化,要么消除其中一些子任务,要么减少子任务的执行次数,要么让子任务运行得更快,有了这些概念,我们就来看看如何优化查询。
2、慢查询基础:优化数据访问
通常查询慢的原因,我们可以简单地分为以下两类:
1. 确认程序是否需要检索大量超过需要的数据,这通常意味着访问了太多的行,也可能是访问了太多的列;
2. 确认mysql服务器是否分析大量超过需要的数据行。
是否向数据库请求了不需要的数据
有些查询会返回应用程序不需要的数据,可能是多余的行,也可能是多余的列,这些多余的数据会被应用程序抛弃,同时也会给mysql服务器带来额外的负担,增加网络开销,另外也会消耗服务器的CPU和内存资源。
查询不需要的数据:
有时候我们只需要查询十条信息,但是mysql却返回了全部的信息,我们只取前面的十条,客户端的应用程序会接收全部的结果集数据,然后抛弃其中大部分的数据,这时候为了避免资源浪费,最简单的方法就是加上limit。
多表关联的时候返回全部列
假设有下列查询:
select * from actor join film_actor on actor.id = film_actor.actor_id where
actor.id = 1;
我们要查询出这个演员演的所有电影,此时会返回两个表全部的数据列,其实我们只需要返回需要的列就行了。
select film_actor.film_id from .....
总是取出全部的列
当我们每次看到select * 的时候,都应该用怀疑的眼光审视,因为很多列可能都是不必要的,如果去取全部列,会让优化器无法完成索引覆盖扫描这类优化,但是,如果这种浪费可以简化开发,提高复用性,如果清楚这种做法的性能影响,那这种方法还是可取的。
总是重复查询相同的数据
不断地重复执行相同的查询,然后每次都返回相同的数据(比如商城主页,比如重复查询用户的头像url),那么这种情况,我们可以把这种热点数据缓存起来,需要的时候从缓存中取出,这样性能显然会更好。
mysql是否在扫描额外的记录
在完成了确定查询只返回最基本的字段后,接下来应该看看查询为了返回结果是否需要扫描了过多的数据。
对于mysql,最简单的衡量查询开销的指标如下:
- 响应时间
- 扫描的行数
- 返回的行数
这三个指标都会记录到MySQL的慢日志记录中,查询慢日志记录是找出扫描行数过多的查询的办法之一。
响应时间
响应时间是两个部分之和:服务时间和排队时间。服务时间是指数据库处理这个查询真正花了多长时间。
排队时间是指服务器因为等待某些资源而没有真正执行查询的时间——可能是等I/O操作完成,也可能是等待行锁,等等。遗憾的是,我们无法把响应时间细分到上面这些部分,除非有什么办法能够逐个测量上面这些消耗,不过很难做到。一般最常见和重要的等待是I/O和锁等待,但是实际情况更加复杂。
扫描的行数和返回的行数
理想情况下扫描的行数和返回的行数是相同的,但在实际应用中,该情况极少发生。例如在做一个关联查询时,服务器必须要扫描多行才能生成结果集中的一行。扫描的行数对返回的行数的比率通常很小,一般在1:1和10:1之间,不过有时候这个值也可能非常非常大。
扫描的行数和访问类型
在评估查询开销的时候,要考虑一下从表中找到某一行数据的成本,mysql有好几种访问方式可以查找并返回一行结果,有些访问方式可能需要扫描很多行才能获取结果,有些访问方式可能无须扫描就能返回结果。
explain语句的type列反应了访问类型,访问类型有全表,范围,唯一索引等
如上图就使用了全表扫描,同时这里的Using Where 标识mysql通过where条件来筛选存储引擎返回的记录。
一般mysql能够使用如下三种方式应用where条件,从好到坏依次为:
1. 在索引中使用where条件来过滤不匹配的记录,这是在存储引擎层完成的。
2. 使用索引覆盖扫描来返回记录(Extra : Using index),直接从索引中过滤不需要的记录并返回命
中的结果,这是在mysql服务层完成的,但无需回表查询记录。
3. 从数据表中返回数据,然后过滤不满足条件的记录(Extra: Using Where)。这在 MySQL服务器层完成,MySQL需要先从数据表读出记录然后过滤。
好的索引可以让查询使用合适的访问类型,尽可能地只扫描需要的数据行。但也不是说增加索引就能让扫描的行数等于返回的行数。
如果发现查询需要扫描大量的数据但只返回少数的行,那么通常可以尝试下面的技巧去优化它:
- 使用索引覆盖扫描,把所有需要用的列都放到索引中,这样存储引擎无须回表获取对应行就可以返回结果了。
- 改变库表结构。例如使用单独的汇总表。
- 重写这个复杂的查询,让MySQL优化器能够以更优化的方式执行这个查询。
3、重构查询方式
在优化有问题的查询时,目标应该是找到一个更优秀的方法获得一个实际需要结果——而不是一定总是要从MySQL获取一个一模一样的结果集,有时候可以换一种写法获得一模一样的结果,但是性能更好。
一个复杂查询还是多个简单查询
我们在设计一个复杂查询的时候,对于是否需要将一个复杂的查询拆分成多个简单的查询,这点值得商榷。在传统的实现中,总是强调要数据库层完成尽可能多的操作,这样的逻辑是基于以前网络通信是一件代价很高的事情。但是这样的想法对于MySQL并不适用,现代带宽已经越来越大,而且mysql在设计上从连接到断开都很轻量级,所以运行多个小查询已经不是什么大问题了。
切分查询
有时候我们对于一个比较大的查询这个时候我们需要分而治之,将大查询切分成小查询,每个查询功能完全一样,只完成一小部分。
举个例子,删除大量旧数据,更新大量用户的数据等,每次都用一个大的语句一次性完成的话,则可能就需要一次性锁住很多数据,占满整个事务日志,耗尽系统资源,阻塞很多很小的但是很重要的查询。
但是如果我们每次从数据库中取出100个用户的数据,然后进行滚动更新,不仅减少了内存占用,也减少了数据库的开销。
分解关联查询
有时候,我们会对很多join的表进行拆解查询,然后将得出的结果再在应用程序中进行关联。
比如说,下面这个查询:
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);
其实将一条sql拆解成三条sql,有以下好处:
- 让缓存的效率更高。许多应用程序可以方便地缓存单表查询对应的结果对象。另外,对MySQL的查询缓存来说,如果关联中的某个表发生了变化,那么就无法使用查询缓存了,而拆分后,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存结果了。
- 将查询分解后,执行单个查询可以减少锁的竞争。
- 在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。
- 查询本身也有可能会提升。用in()代替关联查询,可以让MySQL 按照ID顺序进行查询,这可能比随机的关联更高效。
- 可以减少冗余记录的查询。在应用层做关联查询,意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需要重复地访问一部分数据。
- 更进一步,这样做相当于在应用中实现了哈希关联,而不是使用MySQL 的嵌套循环关联。某些场景哈希关联的效率要高很多。
4、查询执行的基础
当我们向MySQL 发送一个请求的时候,MySQL 到底做了什么:
1. 客户端发送一条查询给服务器。
2. 服务器先检查缓存,如果命中了缓存,则立刻返回存储在缓存中的结果,否则进入下一阶段。
3. 服务器端进行sql解析,预处理,再由优化器生成对应的执行计划。
4. MySQL 根据优化器生成的执行计划,调用存储引擎的API来执行查询。
5. 将结果返回给客户端。
查询缓存
如果当前的查询恰好命中了查询缓存,那么在返回查询结果之前MySQL会检查一次用户权限。这仍然是无须解析查询SQL 语句的,因为在查询缓存中已经存放了当前查询需要访问的表信息。如果权限没有问题,MySQL会跳过所有其他阶段,直接从缓存中拿到结果并返回给客户端。这种情况下,查询不会被解析,不用生成执行计划,不会被执行。
查询优化处理
查询的生命周期的下一步是将一个SQL转换成一个执行计划,MySQL再依照这个执行计划和存储引擎进行交互。这包括多个子阶段:解析SQL、预处理、优化SQL执行计划。
语法解析和预处理:
首先,mysql通过关键字对查询语句进行解析,并生成一颗对应的"解析树"。mysql解析器将使用mysql语法规则校验和解析查询。例如:是否使用错误的关键字,或者关键字的顺序是否正确等等。
预处理器则根据一些MySQL规则进一步检查解析树是否合法,例如,这里将检查数据表和数据列是否存在,还会解析名字和别名,看看它们是否有歧义。
下一步预处理器会验证权限。这通常很快,除非服务器上有非常多的权限配置。
查询优化器:
现在查询已经被认为是合法的了,但是一条查询可以有多种方式进行查询,最后都会返回相同的结果,优化器的作用就是找到这其中最好的执行计划。
MySQL使用基于成本的优化器,他会预测一个查询的成本,然后选择成本最小的一个。
我们可以使用 :
show status like 'Last_query_cost' 来计算最后一个查询的成本。
如图,查询一个这个结果需要从87241个数据页来完成以上查询。
有很多种原因会导致MySQL优化器选择错误的执行计划,如下所示:
- 统计信息不准确。MySQL依赖存储引擎提供的统计信息来评估成本,但是有的存储引擎提供的信息是准确的,有的偏差可能非常大。例如,InnoDB因为其MVCC的架构,并不能维护一个数据表的行数的精确统计信息。
- 执行中的成本估算不等于实际的成本。所以即使统计信息精确,优化器给出的执行计划也可能不是最优的。例如有时候某个执行计划需要读取更多的页,但是它的成本却更小,因为如果这些页面都是顺序或者这些页都已经在内存中的话,MySQL并不知道哪些页在内存或者在磁盘中,所以查询实际执行过程中到底需要多少次物理IO是无法得知的。
- MySQL的最优是基于成本模型,可能和我们所知的缩短查询时间的成本不太相同。
- MySQL从不考虑其它并发执行的查询,这可能会影响到当前查询的速度。
- MySQL不会考虑不受其控制的操作的成本,例如执行存储过程或者用户自定义函数的成本。
- 优化器有时候无法去估算所有可能的执行计划,所以它可能错过实际上最优的执行计划。
下列是MySQL能够处理的优化类型:
- 重新定义关联表的顺序:比如我们a join b表,我们会以一张表为基准,对每一行数据进行对另外一张表的扫描,这时候优化器就会选择表数据比较小的那张,当做基础表,可以减少大量匹配。
- 使用等价变换原则,MySQL会自动简化一些表达式,比如会自动去掉1=1,类似的,比如有 a < b and a = 5,MySQL就会优化为b > 5。
- 对于count() ,max(),min()这些,索引可以帮我们优化查询,例如要找某一列的最小或者最大值,只需要查询索引B-TREE最左边或者最右边的记录,类似的,如果没有where条件的count() MyISAM维护了一个变量来存放数据表的行数。
- 覆盖索引扫描:当索引中的字段完全包括了要查询的字段,就可以从索引中返回所有数据,不用回表查询了。
- 提前终止:当我们满足查询需求的时候,mysql会立刻停止查询,一个典型的例子就是limit关键字,或者mysql发现了一个不成立的条件,比如非空的id等于null,也会提前终止查询。
- 列表in()的比较:mysql 将 in()里面的数据先进行排序,然后通过二分查找的方式来确定列表中的值是否满足,这是一个O(log n)复杂度的操作。
数据和索引的统计信息
因为服务器层没有任何统计信息,所以MySQL查询优化器在生成查询的执行计划时,需要向存储引擎获取相应的统计信息。存储引擎则提供给优化器对应的统计信息,包括:每个表或者索引有多少个页面、每个表的每个索引的基数是多少、数据行和索引长度、索引的分布信息等。优化器根据这些信息来选择一个最优的执行计划。
MySQL如何执行关联查询:
当前MySQL关联执行的策略很简单:MySQL对任何关联都执行嵌套循环关联操作,即MySQL 先在一个表中循环取出单条数据,然后再嵌套循环到下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为止。然后根据各个表匹配的行,返回查询中需要的各个列。MySQL会尝试在最后一个关联表中找到所有匹配的行,如果最后一个关联表无法找到更多的行以后,MySQL返回到上一层次关联表,看是否能够找到更多的匹配记录,依此类推迭代执行。
关联查询优化器
MySQL优化器最重要的一部分就是关联查询优化,它决定了多个表关联时的顺序。通常多表关联的时候,可以有多种不同的关联顺序来获得相同的执行结果。关联查询优化器则通过评估不同顺序时的成本来选择一个代价最小的关联顺序。
下面的查询可以通过不同顺序的关联最后都获得相同的结果:
SELECT film.film_id,film.title,actor.actor_id,actor.first_name[影片和演员信息]
FROM sakila.film
INNER JOIN sakila.film_actor USING(fi1m_id)
INNER JOIN sakila.actor USING(actor_id);
预期顺序:[film、film_actor、actor]
使用EXPLAIN看看MySQL将如何执行这个查询:
使用STRAIGHT_J0IN关键字,按照之前的顺序执行,这里是对应的EXPLAIN输出结果:
结论:倒转的关联顺序会让查询进行更少的嵌套循环和回溯操作。查看对应的 Last_query_cost状态值。看到倒转的关联顺序的预估成本为241,而原来的查询的预估成本为1154。
排序优化:
无论如何排序都是一个消耗很高的操作,所以从性能的角度来看,应尽可能地避免排序或者对大量数据进行排序。
当不能使用索引生成排序结果的时候,MySQL需要自己进行排序,如果数据量小则在内存中进行,如果数据量大则需要使用磁盘,不过MySQL将这个过程统一称为文件排序(filesort),即使完全是内存排序不需要任何磁盘文件时也是如此。
如果需要排序的数据量小于“排序缓冲区”,MySQL使用内存进行“快速排序”操作。如果内存不够排序,那么MySQL会先将数据分块,对每个独立的块使用“快速排序”进行排序,并将各个块的排序结果存放在磁盘上,然后将各个排好序的块进行合并(merge),最后返回排序结果。
MySQL有两种排序算法:
两次传输排序(旧版本使用):
读取行指针和需要排序的字段,对其进行排序,然后再根据排序结果读取所需要的数据行。
这需要进行两次数据传输,即需要从数据表中读取两次数据,第二次读取数据的时候,因为是读取排序列进行排序后的所有记录,这会产生大量的随机I/O,所以两次数据传输的成本非常高。
单次传输排序(新版本使用):
先读取查询所需要的的所有列,然后再给定列进行排序,最后直接返回排序结果,这就不需要对表进行两次读取,对于IO密集型的应用,这样做的效率高了很多。缺点是,如果需要返回的列非常多,非常大,会额外占用大量的空间,而这些列对排序操作本身来说是没有任何帮助的。
查询执行引擎
在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL 的查询执行引擎则根据这个执行计划来完成整个查询。这里执行计划是一个数据结构,而不是和很多其他的关系型数据库那样会生成对应的字节码。
相对于查询优化阶段,查询执行阶段不是那么复杂:MySQL 只是简单地根据执行计划给出的指令逐步执行。在根据执行计划逐步执行的过程中,有大量的操作需要通过调用存储引擎实现的接口来完成,这些接口也就是我们称为“handler API”的接口。查询中的每一个表由一个handler的实例表示。前面我们有意忽略了这点,实际上,MySQL在优化阶段就为每个表创建了一个handler实例,优化器根据这些实例的接口可以获取表的相关信息,包括表的所有列名、索引统计信息,等等。
存储引擎接口有着非常丰富的功能,但是底层接口却只有几十个,这些接口像“搭积木”一样能够完成查询的大部分操作。为了执行查询,MySQL只需要重复执行计划中的各个操作,直到完成所有的数据查询。
5、MySQL查询优化器的局限性
关联子查询
MySQL的子查询实现地非常糟糕,最糟糕的一类是条件中包含IN()的子查询语句,例如,我们希望找到演员id为1的人演过的所有影片,可能会这么写:
select film_name from film where film_id in (select film_id from film_actor
where actor_id = 1)
我们眼里认为,sql会先执行in里面的语句,然后再执行外面的语句 :
select film_name from film where film_id in (1,2,3)
其实不会,MySQL会将相关的外层查询压到子表中,他认为这样会更快:
select film_name from film
where exists (select film_id from film_actor where actor_id = 1 and
film_actor.film_id = film.film_id)
MySQL会先会对film表进行全表扫描,然后根据返回的film_id逐个进行子查询。如果这是一个非常大的表,这样的查询将会非常糟糕。 当然我们可以用join来重写这个语句:
select film_name
from film join film_actor on film_actor.film_id = film.film_id ;
UNION的限制
有时,MySQL无法将限制条件从外层“下推”到内层,这使得原本能够限制部分返回结果的条件无法应用到内层查询的优化上。
如果希望UNION的各个子句能够根据LIMIT只取部分结果集,或者希望能够先排好序再合并结果集的话,就需要在UNION的各个子句中分别使用这些子句。例如,想将两个子查询结果联合起来,然后再取前20条记录,那么MySQL会将两个表都存放到同一个临时表中,然后再取出前20行记录:
(SELECT first_name,last_nameFROM sakila.actor
ORDER BY last_name)
UNION ALL
(SELECT first_name,last_nameFROM sakila.customer
ORDER BY last_name)
LIMIT 20;
等值传递
某些时候,等值传递会带来一些意想不到的额外消耗。例如,有一个非常大的IN()列表,而MySQL优化器发现存在WHERE、ON或者USING的子句,将这个列表的值和另一个表的某个列相关联。
那么优化器会将IN()列表都复制应用到关联的各个表中。通常,因为各个表新增了过滤条件,优化器可以更高效地从存储引擎过滤记录。但是如果这个列表非常大,则会导致优化和执行都会变慢。
最大值和最小值优化
对于MIN()和MAX()查询,MySQL的优化做得并不好。这里有一个例子:
SELECT MIN(actor_id) FROM sakila.actor NHERE first_name = 'PENELOPE';
因为在first_name字段上并没有索引,因此MySQL将会进行一次全表扫描。如果MySQL能够进行主键扫描,那么理论上,当MySQL读到第一个满足条件的记录的时候,就是我们需要找的最小值了,因为主键是严格按照actor_id字段的大小顺序排列的。
6、优化特定类型的查询
优化count()的查询
关于MyISAM,因为这种存储引擎会保存所有行数,但是是在没有where条件下的,有时候我们可以利用这一特性,来加速一些count()的查询,比如:
select count(*) from city where id > 5;
假设我们这时候要扫描5000行数据,如果将条件反转一下,先查询<=5的行数,然后再用总行数减去它,也能获得相同的结果,不过速度会快很多。
select ( select count() from city ) -count() from city where id < = 5;
优化关联查询
- 确保ON或者USING子句中的列上有索引。在创建索引的时候就要考虑到关联的顺序。当表A和表B用列c关联的时候,如果优化器的关联顺序是B、A,那么就不需要在B表的对应列上建上索引。没有用到的索引只会带来额外的负担。一般来说,除非有其他理由,否则只需要在关联顺序中的第二个表的相应列上创建索引。
- 小表驱动大表,写多表连接sql时如果明确知道哪张表是小表可以用straight_join写法固定连接驱动方式,省去mysql优化器自己判断的时间
优化LIMIT分页
在系统中需要进行分页操作的时候,我们通常会使用LIMIT加上偏移量的办法实现。但在偏移量非常大的时候,例如可能是LIMIT 10000,20这样的查询,这时MySQL需要查询10 020条记录然后只返回最后20条,前面10 000条记录都将被抛弃,这样的代价非常高。
- 有时候也可以将LIMIT查询转换为已知位置的查询,让MySQL通过范围扫描获得到对应的结果。例如,如果在一个位置列上有索引,并且预先计算出了边界值,然后进行范围查找;
- 使用预先计算的汇总表,或者关联到一个冗余表,冗余表只包含主键列和需要做排序的数据列;
- 记录上一次的末尾值,下次查询通过索引定位。
MySQL查询剖析
对查询进行性能剖析有两种方式,每种方式都有各自的问题。可以剖析整个数据库服务器,这样可以分析出哪些查询是主要的压力来源(如果已经在最上面的应用层做过剖析,则可能已经知道哪些查询需要特别留意)。定位到具体需要优化的查询后,也可以钻取下去对这些查询进行单独的剖析,分析哪些子任务是响应时间的主要消耗者。
剖析服务器负载
服务器端的剖析很有价值,因为在服务器端可以有效地审计效率低下的查询。定位和优化“坏”查询能够显著地提升应用的性能,也能解决某些特定的难题。还可以降低服务器的整体压力,这样所有的查询都将因为减少了对共享资源的争用而受益(“间接的好处”)。降低服务器的负载也可以推迟或者避免升级更昂贵硬件的需求,还可以发现和定位糟糕的用户体验,比如某些极端情况。
捕获 MySQL的查询到日志文件中
在MySQL中,慢查询日志最初只是捕获比较“慢”的查询,而性能剖析却需要针对所有的查询。而且在MySQL 5.0及之前的版本中,慢查询日志的响应时间的单位是秒,粒度太粗了。幸运的是,这些限制都已经成为历史了。在 MySQL 5.1及更新的版本中,慢日志的功能已经被加强,可以通过设置long_query_time为0来捕获所有的查询,而且查询的响应时间单位已经可以做到微秒级。
在MySQL的当前版本中,慢查询日志是开销最低、精度最高的测量查询时间的工具。如果还在担心开启慢查询日志会带来额外的I/O开销,那大可以放心。我们在I/O密集型场景做过基准测试,慢查询日志带来的开销可以忽略不计(实际上在CPU密集型场景的影响还稍微大一些)。更需要担心的是日志可能消耗大量的磁盘空间。如果长期开启慢查询日志,注意要部署日志轮转((log rotation)工具。或者不要长期启用慢查询日志,只在需要收集负载样本的期间开启即可。
分析查询日志
一般情况下,只需要将慢查询日志文件作为参数传递给pt-query-digest,就可以正确地工作了。它会将查询的剖析报告打印出来,并且能够选择将“重要”的查询逐条打印出更详细的信息。输出的报告细节详尽,绝对可以让生活更美好。该工具还在持续的开发中,因此要了解最新的功能请阅读最新版本的文档。
这里给出一份pt-query-digest输出的报告的例子,作为进行性能剖析的开始。这是前面提到过的一个未修改过的剖析报告:
可以看到这个比之前的版本多了一些细节。首先,每个查询都有一个ID,这是对查询语句计算出的哈希值指纹,计算时去掉了查询条件中的文本值和所有空格,并且全部转化为小写字母(请注意第三条和第四条语句的摘要看起来一样,但哈希指纹是不一样的)。该工具对表名也有类似的规范做法。表名InvitesNew后面的问号意味着这是一个分片(shard)的表,表名后面的分片标识被问号替代,这样就可以将同一组分片表作为一个整体做汇总统计。这个例子实际上是来自一个压力很大的分片过的Facebook应用。
报告中的V/M列提供了方差均值比(variance-to-mean ratio)的详细数据,方差均值比也就是常说的离差指数(index of dispersion)。离差指数高的查询对应的执行时间的变化较大,而这类查询通常都值得去优化。如果pt-query-digest指定了--explain选项,输出结果中会增加一列简要描述查询的执行计划,执行计划是查询背后的“极客代码”。通过联合观察执行计划列和V/M列,可以更容易识别出性能低下需要优化的查询。
最后,在尾部也增加了一行输出,显示了其他17个占比较低而不值得单独显示的查询的统计数据。可以通过--limit和--outliers选项指定工具显示更多查询的详细信息,而不是将一些不重要的查询汇总在最后一行。默认只会打印时间消耗前10位的查询,或者执行时间超过1秒阈值很多倍的查询,这两个限制都是可配置的。
剖析报告的后面包含了每种查询的详细报告。可以通过查询的ID或者排名来匹配前面的剖析统计和查询的详细报告。下面是排名第一也就是“最差”的查询的详细报告:
查询报告的顶部包含了一些元数据,包括查询执行的频率、平均并发度,以及该查询性能最差的一次执行在日志文件中的字节偏移值,接下来还有一个表格格式的元数据,包括诸如标准差一类的统计信息。
接下来的部分是响应时间的直方图。有趣的是,可以看到上面这个查询在Query_timedistribution部分的直方图上有两个明显的高峰,大部分情况下执行都需要几百毫秒,但在快三个数量级的部分也有一-个明显的尖峰,几百微秒就能执行完成。如果这是Percona Server的记录,那么在查询日志中还会有更多丰富的属性,可以对查询进行切片分析到底发生了什么。比如可能是因为查询条件传递了不同的值,而这些值的分布很不均衡,导致服务器选择了不同的索引﹔或者是由于查询缓存命中等。在实际系统中,这种有两个尖峰的直方图的情况很少见,尤其是对于简单的查询,查询越简单执行计划也越稳定。
剖析单条查询
使用SHOW PROFILE
SHOw PROFILE命令是在MySQL 5.1以后的版本中引入的,来源于开源社区中的JeremyCole的贡献。这是在本书写作之际唯一一个在GA版本中包含的真正的查询剖析工具。默认是禁用的,但可以通过服务器变量在会话(连接)级别动态地修改。
mysql> SET profiling = 1;
然后,在服务器上执行的所有语句,都会测量其耗费的时间和其他一些查询执行状态变更相关的数据。
当一条查询提交给服务器时,此工具会记录剖析信息到一张临时表,并且给查询赋予一个从1开始的整数标识符。下面是对Sakila样本数据库的一个视图的剖析结果.
mysql>SELECT *FROM sakila.nicer_but_slower_film_list;
[query results omitted]
997 rows in set (o.17 sec)
该查询返回了997行记录,花费了大概1/6秒。下面看一下SHOW PROFILES有什么结果:
首先可以看到的是以很高的精度显示了查询的响应时间,这很好。MySQL客户端显示的时间只有两位小数,对于一些执行得很快的查询这样的精度是不够的。下面继续看接下来的输出:
尽管剖析报告能帮助我们定位到哪些活动花费了最多的时间,但并不会告诉我们为什么会这样。要弄清楚为什么复制数据到临时表要花费这么多时间,就需要深入下去,继续剖析这一步的子任务。
使用SHOW STATUS
SHOw STATUS是一个有用的工具,但并不是一款剖析工具。SHOW STATUS的大部分结果都只是一个计数器,可以显示某些活动如读索引的频繁程度,但无法给出消耗了多少时间。SHOW STATUS的结果中只有一条指的是操作的时间(Innodb_row_lock_time),而且只能是全局级的,所以还是无法测量会话级别的工作。
尽管SHOW STATUS无法提供基于时间的统计,但对于在执行完查询后观察某些计数器的值还是有帮助的。有时候可以猜测哪些操作代价较高或者消耗的时间较多。最有用的计数器包括句柄计数器(handler counter)、临时文件和表计数器等。下面的例子演示了如何将会话级别的计数器重置为0,然后查询前面(“使用SHOW PROFILE”一节)提到的视图,再检查计数器的结果:
从结果可以看到该查询使用了三个临时表,其中两个是磁盘临时表,并且有很多的没有用到索引的读操作(Handler_read_rnd_next)。假设我们不知道这个视图的具体定义,仅从结果来推测,这个查询有可能是做了多表关联(join)查询,并且没有合适的索引,可能是其中一个子查询创建了临时表,然后和其他表做联合查询。而用于保存子查询结果的临时表没有索引,如此大致可以解释这样的结果。
使用这个技术的时候,要注意SHOW STATUS本身也会创建一个临时表,而且也会通过句柄操作访问此临时表,这会影响到SHOW STATUS结果中对应的数字,而且不同的版本可能行为也不尽相同。比较前面通过SHOw PROFILES获得的查询的执行计划的结果来看,至少临时表的计数器多加了2。
你可能会注意到通过EXPLAIN查看查询的执行计划也可以获得大部分相同的信息,但EXPLAN是通过估计得到的结果,而通过计数器则是实际的测量结果。例如,EXPLAIN无法告诉你临时表是否是磁盘表,这和内存临时表的性能差别是很大的。