查询性能优化
1. 为什么查询速度会慢?
1). 如果把查询看作是一个任务,那么它由一系列子任务组成,每个子任务都会消耗一定的时间。如果要优化查询,实际上要优化其子任务,要么消除其中一些子任务,要么减少子任务的执行次数,要么让子任务运行的更快。
2). 通常来说,查询的生命周期大致可以按照顺序来看:从客户端,到服务器端,然后在服务器上进行解析,生成执行计划,执行,并返回结果给客户端。其中"执行"可以认为是整个生命周期中最重要的阶段,这其中包括
大量为了检索数据到存储引擎的调用以及调用后的数据处理,包括排序、分组等。
3). 在完成这些任务的时候,查询需要在不同的地方花费时间,包括网络,CPU计算,生成统计信息和执行计划、锁等待(互斥等待)等操作,尤其是向底层存储引擎检索数据的调用操作,这些调用需要在内存中操作、CPU操作
和内存不足时导致的IO操作上消耗时间,根据上下文不同,可能会产生大量的上下文切换以及系统调用。
2. 慢查询基础:优化数据访问
查询性能低下最基本的原因是访问的数据太多。某些查询可能不可避免地需要筛选大量数据,但这并不常见。大部分性能低下的查询都可以通过减少访问的数量的方式进行优化。
对于低效查询,可以通过下面两个步骤来分析:
1). 确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行,但有时候可能是访问了太多的列。
2). 确认MySQL服务器层是否在分析大量超过需要的数据行。
2.1 是否向数据库请求了不需要的数据
1). 一些典型案例
a. 查询不需要的记录:一个常见的错误是常常会误以为MySQL会只返回需要的数据,实际上MySQL却是返回全部结果集在进行计算。最简单有效的解决方法是在这样的查询后面加上LIMIT。
b. 多表关联时返回全部列
c. 总是取出全部列:每次看到SELECT * 的时候都需要用怀疑的眼光审视,是不是真的需要返回全部列?取出全部列会让优化器无法完成索引覆盖扫描这类优化,还会为服务器带来额外的网络、IO、内存和
CPU的消耗。
d. 重复查询相同的数据:比较好的方案是,当初次查询的时候将这个数据缓存起来,需要的时候从缓存中取出,这样性能会更好。
2.2 MySQL是否在扫描额外的记录:
1). 对于MySQL,最简单的衡量查询开销的三个指标如下:
a. 响应时间:响应时间是两部分之和:服务时间和排队时间。服务时间是指数据库处理这个查询真正花多长时间。排队时间是指服务器因为等待某些资源而没有真正执行查询的时间--可能是等IO操作完成,也可能
是等待行锁等等。
b. 扫描的行数和返回的行数:分析查询时,查看该查询扫描的行数是非常有帮助的。这在一定程度上能够说明该查询找到需要的数据的效率高不高。
c. 扫描的行数和访问类型:在评估查询开销的时候,需要考虑一下从表中找到某一行数据的成本。MySQL有好几种访问方式可以查询并返回一行结果。有些方式可能需要扫描很多行才能返回一行结果,也有些访问
方式可能无需扫描就能返回结果。
在EXPALIN语句中的type列反应了访问类型。访问类型有很多种,从全表扫描到索引扫描、范围扫描、唯一索引扫描、常数引用等。这里列的这些,速度是从慢到快,扫描的行数是从多到少。你不要记住这
些访问类型,但需要明白扫描表、扫描索引、范围访问和单值访问的概念。
2). 一般MySQL能使用如下三种方式应用WHERE条件,从好到坏依次为:
a. 在索引中使用WHERE条件来过滤不匹配的记录。这是在存储引擎层完成的。
b. 使用索引覆盖扫描(在Extra列中出现Using index)来返回记录,直接从索引中过滤不需要的记录并返回命中的结果。这是在MySQL服务器层完成的,但无需再回表查询记录。
c. 从数据表中返回数据,然后过滤掉不满足条件的记录(在Extra列中出现Using Where)。这在MySQL服务器层完成,MySQL需要先从数据表读取记录然后过滤。
3). 如果发现查询需要扫描大量的数据但只返回少数的行(使用聚合函数等),那么通常可以尝试下面的技巧去优化它们:
a. 使用索引覆盖扫描,把所有需要用的列都放到索引中,这样存储引擎无需回表获取对应行就可以返回结果了。
b. 改变库表结构。例如使用单独的汇总表。
c. 重写这个复杂的查询,让MySQL优化器能够以更优的方式执行这个查询。
3. 重构查询的方式:有时候,可以将查询转换一种写法让其返回一样的结果,但性能更好。
3.1 一个复杂查询还是多个简单查询
a. 设计查询的时候一个需要考虑的重要问题是,是否需要将一个复杂的查询分成过个简单的查询。在传统实现中,总是强调需要数据库层完成尽可能多的工作,这样做的逻辑在于以前总是认为网络通信、
查询解析和优化是一件代价很高的事情。但是这样的想法对于MySQL并不适用,MySQL从设计上让连接和断开连接都很轻量级,在返回一个小的查询结果方面很高效。现代的网络速度比以前要快的多,
无论是带宽还是延迟。
b. MySQL内部每秒能够扫描内存中上百万行数据,相比之下,MySQL响应数据给客户端就慢得多了。在其他条件都相同的时候,使用尽可能少的查询当然是更好的。但是有时候,将一个大查询分解成
多个小查询也是很有必要的。
3.2 切分查询:删除旧数据是一个很好的例子。定期清除大量数据时,如果用一个大的语句一次性删除完成的话,则可能需要一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但很重要的查询。
同时需要注意,如果每次删除数据后,都暂停一会再做下一次删除,可以经服务器压力分散到很长的时间段中。
3.3 分解关联查询:
分解关联查询的方式重构查询有如下的优势:
a. 让缓存的效率更高。许多应用程序可以方便地使用缓存单表查询对应的结果集。
b. 将查询分解后,执行单个查询可以减少锁的竞争。
c. 在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展性。
d. 查询本身效率也可能会有所提升。
e. 可以减少冗余记录的查询。管理查询中可能需要重复地访问一部分数据。
f. 更进一步,这样做相当于在应用中实现了哈希关联,而不是使用MySQL的嵌套循环关联。某些场景哈希关联的效率要高很多。
4. 查询执行的基础:
查询执行的过程:
1). 客户单发送一条查询给服务器
2). 服务器检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段。
3). 服务器端进行SQL解析、预处理,再由优化器生成对应的执行计划。
4). MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询。
5). 将结果返回给客户端。
4.1 MySQL客户端/服务器通信协议:MySQL客户端和服务器之间的通信协议是"半双工"的,这意味着,在任何一个时刻,要么是由服务器向客户端发送数据,要么是由客户端向服务器发送数据,这两个动作不能
同时发生,所以我们也无法将一个消息切换成小块独立来发送。
1). MySQL通常要等待所有的数据都已经发送给客户端才能释放这条查询所占的资源,所以接收全部结果并缓存通常可以减少服务器的压力,让查询能够早点结束、早点释放相应的资源。
2). 查询状态:对于一个MySQL连接,或者说一个线程,任何时刻都有一个状态,该状态表示了MySQL当前正在做什么。有很多种方式能查看当前的状态,最简单的是使用SHOW FULL PROCESSLIST命令(
该命令返回结果中的Command列就表示当前的状态)。下面将这些状态列出来,并做一个简单的解释:
a. Sleep:线程正在等待客户端发送新的请求。
b. Query:线程正在执行查询或者正在将结果发送给客户端。
c. Locked:在MySQL服务器层,该线程正在等待表锁。
d. Analyzing and statistics : 线程正在收集存储引擎的统计信息,并生成查询的执行计划。
e. Coping to tmp table [on disk]:线程正在执行查询,并且将其结果都复制到一个临时表中,这种状态一般要么是在做GROUP BY操作,要么是文件排序操作,或者是UNION操作。如果这个状态后面
还有"on disk"标记,那表示MySQL正在讲一个内存临时表放到磁盘上。
f. Sorting result:线程正在对结果集进行排序。
g. Sending data:这表示多种情况:线程可能在对多个状态之间传输数据,或者而在生成结果集,或者在向客户端返回数据。
4.2 查询缓存:在解析一个查询语句之前,如果查询缓存时打开的,那么MySQL会优先检查这个查询是否命中查询缓存中的数据。这个检查是通过一个大小写敏感的哈希查找实现的。查询和缓存中的查询即使只有
一个字节不同,那也不会匹配缓存结果,这种情况下查询会进入下一个阶段处理。
4.3 查询优化处理:这个阶段包括多个子阶段:解析SQL、预处理、优化SQL查询计划。这个过程中任何错误(例如语法错误)都可能终止。
a. 语法解析器和预处理:MySQL通过关键字将SQL语句进行解析,并生成一颗对应的"解析树",MySQL解析器将使用MySQL语法规则验证和解析查询。预处理则根据一些MySQL规则进一步检查解析树是否合法。
b. 查询优化器:一条查询语可以有很多执行方式,最后都返回相同的结果。优化器的作用是找到这其中最好的执行计划。MySQL使用基于成本的优化器,它将尝试预测一个查询使用某种执行时的成本,并选择其中
成本最小的一个。
1). 有很多种原因会导致MySQL优化器选择错误的执行计划,如下所示:
a. 统计信息不准确。MySQL依赖存储引擎提供的统计信息来评估成本,但是有的存储引擎提供的信息时不准确的,有的偏差可能非常大。例如:InnoDB因为其MVCC的机构,并不维护一个数据表的行数的精确信息。
b. 执行计划中的成本估算不等同于实际执行的成本。
c. MySQL的最优可能和你想的最优不一样。你可能希望执行时间尽可能的短,但是MySQL只是基于其成本模型选择最优的执行计划,而有些时候付这并不是最快的。
d. MySQL从不考虑其他并发执行的查询,这可能会影响到当前的查询速度。
f. MySQL也并不是任何时候都是基于成本的优化。
g. MySQL不会考虑不受其控制的操作成本,例如执行存储过程或者用户自定义函数的成本。
h. 优化器有时候无法去估算所有可能的执行计划,所以他可能错估实际上最优的执行计划。
2). 优化策略可以简单的分为两种:一种静态优化,一种动态优化。
a. 静态优化可以直接对解析树进行分析,并完成优化。静态优化在第一次完成后就一直有效,即使使用不同的参数重复执行也不会发生变化。可以认为这是一种"编译时优化"。
b. 动态优化则和查询的上下文有关,也可能和很多其他因素有关。例如WHERE条件中的取值、索引中条目对应的数据行数等。需要在每次查询的时候重新评估,可以认为是一种"运行时优化"。
c. MySQL对查询的静态优化只需要做一次,但对查询的动态优化则在每次执行时都需要重新评估。有时候甚至在查询的执行过程中也会重新优化。
3). 下面是一些MySQL能够处理的优化过程:
a. 重新定义关联表的顺序:
b. 将外连接转换为内连接
c. 使用等价变化规则:MySQL可以使用一些等价变化来简化并规范表达式。它可以合并和减少一些比较,还可以移除一些恒成立和一些恒不成立的判断。例如:(5=5 ADN a>5) --> a>5。
d. 优化COUNT(),MIN()和MAX():索引和列是否可为空通常可以帮助MySQL优化这类表达式。例如:要找到某一列的最小值,只需要查询对应B-Tree索引最左端的记录,MySQL可以直接获取索引的最小行。
e. 预估并转化为常数表达式:当MySQL检测到一个表达式可以转化为常数的时候,就会一直把该表达式作为常数进行优化处理。
f. 覆盖索引扫描
g. 子查询优化
h. 提前终止查询:在发现已满足查询需求的时候,MySQL总是能够立刻终止查询。一个典型的例子是使用LIMIT。
i. 等值传播:USING(film_id)
j. 列表IN()的比较:在很多数据系统中,IN()完全等同于多个OR条件的子句,因为这两者是完全等价的。在MySQL中这点是不成立的,MySQL将IN()列表中的数据先进行排序,然后通过二分查找的方式来确定
列表中的值是否满足条件,这是一个O(log n)复杂度的操作,等价地转换成OR查询的复杂度为O(n),对于IN()列表中有大量取值的时候,MySQL的处理速度将会更快。
4). 数据和索引的统计信息:因为服务器层没有任何统计信息,所以MySQL查询优化器在生成查询的执行计划时,需要向存储引擎获取相应的统计信息。
5). MySQL如何执行关联查询:当前MySQL关联执行的策略很简单:MySQL对任何关联都执行嵌套循环操作,即MySQL先在一个表中循环取出单条数据,然后再嵌套循环到下一个表中寻找匹配的行,依次下去,直
到所有表中匹配的行为止。然后根据各个表匹配的行,返回查询中需要的各个列。MySQL会尝试在最后一个关联表中找到所有匹配的行,如果最后关联表无法找到更多的行以后,MySQL返回到上一层次关联表,
看是否能够找到更多匹配记录,以此类推迭代执行。
6). 关联查询优化器:MySQL优化器最重要的一部分就是关联查询优化,它决定了多个表关联时顺序。通常多表关联的时候,可以有多种不同的关联顺序来获得相同的执行结果。关联查询优化器则通过评估不同的顺序
时的成本来选择一个代价最小的关联顺序。优化器会将数据量少的表先进行查询(个人认为第一个查询的表越小,临时表就越小)。
7). 排序优化:无论如何排序都是一个成本很高的操作,所以从性能角度考虑,应尽可能避免排序或者尽可能避免对大量数据进行排序。尽量通过索引进行排序。当不能使用索引生成排序结果的时候,MySQL需要自己
进行排序,如果数据量小则在内存中进行,如果数量大则需要使用磁盘,不过MySQL将这个过程统一称为文件排序,即使完全是内存排序不需要任何磁盘文件时也是如此。
MySQL有如下两种排序算法:
a. 两次传输排序(旧版本使用):读取行指针和需要排序的字段,对其进行排序,然后再根据排序结果读取所需要的数据行。需要进行两次传输,即需要从数据表中读取两次数据,第二次读取数据的时候,因为是读
取排序列进行排序后的所有记录。这回产生大量的随机IO。
b. 单次传输排序(新版本使用):先读取查询所需要的所有列,然后在根据给定列进行排序,最后直接返回排序结果。效率更高,但占用内存更大。
如果查询中有LIMIT的话,LIMIT也会在排序之后应用的,所以即使需要返回较少的数据,临时表和需要排序的数据量仍然后非常大。貌似5.6版本有所改进,会先抛弃不满足条件的记录,然后再进行排序。
4.4 查询执行引擎:在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎则根据这个执行计划来完成整个查询。这里执行计划是一个数据结构,而不是和很多其他的关系型数据库那样会
生成对应的字节码。
4.5 返回结果给客户端:MySQL将结果集返回客户端是一个增量、逐步返回的过程。开始生成第一条结果时,MySQL就开始向客户端逐步返回结果集了。
5. MySQL查询优化器的局限性:
5.1 关联子查询:MySQL的子查询实心非常糟糕(5.6版本以后有改进)。最糟糕的一类查询是WHERE条件中包含IN()的子查询语句。
1). 因为使用IN()加子查询,性能经常会非常糟,所以通常建议使用EXISTS()等效的改写查询来获取更好的效率。
2). 一般建议使用左外连接(LEFT OUTER JOIN)代替子查询。
5.2 UNION的限制:MySQL无法将限制条件从外层"下推"到内层,这使得原本能够限制部分返回结果的条件无法应用到内层查询的优化上。
例如如果希望UNION的各个子句能够根据LIMIT只去部分结果集,或者希望能够先排好序再合并结果集的话,就需要在UNION的各个子句中分别使用这些语句。
(SELECT first_name,last_name FROM sakila.actor ORDER BY last_name) UNION ALL (SELECT first_name ,last_name FROM sakila.customer ORDER BY last_name) LIMIT 20;
优化后:
(SELECT first_name,last_name FROM sakila.actor ORDER BY last_name LIMIT 20) UNION ALL (SELECT first_name ,last_name FROM sakila.customer ORDER BY last_name LIMIT 20) LIMIT 20;
5.3 当WHERE子句包含多个复杂条件的时候,MySQL能够访问单个表的多个索引以合并和交叉过滤的方式来定位需要查找的行。
5.4 等值查询:某些时候,等值查询会带来一些意想不到额外消耗。例如:有一个非常大的IN()列表,而MySQ优化器发现存在WHERE、ON或者USING的子句。
5.5 并行执行:MySQL无法利用多核特性来并行执行查询(貌似5.6以后有改进)。
5.6 哈希关联:MySQL不支持哈希关联。
5.7 松散索引扫描:MySQL并不支持松散索引扫描,也就无法按照不连续的方式扫描一个索引。通常,MySQL的索引扫描需要先定义一个起点和终点,即使需要的数据只是这段索引中的很少几个,MySQL仍需扫描这段索引中
的每一个条目。
5.8 最大值和最小值优化:对于MIN()和MAX()查询,MySQL的优化做的并不好。例如:
SELECT MIN(actor_id) FROM sakila.actor WHERE first_name='PENELOPE'
因为first_name上没有索引,所以会进行全表扫描。如果MySQL能够进行主键扫描,那么理论上,当MySQL读到第一个满足条件的记录的时候,就是我们需要的最小值,因为主键是严格按照actor_id大小字段排序的。
一个曲线优化的办法是移除MIN(),然后使用LIMIT来将查询重写。
5.9 在同一个表上查询和更新:MySQL不允许对同一张表同时进行查询和更新。
6. 查询优化器的提示(hint):如果对优化器选择的执行计划不满意,可以使用优化器提供的几个提示(hint)来控制最终的执行计划。
7. 优化特定类型的查询
7.1 优化COUNT()查询
1). COUNT()是一个特殊的函数,有两种非常不同的作用:它可以统计某个列值的数量,也可以统计行数。在统计列值的时候要求列值是非空的(不统计NULL)。如果COUNT()的括号中指定了列或者列的表达式,则
统计的就是这个表达式有值的结果数。最简单的就是我们使用count(*)的时候,这种情况下通配符*并不会向我们猜想的那样扩展所有的行,实际上,它会忽略所有的值而直接统计所有的行数。
2). 使用近似值:有时候某些业务场景并不要求完全精确的COUNT值,此时可以用近似值来代替。
3). 更复杂的优化:覆盖索引,增加汇总表等。
7.2 优化关联查询:
1). 确保ON或者USING子句中的列上有索引。在创建索引的时候就要考虑到关联的顺序。当表A和表B用到列C关联的时候,如果优化器关联顺序是B、A,那就不需要在B表的对应列上建立索引。没有用到的索引只会
带来额外的负担。一般来说,除非有其他理由,否则只需要在关联顺序中的第二个表的相应列上创建索引。
2). 确保任何的GROUP BY 和ORDER BY中的表达式只涉及到一个表中的列。这样MySQL才有可能使用索引来优化这个过程。
7.3 优化子查询:关于优化子查询我们给出的最重要的优化建议就是尽可能使用关联查询代替,至少当前MySQL版本需要这样。
7.4 优化GROUP BY和DISTINCT:
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 功能转移到应用程序中处理。
7.5 优化LIMIT分页:
1). 使用索引
2). 要优化这种查询,要么是在页面中限制分页的数量,要么是优化大偏移量的性能。
3). 尽肯能的使用索引覆盖
4). 延迟关联
5). 有时候也可以将LIMIT查询转换为已知位置的查询,让MySQL通过范围扫描找到对应的结果。
6). 其他优化办法还包括使用预先计算的汇总表,或者关联一个冗余表,冗余表只包含主键列和需要做排序的数据列。
7.6 优化SQL_CALC_FOUND_ROWS:分页的时候,另一个常用的技巧是在LIMIT语句中加上SQL_CALC_FOUND_ROWS提示(hint),这样就可以获得去掉LIMIT以满足条件的行数,因此可以作为分页的总数。
用业务的手段解决:下一页,获取更多数据等。
7.7 优化UNION查询:
1). MySQL总是通过创建填充临时表的方式来执行UNION查询。因此很多优化策略在UNION查询中都没法很好地使用。经常需要手工地将WHERE,LIMIT,ORDER BY等子句"下推"到UNION的各个子查询中,以
便优化器可以充分利用这些条件进行优化。
2). 除非确实需要服务器消除重复的行,否则就一定要使用UNION ALL,这一点很重要。如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这回导致对临时表做唯一性检查。这样做的代价非常高,
即使有ALL关键字,MySQL仍然会使用临时表存储结果。事实上,MySQL总是经结果放入临时表,然后再读出,再返回给客户端。
7.8 静态查询分析:Percona Toolkit中的pt-query-advisor 能够解析查询日志、分析查询模式,然后再给出所有可能存在的潜在问题的查询,并给出足够详细的建议。这像是给MySQL所有的查询做一次全面的健康
检查,它能检测出很多问题。
7.9 用户自定义变量:
8. 一般,我们要尽量避免使用SELECT_FOR_UPDATE。不光是队列表,任何情况下都要尽量避免。
9. 需要处理一种特殊的情况:那些正在被进程处理,而进程本身却由于某种原因退出的情况。这种情况处理起来很简单。你只需要定期运行UPDATE语句将它都更新成原始状态就可以了,然后执行SHOW PROCESSLIST,
获得当前正在工作的线程ID,并使用一些WHERE条件避免取到那些刚开始处理的进程。