査询优化、索引优化、库表结构优化需要齐头并进,一个不落。
在尝试编写快速的查询之前,需要清楚一点,真正重要是响应时间。如果把查询看作是一个任务,那么他由一系列子任务组成,每个子任务都会消耗一定的时间。如果要优化查询,实际上要优化其子任务,要么消除其中一些子任务,要么减少子任务的执行的次数,要么让子任务运行得更快。
MySQL在执行查询的时候有哪些子任务。哪些子任务运行的速度很慢,这里很难给出完整的列表,通常来说查询的生命周期大致可以按照顺序来看:从客户端,到服务器,然后再服务器上进行解析,生成执行计划,执行,并返回结果给客户端。其中“执行”可以认为是整个生命周期中最重要的阶段,这其中包括了大量为了检索数据到存储引擎的调用以及调用后的数据处理,包括排序、分组等。
在完成这些任务的时候,查询需要在不同的地方花费时间,包括网络,CPU计算,生成统计信息和执行计划、锁等待(互斥等待)等操作,尤其是向底层存储引擎检索数据的调用操作,这些调用需要在内存操作、CPU操作和内存不足时导致的I/O操作上消耗时间,根据引擎不同,可能还会产生大量的上下文切换以及系统调用。
在每一个消耗大量时间的查询案例中,我们都能看到一些不必要的额外操作、某些操作被额外地重复了很多次、某些操作执行得太慢等。优化查询的目的就是减少和消除这些操作所花费的时间。有了这些 概念,我们再一起来看看如何优化査询。
査询性能低下最基本的原因是访问的数据太多。
大部分性能低下的査询都可以通过减少访问的数据量的方式进行 优化。对于低效的査询,我们发现通过下面两个步骤来分析总是很有效:
有些査询会请求超过实际需要的数据,然后这些多余的数据会被应用程序丢弃。这会给MySQL服务器带来额外的负担,并增加网络开销,另外也会消耗应用服务器的CPU 和内存资源。
在EXPLAIN语句中的type列反应了访问的类型。访问类型有很多种,从全表扫描到索引扫描、范围扫描、唯一索引查询、常数引用等。这里列的这些,速度是从慢到快,扫描的行数也是小到大。你不需要记住这些访问类型,但是要明白扫描表,扫描索引,范围访问和单值访问的概念。如果查询没有办法找到合适的访问类型,那么最好的办法通常就是增加一个合适的索引。
MySQL内部每秒能够扫描内存中上百万行数据,相比之下,MySQL响应数据给客户端 就慢得多了。在其他条件都相同的时候,使用尽可能少的査询当然是更好的。但是有时候, 将一个大査询分解为多个小査询是很有必要的。
有时候对于一个大査询我们需要“分而治之”,将大査询切分成小査询,每个査询功能 完全一样,只完成一小部分,每次只返回一小部分査询结果。
删除旧的数据就是一个很好的例子。定期地清除大量数据时,如果用一个大的语句一次 性完成的话,则可能需要一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞 很多小的但重要的査询。将一个大的DELETE语句切分成多个较小的查询可以尽可能小地 影响MySQL性能,同时还可以减少MySQL复制的延迟。
对每一个表进行一次单表查询,然后再应用程序中进行关联,例如
mysql> 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';
可以分解成下面的语句来代替
mysql> SELECT * FROM tag WHERE tag='mysql';
mysql> SELECT * FROM tag_post WHERE tag_id=1234;
mysql> SELECT * FROM post WHERE post.id in (123,456,567,9098,8904);
查询执行路径
步骤:
4.1.1、MySQL客户端和服务器之间的通讯是”双半工“的,这意味着,在任何一个时刻,要么是由服务器向客户端发送数据,要么是由客户端向服务器发送数据,这两个动作不能同时发生。
4.2.2、查询状态:对于一个MySQL连接,或者说一个线程,任何时刻都有一个状态,表示MySQL当前在做什么。我们使用最简单的SHOW FULL PROCESSLIST命令(该命令返回结果中的Command列就表示当前的状态)来查询。下面将这些状态列出来,并做一个简单的解释:
在解析一个查询语句之前,如果查询缓存是打开的,那么MYSQL会优先检查这个查询是否命中查询缓存中的数据。
这个检查是通过一个对大小写敏感的哈希查找的。查询和缓存中的查询即使只有一个不同,也不会匹配缓存结果。如果命中缓存,那么在但会结果前MySQL会检查一次用户权限,有权限则跳过其他步骤直接返回数据
查询的生命周期的下一步是将一个SQL转换成执行计划,MySQL再依照这个执行计划和存储引擎进行交互。
MySQL解析器将使用MySQL语法规则验证和解析查询。例如验证是否使用错误的关键字、关键字顺序、引号前后是否匹配等,预处理器则根据一些MySQL 规则进一步解析树是否合法,例如检查数据表和数据列是否存在,解析名字和别名是否有歧义等
一条查询可以有很多种执行方式,最后都返回相同的结果。优化器的作用就是找到其中最好的执行计划
有很多中原因导致MySQL优化器选择错误的计划,如下所示:
统计信息不准确:MySQL依赖存储引擎提供的统计信息来评估成本,但是有的存储引擎提供的信息偏差有点大,例如InnoDB因为其MVCC的架构,并不能维护一个数据表的行数的精确统计信息
MySQL的查询优化器使用很多策略来生成一个最优的执行计划。
在服务器层有查询优化器,却没有保存数据和索引的统计信息。统计信息由存储引擎实现,不同的存储引擎可能会存储不同的统计信息,有的引擎根本不存储任何统计信息,例如Archive引擎。
因为服务器层没有任何统计信息,所有MySQL查询优化器在生成查询的执行计划时,需要向存储引擎获取相应的统计信息,优化器根据这些信息来选择一个最优的执行计划。
MySQL中“关联”认为任何一个查询都是一次“关联”,并不仅仅是一个查询需要到两个表匹配才叫关联。素以在MySQL中,每一个查询,每一个片段(包括子查询,甚至于单表的SELECT)都可能是关联。
MySQL关联查询的策略很简单:MySQL对任何关联都执行嵌套循环关联操作,即MySQL先在要给表中循环取出单条数据,然后再嵌套循环到下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为止。然后根据各个表的行,返回查询中需要的各个列。
和很多其他关系数据库不同,MySQL并不会生成查询字节码来执行查询。MySQL生成查询的一颗指令树,然后通过存储引擎执行完成这颗树并返回结果
如果优化器给出的并不是最优的关联顺序,这时可以使用STRAIGHT_JOIN关键字重写查询,让优化器按照你认为最优的关联顺序执行——不过老实说,人的判断很难那么精准。绝大多数时候,优化器做出的选择都比普通人的判断更精准。
如果超过N个表的关联,那么需要检查N的阶乘种关联顺序。我们称之为所有可能的执行计划的“搜索空间‘,当搜索空间非常大的时候,优化器选择使用”贪婪“搜索方式查找”最优’的关联顺序。当关联的表超过optimizer_search_depth的限制的时候,就会选择“贪婪”搜索模式了。
在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎则根据这个执行计划来完成整个查询。这里执行计划是一个数据结构,而不是和很多其他的关系型数据库那样会生成对应的字节码。
即使查询不需要返回结果集给客户端,MySQL仍然会返回这个查询的一些信息,如查询影响到的行数。如果查询可以被缓存,那么MySQL在这个阶段也会将结果存放到缓存中。
MySQL将结果集返回客户端是一个增量、逐步返回的过程。开始生成第一条结果时,MySQL就开始向客户端逐步返回结果集了。
1). COUNT()是一个特殊的函数,有两种非常不同的作用:它可以统计某个列值的数量,也可以统计行数。在统计列值的时候要求列值是非空的(不统计NULL)。如果COUNT()的括号中指定了列或者列的表达式,则
统计的就是这个表达式有值的结果数。最简单的就是我们使用count(*)的时候,这种情况下通配符*并不会向我们猜想的那样扩展所有的行,实际上,它会忽略所有的值而直接统计所有的行数。
2). 使用近似值:有时候某些业务场景并不要求完全精确的COUNT值,此时可以用近似值来代替。
3). 更复杂的优化:覆盖索引,增加汇总表等。**
1). 确保ON或者USING子句中的列上有索引。在创建索引的时候就要考虑到关联的顺序。当表A和表B用到列C关联的时候,如果优化器关联顺序是B、A,那就不需要在B表的对应列上建立索引。没有用到的索引只会
2). 确保任何的GROUP BY 和ORDER BY中的表达式只涉及到一个表中的列。这样MySQL才有可能使用索引来优化这个过程。**
关于优化子查询我们给出的最重要的优化建议就是尽可能使用关联查询代替,至少当前MySQL版本需要这样。
1). 它们都可以使用索引来优化,这也是最有效的方法。
2). 在MySQL中,当无法使用索引的时候,GROUP BY使用两种策略来完成:使用临时表或文件排序来做分组。对于任何查询语句,这两种策略的性能都有可以提升的地方。可以通过使用提示SQL_BIG_RESULT和
SQL_SMALL_RESULT来让优化器按你希望的方式运行。
3). 如果需要对关联查询分组(GROUP BY),并且是按照查找表中的某个列进行分组,那么通常采用查找表的标识列分组的效率比其他列更高。**
4). 如果没有通过ORDER BY子句显式地指定排序列,当查询使用GROUP BY 子句的时候,结果集会自动按照分组的列进行排序。如果不关心结果集的顺序,而这中默认排序又导致了需要文件排序,则可以使用
ORDER BY NULL,让MySQL文件不再进行排序。也可以在GROUP BY子句中直接使用DESC或者ASC关键字,使分组的结果集按照需要的方向排序。
5). 优化GROUP BY WITH ROLLUP:分组查询的一个变种思想就是要求MySQL对返回的分组结果再做一次超级聚合。最好的办法尽可能的将WITH ROLLUP 功能转移到应用程序中处理。**
1). 使用索引
2). 要优化这种查询,要么是在页面中限制分页的数量,要么是优化大偏移量的性能。
3). 尽肯能的使用索引覆盖
4). 延迟关联
5). 有时候也可以将LIMIT查询转换为已知位置的查询,让MySQL通过范围扫描找到对应的结果。
6). 其他优化办法还包括使用预先计算的汇总表,或者关联一个冗余表,冗余表只包含主键列和需要做排序的数据列。
分页的时候,另一个常用的技巧是在LIMIT语句中加上SQL_CALC_FOUND_ROWS提示(hint),这样就可以获得去掉LIMIT以满足条件的行数,因此可以作为分页的总数。
用业务的手段解决:下一页,获取更多数据等。
1). MySQL总是通过创建填充临时表的方式来执行UNION查询。因此很多优化策略在UNION查询中都没法很好地使用。经常需要手工地将WHERE,LIMIT,ORDER BY等子句"下推"到UNION的各个子查询中,以
便优化器可以充分利用这些条件进行优化。
2). 除非确实需要服务器消除重复的行,否则就一定要使用UNION ALL,这一点很重要。如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这回导致对临时表做唯一性检查。这样做的代价非常高,
即使有ALL关键字,MySQL仍然会使用临时表存储结果。事实上,MySQL总是经结果放入临时表,然后再读出,再返回给客户端。**
Percona Toolkit中的pt-query-advisor 能够解析查询日志、分析查询模式,然后再给出所有可能存在的潜在问题的查询,并给出足够详细的建议。这像是给MySQL所有的查询做一次全面的健康
检查,它能检测出很多问题。
如果把创建高性能应用程序比作是一个环环相扣的“难题”,除了前面介绍的schema. 索引和査询语句设计之外,査询优化应该是解开“难题”的最后一步了。要想写一个好 的査询,你必须要理解schema设计、索引设计等,反之亦然。
理解査询是如何被执行的以及时间都消耗在哪些地方,这依然是前面我们介绍的响应时 间的一部分。再加上一些诸如解析和优化过程的知识,就可以更进一步地理解上一章讨 论的MySQL如何访问表和索引的内容了。这也从另一个维度帮助读者理解MySQL在 访问表和索引时査询和索引的关系。
参考:
《高性能 MySQL 第三版》