MySQL查询优化处理
查询的生命周期的下一步是将一个sql转化成一个执行计划,MySQL再依照这个执行计划和存储引擎进行交互。这包括多个子阶段:解析sql,预处理,优化sql执行计划。这个过程中任何错误(例如语法错误)都可能终止查询。这里不打算详细介绍MySQL内部实现,而只是选择性的介绍其中几个独立的部分,在实际中,这几部分可能以前执行也可能单独执行。我们的目的是帮助大家理解MySQL是如何执行查询的,以便写出更优秀的查询。
· 语法解析器和预处理
首先,MySQL通过关键字语句进行解析,并生成一科对应的“解析树”,MySQL集线器将使用MySQL语法规则验证和解析查询。例如,它将验证是否使用错误的关键字,或者使用关键字的顺序是不是正确等,再或者他还会验证引号是否能前后正确匹配。
预处理器则根据一些MySQL规则进一步验证解析树是否合法,,例如,这里将坚持数据表和数据列是否存在,还会解析名字和别名,看看他们是否有歧义。
下一步预处理器会验证权限。这通常会非常快,除非服务器上有非常多的权限配置。
查询优化器
限制语法数被认为是合法的了,并且由优化器将其转化成执行计划。一条查询可以有很多种查询方式,最后都会返回相同的结果。游虎丘的作用就是找到这其中最好的执行计划。
MySQL使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。最初,成本的最小单位是随机读取一个4k的数据源的成本,后来(成本的计算公式)变得更加复杂,并且引入了一些“因子”来估算某些操作的代价,如当执行一次where条件比较的成本。可以通过查询当前会话的Last_query_cost的值来的值MySQL计算的当前查询的成本。
SELECT SQL_NO_CACHE COUNT(*) FROM actor;
SELECT STATUS LIKE 'Last_query_cost'
Variable_name Value
Last_query_cost 1040.343400
这个结果表示MySQL的优化器认为大概需要做1040个数据源的随机查找才能完成上面的查询。这是根据一系列的统计信息计算得来的:每个表或者索引的页面个数、索引的基数(索引中不同值的数量)、索引和数据行的长度、索引的分布情况。优化器在评估成本的时候并不会考虑任何层面的缓冲,它假设读取任何数据都需要一次磁盘io
很多原因会导致MySQL优化器选择错误的执行计划,如下所示:
统计信息不准确。MySQL依赖存储引擎提供的统计信息来评估成本,但是有的存储引擎提供的信息是准确的,优点偏差可能非常大。例如,innodb因为其mvcc的架构,并不能维护一个数据表的行数的精确的统计信息。
执行计划中的成本估算不等同于实际执行的成本。所以即使统计信息准确,优化器给出的执行计划也可能不是罪优的。例如有时候某个执行计划虽然需要读取更多的页面,但是他的成本却更小。因为如果这些页面都是顺序读取或者这些页面都已经在内存中存在,那么它的访问成本将会很小。MySQL层面并只知道那些页面在内存中,那些在磁盘上,所以查询实际执行的过冲中到底需要多少次窝里io是无法得知的。
MySQL的最优化可能和你像的最后不一样。你可能希望执行时间尽可能的短,但是MySQL只是基于其成本模型选择最优的执行计划,而有些时候这并不是最快的执行方式。索引,这里我们看到的根据执行成本来选择执行计划并不是完美的模型。
MySQL从不考虑其他并发的执行查询,这可能会影响到当前的查询速度。
MySQL也并不是任何时候都是基于成本的优化。有时也会基于一些固定的规则,例如,如果在全文所搜的MATCH()子句,则在全文索引的时候就使用全文索引,即使有时候使用别的索引和where条件可以远比这方式要快,MySQL仍然会使用对应的全文索引。
MySQL不会考虑其控制的操作成本,例如执行存储过程或者用户自定义函数的成本。
后面我们还会看到,优化器有时候无法去估算所有可能的执行计划,所以它可能错过实际上最优的执行计划。
MySQL的查询优化器是一个非常复杂的部件,它使用了很多优化策略来生成一个最优的执行计划。优化策略可以简单的分为两种:一种是静态优化,一种是动态优化。静态优化可以直接对解析树进行分析,并完成优化。例如,又好看可以通过一些简单的代数变化将WHERE 条件转换成另一种等价的形式。静态优化不依赖于特别的数值,如where条件中带入的一些常数等。静态优化在第一次完成后就一直有效,即使使用不同的参数执行查询也不会发生变化。可以认为这是一种“编译时优化”。
相反,动态优化则和查询的上下文有关,也可能和很多其他因素有关,例如WHERE 条件中的取值,索引中那个条目对应的数据行数等。这需要在每次查询的时候都重新评估,可以认为这是 “运行时优化”。
在执行语句和存储过程的时候,动态优化和静态优化的区别非常重要。MySQL对查询的静态优化只需要做一次,但对查询的动态优化规则在每次执行的时候都需要评估,有时候甚至在查询的过程中重新优化(例如,在关联操作过程中,范围检查的执行计划会针对每一行重新评估索引。可以通过EXPLAIN 执行计划红的Extra列是否有“range checked for each record” 来确认这一点。该执行计划还会增加select_full_range_join 这个服务器变量的值)。
下面是MySQL能够处理的优化类型:
重新定义关联表的顺序
数据表的关联并不总是按照在查询中指定的顺序进行。决定关联的顺序是优化器很重要的一部分功能。
将外连接转化成内连接
并不是所有的OUTER JOIN 语句都必须以外连接的方式执行。诸多因素,例如WHERE 条件,库表结构都可能会让外连接等价成一个内连接。MySQL能够识别这点,并重写查询,让其可以调整关联顺序。
使用等价变换规则
MySQL可以使用一些等价变换来简化并规范表达式。它可以合并和减少一些比较,还可以移除一些恒成立和一些恒不成立的判断。例如(5=5 and a>5) 将被改写成a>5 .类似的,如果有(a<b AND b<c) AND a = 5则会改写成b>5 AND b=c AND a=5。这些条件对于我们编写条件语句很有用。
优化COUNT () ,MIN()和MAX()
索引和列是否可为空通常可以帮助MySQL优化这类表达式。例如,要找到某一列的最小值,只需要查询在B-Tree索引最左短的记录,MySQL可以直接获取索引一行记录。在优化器生成执行计划的时候就可以利用这一点,在B-Tree 索引中,优化器会将这个表达式作为一个常数对待。类似的,如果要查找一个最大值,也只需要读取b-tree所用的最后一条记录。如果MySQL使用了这种类型的优化,那么再EXLAIN中就可以看到’Select tables optimized away‘ 。从字面的意思可以看出,他表示优化器已经冲执行计划中移除了该表,并以一个常数取而代之。
预估并转化为常数表达式
当MySQL检测到一个表达式可以转换为常数的时候,就会一直把该表达式作为常数进行优化处理。例如,一个用户自定义变量在查询中没有发生变化时就可以转换成一个常数。数学表达式则是另一种典型的例子。
让人惊讶的是,在优化阶段,有时候甚至一个查询也能够优化成一个常数。一个例子是在索引上执行min()函数。甚至是主键或者唯一键查找语句也可以转换为常数表达式。例如在where子句中使用了该类索引的常数条件,MySQL可以在查询开始阶段就先找到这些值,这样优化器就能够知道并转换为常量表达式。下面是一个例子:
EXPLAIN SELECT film.film_id,film_actor.actor_id FROM film inner join film_actor USINT(film_id) WHERE fim.fil_id = 1
MySQL将分两步来执行这个查询。第一步先从film 表找到需要的行。因为在film_id 字段上有主键索引,所以MySQL优化器知道这只会返回一条数据。因为优化器已经明确指定有多少个值需要做索引查询,索引这里的表访问类型是const 。
在执行计划的第二步,MySQL将第一步中返回的film_id列当作一个已知取值的列来处理。因为优化器清楚在第一步执行完成之后,该值就会是明确的了。注意到正如第一步中一样使用fiml_actor字段对表的访问类型也是const。
另一种会看到常数条件的情况是通过等式将常数值从一个表传到另一个表,这可以同where,using 或者on 语句来限制某列取值为常数。在上面的例子中,因为使用了USING子句,优化器知道了也限制了film_id在整个查询过程中始终都是一个常量
覆盖索引扫描
当索引中的列包含所有查询中需要使用的列的时候,MySQL就可以使用索引返回需要的数据,而无需查询对应的数据行。
子查询优化
MySQL在某些情况下可以将子查询转换成一种效率更高的形式,从而减少多个查询多次对数据进行访问。
提前终止查询
当发现已经满足查询需求的时候,MySQL总是能够立刻终止查询。一个典型的例子就是当试用了limit 子句的时候。除此之外,MySQL还有几类情况也会提前终止查询,例如发现了一个不成了的条件,只是MySQL可以立刻返回一个空结果。
EXPLAIN SELECT <col> from film where film_id = -1.
从这个例子看到查询在优化阶段就已经终止。除此之外,MySQL在执行过程中,如果发现某些特殊的条件,MySQL都可以使用这类优化。例如:
SELECT film.film_id FROM film left outer join film_actor using(film_id) WHERE film_actor.film_id IS NULL
这个查询会过滤掉所有有演员的电影。每一步电影可能有很多的演员,但是上面的查询一定找到任何一个,就会停止并立刻判断下一部电影,因为只要有一个原因,那么where条件则会过滤掉此类电影。类似的这种‘不同值/不存在’的优化一般可以用于DISTINCT 、NOT EXIST或者LEFT JOIN 类型的查询。
等值传播
如果两个列的值通过灯饰关联,那么MySQL能够把其中一个列的where条件传递到另一个列上,例如:
SELECT film.film_id FROM film INNER JION film_actor USING(film_id) WHERE film.film_id > 500
因为这里使用了film_id 进行了等值关联,MySQL指定这里的where子句不仅适用于film表,而且对于film_actor表同样适用。如果使用的是其他的数据库管理系统,可能还需要手动通过一些条件来告知优化器这个where条件适用于两个表,那么写法效果如下:
。。。WHERE film.film_id > 500 AND film_actor.film_id > 500
在MySQL中这是不需要的,这样写反而会让查询更难维护。
列表in()的比较
在很多数据库系统中in() 完全等同于多个or条件子句,因为这两者完全等价的。在MySQL中这点是不成立的,MySQL将in()列表中的数据先进行排序,然后通过二分查找的方式来确定列表中的值是否满足条件,这是一个O(log n)复杂度的操作,等价的转换成OR查询的复杂度为O(n) 。对于in()列表中有大量取值的时候,MySQL的处理速度将会更快。
上面列举的远不是MySQL优化器的全部,MySQL还会做大量其他的优化。如果说上面的这些讨论中我们应该学到什么,那就是‘不要自以为比优化器更聪明’。最终你可能会占点便宜,但是更有可能会使查询变得复杂而难以维护,而且最终的收益率为0.
当然,虽然优化器已经很智能了,但是有时候也无法给出最优的结果。有时候你可能比油耗更了解数据。
因为服务器层没有任何统计信息,所以MySQL优化器在生成查询的执行计划时需要向存储引擎获取相应的统计信息。存储引擎则提供给游虎丘对于的统计信息,包括:每个表或者索引有多少个页面、每个表的每个索引基数是度搜,数据行和索引长度、索引的分布信息等。游虎丘根据这些信息来选择一个最优的执行计划。