在确定查询只是返回需要的数据之后,接下来应该看看查询为了返回结果是否扫描了过多的数据。对于MySQL,最简单的衡量查询开销的三个指标如下:
1响应时间,2扫描行数,3返回行数
没有那个指明能够完美的衡量查询的开销,但他们大致反映了MySQL在内存执行查询时需要访问多少数据,并可以大概推算出查询运行的时间。这三个指标都会记录到MySQL的慢日志中,索引检查慢日志记录时找出扫描行过多的查询的好办法。
1响应时间
要记住,响应时间只是一个表面上的值。这样说肯能看起来和前面关于响应时间的说法有矛盾?其实并不矛盾,响应时间仍然是最重要的指标。这一点有些复杂,细细道来。
响应时间是两个部分之和,服务时间和排队时间。服务时间是指数据库处理这个查询真正花了多长时间。排队时间是指服务器为了等等某些资源而没有真正执行查询的时间--可能是等待io的完成,也可能是等待锁,等等。遗憾的是,我们无法把响应时间细分到这些部分,除非有什么办法能够逐个测量上面这些消耗,不过很难做到。一般最常见和重要的是等待io和锁等待,但实际情更加复杂。
所以在不同类型的应用压力下,响应时间并没有什么一致的规律或者公式。诸如存储引擎的锁(表锁,行锁),高并发资源竞争,硬件响应等诸多因素都会影响响应时间,所以,响应时间既可能是一个问题的结果也可能是一个问题的原因,不同案例情况不同。
当你看到一个查询的响应时间的时候,首先需要问问自己,这个响应时间是否是一个合理的值。实际上可以使用“快速上限估计”法来gust查询的响应时间。概况的说,了解这个查询需要那些索引以及它的执行计划是什么,然后计算大概需要多少个顺序和随机io,在用其诚意在具体硬件条件下的一侧iodine消耗时间,最后把这些消耗都加起来,就可以获得一个大概参考值来排队当前响应时间是不是一个合理的值。
2.扫描的行数和返回的行数。
分析查询时,查看该查询扫描的行数是非常有帮助的。这在一定程度上能够说明该查询找到需要的数据的效率高不高。
对于找出那些糟糕查询,这个指标可能还不够完美,因为并不是所有的行的访问代价都是相同的。较短的行的访问速度相当快,内存中的行业比磁盘中的行的访问速度要快的多。
理想的情况下,扫描的行数和返回的行数应该是相同的。但实际上这种美事并不多。例如在做一个关联查询的时候,扫描的行数和对返回的行数的比率通常都很小,一般在1:1和10:1之间,不过有时候这个值也可能非常大。
3扫描的行数和访问类型。
在评估查询开销的时候,需要考虑一下从表中找到某一行数据的成本。MySQL有好几种访问方式可以查找并返回一行接口。这些访问方式可能需要访问很多行才能返回一条结果,也有些访问方式可能无需扫描就能返回结果。
在EXPLAIN语句中的type列反映了访问类型。访问类型有很多种,从全表扫描到索引扫描,范围扫描,唯一索引,常数索引。这里列的这些,速度是从慢到快,扫描的行数也是从多到少。你不需要记住这些访问类型,但需要明明扫描表,扫描索引,范围访问和单值访问的概念。
如果查询没有办法找到合适的访问类型,那么解决的最好办法通常就是增加一个合适的索引,这也是我们前一章讨论的问题。现在应该明白为什么索引对于查询优化如此重要了。索引让MySQL以最高效,扫描行数最少的方式找到需要的记录。
SELECT * FROM actor WHERE film_id = 1;
这个查询将返回10 行数据,从explain的结果可以看出,MySQL在索引idx_fk_film_id上使用了ref访问类型来执行查询。
EXPLAIN SELECT * FROM film_actor WHERE film_id = 1G
id:1
select type :simple
type:ref
table :film_actor
possible key :****
key :****
key len:2
ref :const
rows:10
explain的结果也显示MySQL预估需要访问10行数据。换句话说,查询优化器认为这种访问类型可以高效的完成查询。如果没有合适的索引会怎样呢》、?MySQL就不得不使用一种更糟糕的访问类型,下面我们来看看如果删除对应的索引来运行这个查询:
ALTER TABLE film_actor DROP FOREIGN KEY fk_fiml_actor_film;
ALTER TABLE film_actor DROP KEY idx_fk_film_id
EXPLAIN SELECT * FROM film_actor WHERE film_id=1 G
id:1
select_type:simple
type:ALL
table:film_actor
possible_key:NULL
kye:NULL;
key_len:null
ref :null
rows:5073
EXTRA :using where
正如我们预测的,访问类型变成了一个全表扫描,限制MySQL预估需要扫描5073条记录就能完成这个查询。这里的USING WHERE 表示MySQL将通过WHERE 条件来筛选存取引擎返回的记录。
一般MySQL能够使用如下三种方式使用WHERE 条件,从好到坏依次为:
1在索引中使用where调价来过滤掉不匹配的记录。这是在存储引擎层完成的。
2使用索引覆盖扫描(在Extra列中出现USING INDEX )来返回记录,直接从索引中过滤掉不需要的记录并返回命中结果。这是在MySQL服务器层完成的,但无需再回表查询记录。
3从数据表中返回数据,然后过滤不满足条件的记录(在extra 列中出现using where)。这是在MySQL服务器层完成的,MySQL需要从数据表中读出记录然后过滤。上面这个例子说明了好的索引是多么重要。好的索引可以让查询使用合适的访问类型,尽可能的只扫描需要的数据行。但也不是说增加索引就能让扫描的行数等于返回的行数。例如下面的使用聚合函数COUNT()的查询:
SELECT actor_id ,COUNT(*) FROM film_actor GROUP BY actor_id
这个查询需要读取几千行数据,但是仅返回了200行结果。没有什么索引能够让这样的查询减少需要扫描的行数。
不幸的是,MySQL不会告诉我们生成结果实际上需要扫描多数行数据,而只会告诉我们生成结果时一共扫描了多数行数据。扫描的函数中大部分都很可能被where条件过滤掉的,对最终的结果并没有贡献。在上面的例子中,我们傻叉收益后,看到MySQL需要扫描索引记录然后根据where条件过滤,最终返回10行结果。理解一个查询需要扫描多数行和实际需要使用的行数首先需要理解这个查询背后的逻辑和思想。
如果发现查询需要大量的数据但值返回少数行,那么通常可以尝试下面的技巧去优化它:
1使用索引覆盖扫描,把所有需要用的列都放到索引中,这样存储引擎无需回表获取对应的行就可以返回结果了
2该表库表结构。例如使用单独的汇总表、
3重写这个查询,然MySQL优化器能够以更优化的方式执行这个查询。