MySQL系列-- 4. 查询性能优化

4. 查询性能优化

4.1 为何查询速度会变慢

  • 快速查询取决于响应时间
  • 若是把查询当作是一个任务,那么它由一系列子任务组成,每一个子任务都会消耗必定的时间。若是要优化查询,实际上要优化其子任务,要么消除其中一些子任务,要么减小子任务的执行次数,要么让子任务运行得更快(有时候须要修改一些查询,减小这些查询对系统中运行的其余查询的影响,这时候是在减小一个查询的资源消耗)。
  • 查询的生命周期:
    • 从客户端
    • 到服务器
    • 在服务器上:
      • 解析
      • 生成执行计划
      • 执行:最重要的阶段。包括了大量为了检索数据到存储引擎的调用及调用后的数据处理,包括排序,分组等。
    • 返回结果给客户端
  • 完成这些任务,须要在不一样的地方花费时间。包括网络,CPU计算,生成统计信息和执行计划、锁等待(互斥等待)等操做,尤为是向底层存储引擎检索数据的调用操做,这些调用须要在内存操做、CPU操做和内存不足时致使的IO操做上消耗的时间。根据存储引擎的不一样,可能还会产生大量的上下文切换以及系统调用。

4.2 慢查询基础:优化数据访问:

查询性能低下最根本的缘由是访问的数据太多。某些查询可能不可避免地须要筛选大量数据,但这并不常见。大部分性能低下的查询均可以经过减小访问的数据量进行优化。对于低效的查询,能够经过下面两个步骤分析:mysql

  • 确认应用程序是否在检索大量超过须要的数据。这一般意味着访问了太多的行,但有时候也多是访问了太多的列。
  • 确认MySQL服务器层是否在分析大量超过须要的数据行。

4.2.1 是否向数据库请求了不须要的数据

有些查询会请求超过实际须要的数据,而后这些多余的数据会被应用程序丢弃。这会给MySQL服务器带来额外的负担,并增长网络开销,另外也会消耗应用服务器的CPU和内存资源。算法

典型案例:sql

  • 查询不须要的记录:一个常见错误是误觉得MySQL只会返回须要的数据,实际上MySQL确实先返回所有结果集再进行计算。最简单有效的办法是在这样的查询后面加上LIMIT。
  • 多表关联时返回所有列:正确作法是只取须要的列
  • 老是取出所有的列:
    • 取出所有的列,会让优化其没法完成覆盖扫描这类优化,还会给服务器带来额外的资源消耗。要慎重。
    • 可是这能够简化开发,提供相同代码片断的复用性,或者应用程序使用了某种缓存机制等其余有必要取出所有列的因素。若是清楚这么作的性能影响,也是能够考虑的。
  • 重复查询相同的数据:建议初次查询的时候将这个数据缓存起来,须要的时候从缓存中取出。

4.2.2 MySQL是否在扫描额外的记录

对于MySQL,最简单的衡量查询开销的三个指标:响应时间、扫描的行数和返回的行数。没有哪一个指标可以完美地衡量查询的开销,但它们大体反映了MySQL在内部执行查询时须要访问多少数据,并能够大概推算出查询运行的时间。这三个指标都会记录到MySQL的慢日志中,检查慢日志记录是找出扫描行数过多的查询的好办法。数据库

  • 响应时间:
    • 响应时间=服务时间+排队时间。实际上没办法细分,目前尚未办法测量。
      • 服务时间:数据库处理这个查询真正花的时间。
      • 排队时间:服务器由于等待某些资源而没有真正执行查询的时间。
    • 看到一个查询的响应时间的时候,要评估是否合理。归纳地说,了解这个查询须要那些索引以及它的执行计划是什么,而后计算大概须要多少个顺序和随机IO,在用其乘以在具体硬件条件下一次IO消耗的时间,最后把这些消耗都加起来获得一个参考值。
  • 扫描的行数和返回的行数
    • 分析查询时,查看该查询扫描的行数是很是有帮助的。在必定程度上可以说明该查询找到须要的数据效率高不高。
    • 不过这个指标还不够完美,由于并非全部的行的访问代价都是同样的。
    • 理想状况下扫描的行数和返回的行数应该是相同的。但实际状况下不多存在,好比关联查询。通常扫描的行数与返回的行数比率一般在1:1和10:1之间。
  • 扫描的行数和访问类型
    • 在评估查询开销的时候,须要考虑从表中找到某一行数据的成本。
    • 访问类型(EXPLAIN语句中的type列,row列显示扫描的行数):
      • ref
      • ALL(全表扫描)
    • MySQL应用WHERE条件的方式:
      • 在索引中使用WHERE条件来过滤不匹配的记录,这是在存储引擎完成的。
      • 使用覆盖索引扫描(在EXTRA 列中出现了Using index)来返回记录,直接从索引中过滤不须要的记录并返回命中结果。这是在MySQL服务器完成的,但无须再回表查询
      • 从数据表中返回数据,而后过滤不知足条件的记录(在Extra列中出现Using Where)。这在MySQL服务器层完成,MySQL须要先从数据表读出记录而后过滤。
    • 优化技巧:
      • 使用索引覆盖扫描,把全部须要的列都放到索引中,这样存储引擎无须回表获取对应的行就能够返回结果。
      • 改变库表结构。例如使用单独的汇总表
      • 重写这个复杂的查询,让MySQL优化器可以以更优化的方式执行这个查询。

4.3 重构查询方式

4.3.1 一个复杂的查询仍是多个简单的查询

  • MySQL从设计上让链接和断开链接都很轻量级,在返回一个小的查询结果方面很高效。
  • MySQL内部每秒可以扫描内存中上百万行数据,相比之下,MySQL响应数据给客户端就慢得多了。
  • 使用尽量少的查询,但某些场景下将一个大查询分解为多个小查询是颇有必要的。

4.3.2 切分查询

  • 有时候须要对一个大查询切分红小查询,每一个查询功能彻底同样,只完成一小部分,每次只返回一小部分查询结果。
  • 例如删除旧的数据,切分该查询能够尽量小地影响性能,还能够减小MySQL复制的延迟。一次删除一万行数据通常来讲是一个比较高效并且对服务器影响也最小的作法,若是是事务型引擎,不少时候小事务可以更高效。另外,每次删除后都暂停一会,可以将一次性的压力分散到一个很长的时间段中,就能够大大下降对服务器的影响,还能够大大减小删除时锁的持有时间。

4.3.3 分解关联查询

不少高性能的应用都会对关联查询进行分解。简单地,能够对每个表进行一次单表查询,而后将结果在应用程序中进行管理缓存

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 whre post.id in (123, 456);复制代码
  • 优点:
    • 让缓存效率更高。许多应用程序能够方便地缓存单表查询时对应的结果对象,这样能够减小查询时的条件。而对MySQL的查询缓存来讲,若是关联中的某个表发生了变化,那么久没法使用查询缓存了,而拆分后,若是某个表不多改变,那么基于该表的查询就能够重复利用查询缓存结果。
    • 执行单个查询能够减小锁的竞争。
    • 在应用层作关联,能够更容易对数据库进行拆分,更容易作到高性能和可扩展。
    • 查询自己效率也可能会有所提高。例如使用IN()代替关联查询,可让MySQL按照ID顺序进行查询,这可能比随机的关联要更高效。
    • 能够减小冗余记录的查询。在应用层作关联查询,意味着对于某条记录应用只须要查询一次,而在数据库中作关联查询,则可能须要重复地访问一部分数据。从这点看,这样的重构还可能会减小网络和内存的消耗。
    • 更进一步,这样作至关于在应用中实现了哈希关联,而不是使用MySQL的嵌套循环关联。某些场景哈希关联的效率要高得多。
  • 经过重构查询将关联放到应用程序中更加高效的场景:
    • 当应用可以方便地缓存当个查询的结果
    • 当能够将数据分布到不一样的MySQL服务器上
    • 当可以使用IN()的方式来代替关联查询
    • 当查询中使用同一个数据表的时候

4.4 查询执行的基础

当向MySQL发送一个请求的时候,MySQL的工做流程:性能优化

查询执行路径
查询执行路径

  • 客户端发送一条查询给服务器
  • 服务器先检查查询缓存,若是命中了缓存,则马上返回存储在缓存中的结果。不然进入下一阶段
  • 服务器进行SQL解析、预处理,再有优化器生成对应的执行计划。
  • MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询
  • 将结果返回给客户端

4.4.1 MySQL客户端/服务器通讯协议

  • 客户端与服务器之间的通讯协议是”半双工“的,这意味着在任何一个时刻,只能有客户端或服务器的其中一个发送数据。
    • 限制:
      • 没办法进行流量控制,一旦一端开始发送消息,另外一端要接收完整消息才能响应它。
    • 客户端用一个单独的数据包将查询传给服务器。若是查询太大,服务器会根据配置max_allowed_packet拒绝更多数据并抛出相应错误。
    • 服务器响应给用户的数据一般更多,由多个数据包组成。当服务器开始响应客户端请求时,客户端必须完整地接收整个返回结果。若是只取前面几条结果,或者接收几条结果后就直接断开链接,建议在查询中加上LIMIT限制。
    • 客户端多数链接MySQL的库函数(如Java,Python)均可以获取所有结果集并缓存到内存中,还能够逐行获取须要的数据。默认通常是得到所有结果集并缓存到内存中。
      • MySQL一般须要等到全部的数据都已经发送给客户端才能释放这条查询所占用的资源,因此接收所有结果并缓存经过能够减小服务器的压力,让查询早点结束而释放相应资源。
      • 当使用多数链接MySQL的库函数从MySQL获取数据时,其结果看起来都像是从MySQL服务器获取数据,而实际上都是从这个库函数的缓存获取数据。可是当返回一个很大的结果集时,库函数会很不少时间和内存来存储全部的结果集,若是可以尽早开始处理,就能大大减小内存消耗,这种状况下能够不使用缓存来记录结果而是直接处理。这样作的缺点是,对服务器来讲,须要等到查询完成后才能释放资源,所以服务器的资源都是被这个查询占用。
    • 查询状态,对一个MySQL链接或者说一个线程,任什么时候候都有一个状态。最简单办法使用SHOW FULL PROCESSLIST命令查看:
      • SLEEP:线程正在等待客户端发送新的请求
      • QUERY:线程正在执行查询或者正在将查询结果发送给客户端
      • LOCKED:在MySQL服务器层,该线程正在等待表锁。在存储引擎级别实现的锁,例如InnoDB的行锁,并不会体如今线程状态中。对于MyISAM来讲这是一个比较典型的状态,在其余没有行锁的引擎中也常常会出现。
      • Analyzing and statistics:线程正在收集存储引擎的统计信息,并生成查询的执行计划
      • Copying to tmp table [on disk]:线程正在执行操做,而且将其结果集都复制到一个临时表中,这种状态通常要么是在作GROUP BY操做,要么是文件排序操做,或者是UNION操做。若是这个状态后面还有”on disk“标记,那标识MySQL正在将一个临时内存表放到磁盘上。
      • Sorting result:线程正在对结果集进行排序。
      • Sending data:线程可能在多个状态间传送数据,或者在生成结果集,或者在向客户端返回数据。

4.4.2 查询缓存

  • 在解析一个查询语句以前,若是查询缓存是打开的,那么MySQL会优先检查这个查询是否命中查询缓存的数据。
    • 这个检查是经过一个大小写敏感的哈希查找来实现的。
    • 查询与缓存中的查询即便只有一个字节不一样,那也不会匹配缓存结果,这种状况下查询就会进入下一阶段的处理。
    • 若是当前的查询刚好命中了查询缓存,那么在返回查询结果以前MySQL会检查一次用户权限。这也不须要解析查询SQL语句的,由于在查询缓存中已经存放了当前查询须要访问的表信息。若是权限没有问题,MySQL会跳过全部其它的阶段,直接从缓存中拿到结果并返回给客户端。

4.4.3 查询优化处理

查询的生命周期的下一步是将一个SQL转换成一个执行计划,MySQL再依照这个执行计划和存储引擎进行交互,这包括多个子阶段:解析SQL、预处理、优化SQL执行计划。这个过程当中的任何错误(例如语法错误)均可能终止查询,另外在实际执行中,这几部分可能一块儿执行也可能单独执行。服务器

  • 语法解析器和预处理:网络

    • 流程:
      • MySQL经过关键字将SQL进行解析,并生成一颗对应的”解析树“。MySQL解析器将使用MySQL语法规则验证和解析查询。例如:验证是否使用错误的关键词,或者使用关键词的顺序是否正确等,再或者它还会验证引号是否能先后正确匹配。
      • 预处理器则根据一些MySQL规则进一步检查解析树是否合法,例如,这里将检查数据表和数据列是否存在,还会解析名字和别名,看看它们是否有歧义。
      • 预处理器会验证权限。这一般很快,除非服务器上有很是多的权限配置。
  • 查询优化器:数据结构

    • 通过语法解析器和预处理后,语法树被认为是合法的,并将由优化器将其转化成执行计划。架构

      • 一条查询能够有不少执行方式,最后都返回相同的结果。
      • 优化器的做用就是找到这其中最好的执行计划。
    • MySQL使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。

      • 最初,成本的最小单位是随机读取一个4K数据页的成本,后来成本计算公式变得更加复杂,而且引入了一些”因子“来估算这些操做的代价,好比当执行一次WHERE条件比较的成本。
      • 能够经过SHOW STATUS LIKE 'Last_query_cost';来查询当前会话的当前查询的成本,其值N为MySQL的优化器认为大概须要作N个数据页的随机查找才能完成当前的查询。
      • Last_query_cost根据一系列统计信息计算得来:每一个表或索引的页面个数、索引的基数(索引中不一样值的数量)、索引和数据行的长度、索引的分布状况。优化器在评估成本的时候并不考虑任何层面的缓存,它假设读取任何数据都须要一次磁盘IO。
    • 致使MySQL选择错误的执行计划的缘由:

      • 统计信息不许确。MySQL依赖存储引擎提供的统计信息来评估成本,可是有的存储引擎提供的信息可能误差很是大。例如,InnoDB由于其MVVC的架构,并不能维护一个数据表的行数的精确统计信息。
      • 执行计划中的成本估算不等同于实际执行的成本。因此即便统计信息精确,优化器给出的执行计划也可能不是最优的。例若有时候某个执行计划虽然须要读取更多的页面,可是它的成本却更小,由于这些页面可能都是顺序读或者这些页面已经在内存中,它的访问成本将很小。
      • MySQL的最优可能和你想的最优不同。你可能但愿执行时间尽量短,可是MySQL只是基于其成本模型选择最优的执行计划,而有些时候这并非最快的执行方式。因此,这里咱们看到根据执行成原本选择执行计划并非完美的模型。
      • MySQL从不考虑其余并发执行的查询,这可能会影响到当前查询的速度。
      • MySQL也并非任什么时候候都是基于成本的优化。有时也会基于一些固定的规则,例如,若是存在全文搜索的MATCH()子句,则在存在全文索引的时候就使用全文索引,即便有时候使用别的索引和WHERE条件能够远比这种方式要快,MySQL也仍然会使用对应的全文索引。
      • MySQL不会考虑不受其控制的操做成本,例如执行存储过程或者用户自定义函数的成本。
      • 优化器有时候没法去估算全部可能的执行计划,因此它可能错过实际上最优的执行计划。
    • 优化策略:

      • 静态优化:直接对解析树进行分析,并完成优化。例如,优化器能够经过一些简单的代数变换将WHERE条件转换成另外一种等价形式。静态优化不依赖于特别的数值,如WHERE条件中带入的一些常数等。静态优化在第一次完成后就一直有效,即便使用不一样的参数重复执行查询也不会发生变化。能够认为这是一种”编译时优化“。
      • 动态优化:与上下文有关,也可能和其余不少因素有关,例如WHER条件中的取值,索引中条目对应的数据行数等。这须要在每次查询的执行时候都从新评估,甚至在其执行过程当中也会从新优化,能够认为这是”运行时优化“。
    • MySQL可以处理的优化类型:

      • 从新定义关联表的顺序:数据表的关联并不老是按照在查询中指定的顺序执行。决定关联的顺序是优化器很重要的一部分功能。

      • 将外链接转换为内链接:并非全部的OUTER JOIN语句都必须之外链接的方式执行。例如WHERE条件,库表结构均可能会让外链接等价于一个内链接。

      • 使用等价变换规则:MySQL使用一些等价变换来简化并规范表达式。它能够合并和减小一些比较,还能够移除一些恒成立和一些恒不成立的判断。

      • 优化COUNT()、MIN()和MAX():索引和列是否可为空能够帮助MySQL优化这类表达式。例如,要找到某一列的最小值,只须要查询B-Tree索引最左端的记录,MySQL能够直接获取,并在优化器生成执行计划的时候就能够利用这一点(优化器会将这个表达式做为一个常数对待,在EXPLAIN就能够看到"Select tables optimized away")。相似的,没有任何WHERE条件的COUNT(*)查询一般也可使用存储引擎提供的一些优化(MyISAM维护了一个变量来存放数据表的行数)

      • 预估并转换为常数表达式:MySQL检测到一个表达式能够转换为常数的时候,就会一直把该表达式做为常数进行优化处理。例如:一个用户自定义变量在查询中没有发生变化、数学表达式、某些特定的查询(在索引列上执行MIN,甚至是主键或惟一键查找语句)、经过等式将常数值从一个表传到另外一个表(经过WHERE、USING或ON来限制某列取值为常数)。

      • 覆盖索引扫描:当索引中的列包含全部查询中全部须要的列的时候,MySQL就可使用索引返回须要的数据,而无须查询对应的数据行。

      • 子查询优化:在某些状况下能够将子查询转换成一种效率更高的形式,从而减小多个查询屡次对数据的访问。

      • 提早终止查询:当发现已经知足查询的需求,可以马上终止查询。例如使用了LIMIT子句,或者发现一个不成立的条件(当即返回一个空结果)。当存储引擎须要检索”不一样取值“或者判断存在性的时候,例如DISTINCT,NOT EXIST()或者LEFT JOIN类型的查询,MySQL都会使用这类优化。

      • 等值传播:若是两个列的值经过等式关联,那么就能够把其中一个列的WHERE条件传递到另外一个列上。

        SELECT film.film_id
        FROM sakila.film
            INNER JOIN sakila.film_actor USING(film_id)
        WHERE film.file_id > 500;
        -- 若是手动经过一些条件来告知优化器这个WHERE条件适用于两个表,在MySQL中反而让查询更难维护。
        ... WHERE film.file_id > 500 AND film_actor.film_id > 500;复制代码
      • 列表IN()的比较:不一样于其它数据库IN()彻底等价于多个OR条件语句,MySQL将IN()列表中的数据先进行排序,而后经过二分查找的方式来肯定列表的值是否知足条件,前者查询复杂度为O(n),后者为O(log n)。对于有大量取值的状况,MySQL这种处理速度会更快。

  • 数据和索引的统计信息:

    • MySQL架构在服务器层有查询优化器,但却没有保存数据和索引的统计信息。由于统计信息由存储引擎实现,不一样的存储引擎可能会存储不一样的统计信息。
    • MySQL查询优化器在生成查询的执行计划时,须要向存储引擎获取相应的统计信息。存储引擎则提供给优化器对应的统计信息,包括:每一个表或索引有多少个页面、每一个表的每一个索引的基数是多少、数据行和索引的长度、索引的分布信息等。
  • MySQL如何执行关联查询:

    • MySQL认为任何一个查询都是管理,而不局限于须要两个表的匹配,包括每个查询,每个片断(例如子查询,甚至基于单表的SELECT)
      • UNION查询的例子:MySQL先将一系列的单个查询结果放到一个临时表中,而后再从新读出临时表数据来完成UNION查询。该读取临时表结果的操做也是关联。
    • MySQL关联执行的策略:对任何关联都执行嵌套循环关联操做,即MySQL先在一个表中循环取出单条数据,而后再嵌套循环到下一个表中寻找新配的行,依次下去,直到找到全部表中匹配的行为止。而后根据各个表匹配的行,返回查询中须要的各个列。MySQL会尝试在最后一个关联表中找到全部匹配的行,若是最后一个关联表没法找到更多的行之后,MySQL返回到上一层关联表,看是否可以找到更多的匹配记录,依此类推迭代执行。
      • 从本质上来讲,全部类型的查询都以一样的方式运行。例如,MySQL在FROM子句中遇到子查询时,先执行子查询并将其结果放到一个临时表(MySQL的临时表是没有任何索引的,UNION查询也同样),而后将这个临时表看成一个普通表对待。简而言之,全部的查询类型都转换成相似的执行计划(在MySQL5.6和MariaDB有重大改变,两个版本都引入更加复杂的执行计划)
      • 不过不是全部的查询均可以转换。例如,全外链接就没法经过嵌套循环和回溯的方式完成,这是当发现关联表中没有找到任何匹配行的时候,则多是由于关联是刚好从一个没有任何匹配的表开始。这大概也是MySQL并不支持全外链接的缘由。
  • 执行计划:

    • MySQL生成查询的一颗指令树,而后经过存储引擎执行完成这颗指令树并返回结果,这点和其余生成查询字节码来执行查询的其它关系数据库不一样。
    • 最终的执行计划包含了重构查询的所有信息。能够对查询执行EXPLAIN EXTENDED后再执行SHOW WARNINGS看到重构的查询(和原查询有相同的语义,可是查询语句可能并不彻底相同)
    • 任何多表查询均可以用一棵树来表示,咱们能够理解为一颗平衡树,可是MySQL的执行计划是一颗左侧深度优先的树。
  • 关联查询优化器:

    • MySQL优化器最重要的一部分就是关联查询优化,它决定了多个表关联时的顺序。一般多表关联的时候,能够有多张不一样的关联顺序,而关联查询优化器则经过评估不一样顺序的成原本选择一个代价最小的关联顺序。
    • 有的时候,优化器给出的并非最优的关联顺序,这时可使用STRAIGHT_JOIN关键字来重写查询,让优化器按照你认为的最优关联顺序执行——绝大多数时候,优化器作出的选择都更为准确。
    • 优化器会尽量遍历没一个表而后逐个作嵌套循环计算每一颗可能的执行计划的树的成本,最后返回一个最优的执行计划。
      • 若是有N个表关联,那么须要检查n的阶乘种关联顺序。咱们称之为全部可能的执行计划的”搜索空间“。若是搜索空间很是大,当须要关联的表超过optimizer_search_depth的限制,优化器会选择使用”贪婪“搜索的方式查找”最优“的关联方式。
    • 优化器偶尔也会选择一个不是最优的执行计划。
    • 有时,各个查询的顺序并不能随意安排,这时关联优化器能够根据这些规则大大减小搜索空间,例如,左链接、相关子查询。这是由于后面的表的查询须要依赖于前面表的查询结果,这种依赖关系一般能够帮助优化器大大减小须要扫描的执行计划数量。
  • 排序优化

    • 不管如何排序都是一个成本很高的操做,因此从性能角度考虑,应尽量避免排序或者尽量避免对大量数据进行排序。

    • 文件排序:当不能使用索引生成排序结果的时候,MySQL须要本身进行排序,若是数据量小则在内存中进行,若是数据量大则须要使用磁盘。

      • 若是须要排序的数据量小于”排序缓冲区“,MySQL使用内存进行”快速排序“操做。
      • 若是内存不够排序,MySQL会先将数据分块,对每一个独立的块使用”快速排序“进行排序,并将各个块的排序结果存放在磁盘上,而后将各个排序好的块进行合并,最后返回排序结果。
    • 排序算法:

      • 两次传输排序(旧版本使用):读取行指针和须要排序的字段,对其进行排序,而后在根据排序结果读取须要的数据行。这须要两次数据传输,即须要从数据表中读取两次数据,第二次读取数据的时候,由于读取顺序列进行排序后的全部记录,这会产生大量的随机IO,因此成本很是高。当使用的是MyISAM表的时候,成本可能会更高,由于MyISAM使用系统调用进行数据读取(很是依赖操做系统对数据的缓存)。不过这样作的优势是:排序的时候存储尽量少的数据,让”排序缓冲区“中尽量容纳更多的行数进行排序。
      • 单次传输排序(4.1后新版本使用):先读取查询所须要的全部列,而后再根据给定列进行排序,最后直接返回排序结果。有点事只须要一次顺序IO读取全部的数据,而无须任何的随机IO,缺点是若是须要返回的列很是大,会额外占用大量的空间,而这些列对排序操做来讲是没有任何做用的。由于单条排序记录很大,因此可能会有更多的排序块须要合并。
      • 很难说以上两个算法效率更高,当查询须要全部的列的总长度不超过max_lenght_for_sort_data时,MySQL使用“单次传输排序”,能够经过参数选择来影响MySQL排序算法的选择。
    • 进行文件排序的时候须要使用的临时存储空间可能会比想象的要大得多。缘由在于MySQL排序时,对每个排序记录都会分配一个足够长的定长空间来存放。

      • 这个定长空间必须足够长以容纳其中最长的字符串。若是是VARCHAR列则须要分配其完整长度,若是使用UTF-8字符集,则为每一个字符预留三个字节。
    • 在关联查询的时候排序:

      • 若是ORDER BY子句中全部列都来自关联的第一个表,那么MySQL在关联处理第一个表的时候进行文件排序。能够在EXPLAIN看到Extra字段有“Using filesort”

      • 除第一种场景,MySQL都会先将关联的结果放到一个临时表中,而后在全部的关联都结束后,再进行文件排序操做。用EXPLAIN可看到“Using temporary;Using filesort”。若是查询中有LIMIT的话,LIMIT也会在排序以后应用,因此即便须要返回较少的数据,临时表和须要排序的数据仍然很是大。

        5.6后版本在这里作了些改进:当只须要返回部分排序结果的时候,例如使用了LIMIT子句,MySQL再也不对全部的结果进行排序,而是根据实际状况,选择抛弃不知足条件的结果,而后在进行排序。

4.4.4 查询执行引擎

  • 在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎则根据这个执行计划来完成整个查询。

    • 这里的执行计划是一个数据结构,不一样于其余关系数据库生成的字节码。
  • 查询执行阶段不是那么复杂,MySQL只是简单地根据执行计划给出的指令逐步执行。在根据执行计划逐步执行的过程当中,又大量的操做须要调用存储引擎实现的“handle API”接口来完成。

    • 查询中的每个表由一个handler实例来标识。实际上,MySQL在优化阶段就为每一个表建立了一个handler实例,优化器根据这些实例的接口能够获取表的相关信息。
    • 存储引擎的接口有着很是丰富的功能,可是底层接口却只有十几个,这些接口相互配合可以完成查询的大部分操做
  • 并非全部的操做都有handler完成。例如,当MySQL须要进行表锁的时候,handler可能会实现本身级别的、更细粒度的锁,如InnoDB就实现了本身的行基本锁,但这并不能代替服务器层的表锁。若是是全部存储共有的特性则由服务器层实现,如时间、日期函数、视图和触发器等。

4.4.5 返回结果给客户端

  • 查询执行的最后一个阶段。即便查询不须要返回结果集给客户端,MySQL仍然会返回这个查询的一些信息,如该查询影响到的行数。
  • 若是查询能够被缓存,那么MySQL在该阶段也会将结果放到查询缓存中。
  • 结果集返回客户端是一个增量、逐步返回的过程。例如,服务器处理完关联操做的最后一个关联表,开始生成第一条结果时,MySQL就能够开始向客户端逐步返回结果。
    • 好处:
      • 服务器端无须存储太多的结果,也就不会由于要返回太多的结果而消耗太多的内存。
      • 让客户端第一时间得到返回的结果。可以使用SQL_BUFFER_RESULT设置。
    • 结果集中的每一行都会以一个知足MySQL客户端/服务器通讯协议的封包发送,再经过TCP协议进行传输,在TCP传输的过程当中,可能对MySQL的封包进行缓存而后批量传输。

4.5 MySQL查询优化器的局限性

MySQL的万能“嵌套循环”并非对每种查询都是最优的,但只对少部分查询不适用,咱们每每能够经过改写查询让MySQL高效地完成工做。另外,5.6版本会消除不少本来的限制,让更多的查询可以已尽量高的效率完成。

4.5.1 关联子查询

MySQL的子查询实现得很是糟糕,最糟糕的一类查询是WHERE条件语句中包含IN()的子查询。

SELECT * FROM sakila.film
WHERE film_id IN(
    SELECT film_id FROM sakil.film_actor WHERE actor_id =1 );
-- MySQL对IN()列表中的选项有专门的优化策略,但关联子查询并非这样的,MySQL会将相关的外层表压到子查询中,它认为这样能够高效地查找到数据行。也就是说,以上查询会被MySQL更改为:
SELECT * FROM sakila.film
WHERE EXISTS(
    SELECT film_id FROM sakil.film_actor WHERE actor_id =1
    AND film_actor.film_id = film.film_id);
-- 这时子查询须要根据film_id来关联外部表的film,由于须要film_id字段,因此MySQL认为没法先执行这个子查询。经过EXPLIAN能够看到子查询是一个相关子查询(DEPENDENT SUBQUERY),而且能够看到对film表进行全表扫描,而后根据返回的film_id逐个进行子查询。若是外层是一个很大的表,查询性能会很糟糕。
-- 优化重写方式1:
SELECT film.* FROM sakila.film
    INNER JOIN sakil.film_actor USING(film_id) 
WHERE actor_id =1;
-- 优化重写方式2:使用函数GROUP_CONCAT()在IN()中构造一个逗号分割的列表。
-- 优化重写方式3,使用EXISTS()等效的改写查询:
SELECT * FROM sakila.film
WHERE EXISTS(
    SELECT film_id FROM sakil.film_actor WHERE actor_id =1
    AND film_actor.film_id = film.film_id);复制代码
  • 如何用好关联子查询
    • 并非全部的关联子查询性能都不好,须要先测试再作出判断。不少时候,关联子查询是一种很是合理、天然、甚至是性能最好的写法。

4.5.2 UNION的限制

  • 有时,MySQL没法将限制条件从外层“下推”到内层,这使得原表可以限制部分返回结果的条件没法应用到内层查询的优化上。

  • 若是但愿UNION的各个子句可以根据LIMIT只取部分结果集,或者但愿可以先排好序再合并结果集的话,就须要在UNION的各个子句中分别使用这些子句。另外,从临时表取出数据的顺序是不必定的,若是要得到正确的顺序,还须要加上一个全局的ORDER BY 和 LIMIT

    (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;
    -- 在UNION子句分别使用LIMIT
    (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;复制代码

4.5.3 索引合并优化

  • 5.0及其新版本,当WHERE条件包含多个复杂条件的时候,MySQL可以访问当个表的多个索引以合并和交叉过滤来定位须要查找的行。

4.5.4 等值传递

  • 某些时候,等值传递会带来一些意想不到的额外消耗。例如,有一个很是大的IN()列表,而MySQL优化器发现存在WHERE、ON或者USING的子句,将这个列表的值和另外一个表的某个列相关联。
    • 优化器会将IN()列表都复制应用到关联的各个表中。一般各个表由于新增了过滤条件,优化器能够更高效地从存储引擎过滤记录。可是若是列表很是大,则会致使优化和执行都会变得更慢。

4.5.5 并行执行

  • MySQL没法利用多核特性来并行执行查询,这点和其余关系型数据库不同。

4.5.6 哈希关联

  • MySQL并不支持哈希关联——全部的关联都是嵌套循环关联。不过,能够经过创建一个哈希索引来曲线实现哈希关联。
  • 若是使用的是Memory存储引擎,则索引是哈希索引,因此关联的时候也相似于哈希关联。

4.5.7 松散索引扫描

  • MySQL并不支持松散索引扫描,也就没法按照不连续的方式扫描一个索引。一般,MySQL的索引扫描须要先定义一个起点和终点,即便须要的数据只是这段索引中不多数的几个,MySQL仍须要扫描这段索引中每个字段。

  • 示例:假设咱们有索引(a,b),有如下查询SELECT ... FROM tb1 WHERE b BETEWEEN 2 AND 3;,由于只使用了字段b而不符合索引的最左前缀,MySQL没法使用这个索引,从而只能经过全表扫描找到匹配的行。

    MySQL经过全表扫描找到须要的记录
    MySQL经过全表扫描找到须要的记录

    了解索引结构的话,会发现还有一个更快的办法执行上面的查询。索引的物理结构(不是存储引擎API)使得能够先扫描a列第一个值对应的b列的范围,而后在跳到a列第二个只扫描对应的b列的范围,即松散索引扫描。这时就无须再使用WHERE过滤,由于已经跳过了全部不须要的记录。MySQL并不支持松散索引扫描

    松散索引扫描
    松散索引扫描

  • MySQL5.0 之后的版本,某些特殊的场景下是可使用松散索引扫描的。例如,在一个分组查询中须要找到分组的最大值和最小值:

    -- 在Extra字段显示“Using index for group-by”,表示使用松散索引扫描
    EXPLAIN SELECT actor_id, MAX(film_id)
    FROM sakila.film_actor
    GROUP BY actor\G;复制代码
  • 在MySQL很好地支持松散索引扫描以前,一个简单的绕过办法就是给前面的列加上可能的常数值。5.6以后的版本,关于松散索引扫描的一些限制将会经过“索引条件下推(index condition pushdown)”的方式解决

4.5.8 最大值和最小值优化

  • 对于MIN()和MAX()查询,MySQL的优化作得并很差。

    SELECT MIN(actor_id) FROM sakila.actor WHERE first_name = 'PENELOPE';
    -- 由于在first_name上没有索引,MySQL将会进行一次全表扫描。若是MySQL可以进行主键扫描,那么理论上当MySQL读到第一个知足条件的记录,就是须要找到的最小值,由于主键是严格按照actor_id字段的大小顺序排列的。
    -- 曲线优化办法:移除MIN(),而后使用LIMIT
    SELECT actor_id FROM sakila.actor USE INDEX(PRIMARY) WHERE first_name = 'PENNLOPE' LIMIT 1;
    -- 该SQL已经没法表达它的本意,通常咱们经过SQL告诉服务器须要什么数据,再由服务器决定如何最优地获取数据。但有时候为了得到更高的性能,须要放弃一些原则。复制代码

4.5.9 在同一个表查询和更新

  • MySQL不容许对同一张表同时进行查询和更新。这其实并非优化器的限制,若是清楚MySQL是如何执行查询的,就能够避免这种状况。能够经过生成表来绕过该限制。

    -- 符合标准的SQL,可是没法运行
    mysql> UPDATE tbl AS outer_tbl
    -> SET cnt = (
    -> SELECT count(*) FROM tbl AS inner_tbl
    -> WHERE inner_tbl.type = outer_tbl.type
    -> );
    -- 生成表来绕过该限制:
    mysql> UPDATE tbl
    -> INNER JOIN(
    -> SELECT type, count(*) AS cnt
    -> FROM tbl
    -> GROUP BY type
    -> ) AS der USING(type)
    -> SET tbl.cnt = der.cnt;复制代码

4.6 查询优化器的提示(hint)

若是对优化器选择的执行计划不满意,可使用优化器提供的几个提示(hint)来控制最终的执行计划。不过MySQL升级后可能会致使这些提示无效,须要从新审查。

  • 部分提示类型:

    • HIGH_PRIORITY和LOW_PRIORITY:

      告诉MySQL当多个语句同时访问某一个表的时候,这些语句的优先级。只对使用表锁的存储引擎有效,但即便是在MyISAM中也要慎重,由于这两个提示会致使并发插入被禁用,可能会致使严重下降性能

      • HIGH_PRIORITY:用于SELECT语句时,MySQL会将此语句从新调度到全部正在等待表锁以便修改数据的语句以前。其实是将其放在表的队列的最前面,而不是按照常规顺序等待。用于INSERT语句,其效果只是简单地抵消了全局LOW_PRIORITY设置对该语句的影响。
      • 用于SELECT、INSERT、UPDATE和DELETE语句,让该语句一直处于等待状态,只要队列中还有须要访问同一个表的语句——即便那些比该语句还晚提交到服务器的语句。
    • DELAYED:

      • 只对INSERT和REPLACE有效。
      • MySQL会将使用该提示的语句当即返回给客户端,并将插入的行数据放入到缓冲区,而后在表空闲时批量将数据写入。
      • 日志系统使用这样的提示很是有效,或者是其余须要写入大量数据可是客户端却不须要等待单条语句完成IO的应用。
      • 限制:并非全部的存储引擎都支持;而且该提示会致使函数LAST_INSERT_ID()没法正常工做
    • STRAIGHT_JOIN:

      当MySQL没能正确选择关联顺序的时候,或者因为可能的顺序太多致使MySQL没法评估全部的关联顺序的时候,STRAIGNT_JOIN都会颇有用。特别是在如下第二种状况,MySQL可能会花费大量时间在”statistics“状态,加上这个提示会大大减小优化器的搜索空间。

      能够先使用EXLPAN语句来查看优化器选择的关联顺序,而后使用该提示来重写查询,肯定最优的关联顺序。可是在升级MySQL的时候,要从新审视这类查询。

      • 放置在SELECT语句的SELECT关键字以后:让查询中全部的表按照在语句中出现的顺序进行关联
      • 放置在任何两个关联表的名字之间:固定其先后两个表的关联顺序。
    • SQL_SMALL_RESULT和SQL_BIG_RESULT:

      • 只对SELECT语句有效,告诉优化器对GROUP BY或者DISTINCT查询如何使用临时表和排序。
      • SQL_SMALL_RESULT告诉优化器结果集很小,能够将结果集放在内存中的索引临时表,避免排序操做
      • SQL_BIG_RESULT告诉优化器结果集可能会很是大,建议使用磁盘临时表作排序操做
    • SQL_BUFFER_RESULT:

      • 告诉优化器将查询结果放入到一个临时表,而后尽量地释放表锁。
    • SQL_CACHE和SQL_NO_CACHE

      • 告诉MySQL这个结果集是否应该缓存在查询缓存中
    • SQL_CALC_FOUND_ROWS:

      • 严格来讲,并非一个优化器提示,它不会告诉优化器任何关于执行计划的东西。
      • 让MySQL返回的结果集包含更多信息。
      • 查询中加上该提示MySQL会计算除去LIMIT子句后这个查询要返回的结果集的总数,而实际上只返回LIMIT要求的结果集
    • FOR UPDATE和LOCK IN SHARE MODE

      • 也不是真正的优化器提示。
      • 主要控制SELECT语句的锁机制,但只对实现了行级锁的存储引擎有效。
      • 会对符合查询条件的数据行加锁。对于INSERT...SELECT语句是不须要这两个提示的,由于5.0及新版本会默认加锁。
      • InnoDB是惟一内置支持这两个提示的引擎。可是这两个提示会让某些优化没法正常使用,例如索引覆盖扫描。InnoDB不能在不访问主键的状况下排他地锁定行,由于行的版本信息保存在主键中。
      • 这两个提示常常被滥用,很容易形成服务器的锁争用问题,应该尽量避免使用。
    • USING INDEX、IGONRE INDEX和FORCE INDEX:

      • 告诉优化器使用或不使用哪些索引来查询记录(例如,在决定关联顺序的时候使用哪一个索引)。
      • 5.1及新版本能够经过FOR ORDER BY和FOR GROUP BY来制定是否对排序和分组有效。
      • USING INDEX和FORCE INDEX基本相同。可是FORCE INDEX会告诉优化器全表扫描的成本会远远高于索引扫描,哪怕实际上该索引的用处不大。当发现优化器选择了错误的索引,或者由于某些缘由(好比在不适用ORDER BY的时候但愿结果有序)要使用另外一个索引时,可使用该提示。
  • 5.0和更新版本新增用来控制优化器行为的参数:

    • optimizer_search_depth:控制优化器在穷举执行计划时的限度。若是查询长时间处于"Statistics"状态,能够考虑调低
    • optimizer_prune_level:默认打开,让优化器根据须要扫描的行数来决定是否跳过某些执行计划
    • optimizer_swith:包含了一些开启/关闭优化器特性的标志位。

4.7 优化特定类型的查询

4.7.1 优化count()查询

  • count()的做用:

    • 统计某个列值的数量,要求列值是非空的(不统计NULL)。若是在COUNT()的括号中指定了列或者列的表达式,则统计的就是这个表达式有值的结果数(而不是NULL)
    • 统计行数。当MySQL确认括号内的表达式值不可能为空时,实际上就是统计行数。
      • 当使用COUNT()时,并不会像咱们猜测的扩展成全部的列,实际上,它会忽略全部的列而直接统计全部的行数。
      • 常见错误:在括号内指定了一个列却但愿统计结果集的行数。
  • 关于MyISAM的神话:

    • 只有没有任何WHERE条件的count(*),MyISAM的COUNT()函数才会很是快,由于此时无须实际地去计算表的行数。MySQL能够利用存储引擎的特性直接得到这个值。
    • 若是MySQL知道某列col不可能为NULL值,那么内部会把count(col)优化成count(*)
    • 当统计带有WHERE子句,MyISAM的COUNT()和其余存储引擎没有任何不一样,就不会再有神话般的速度。
  • 简单的优化

    • 利用MyISAM在count(*)全表很是快的特性,来加速一些特定条件的查询。

      -- 使用标准数据据worold
      SELECT count(*) FROM world.city WHERE ID > 5;
      -- 将条件反转,可很大程度减小扫描行数到5行之内
      SELECT (SELECT count(*) FROM world.city) - COUNT(*) 
      FROM world.city WHERE ID <= 5;复制代码
    • 示例:假设可能须要经过一个查询返回各类不一样颜色的商品数量

      -- 使用SUM
      SELECT SUM(IF(color = 'blue', 1, 0)) AS blue,SUM(IF(color = 'red', 1, 0)) AS red FROM items;
      -- 使用COUNT,只须要将知足条件的设置为真,不知足设置为NULL
      SELECT COUNT(color = 'blue' OR NULL) AS blue, COUNT(color = 'red' OR NULLASred FROM items;复制代码
  • 使用近似值:

    • 有时候某些业务场景并不要求彻底精确的COUNT值,此时能够用近似值来代替。
    • EXPLAIN出来的优化器估算的行数就是一个不错的近似值,执行EXPLAIN并不须要真正地去执行查询,因此成本很低。
    • 例如:若是要精确统计网站在线人数,一般WHERE条件会很复杂,一方面须要过滤当前非活跃用户,另外一方面还须要过滤系统中某些特定ID的默认用户,去掉这些约束条件对总数的影响很小,但却可能很好地提升该查询的性能。更进一步优化则能够尝试删除DISTINCT这样的约束来避免文件排序。这样重写的查询比精确查询要快得多,而返回的结果则几乎相同。
  • 更复杂的优化:

    • 一般来讲,COUNT()都须要扫描大量的行(意味着要访问大量数据)才能得到精确的结果,所以是很难优化的。
    • 优化方法:
      • 前面提到的方法
      • 在MySQL层面能作的只有索引覆盖扫描
      • 考虑修改应用架构,能够增长汇总表,或者相似Memcached这样的外部缓存系统。可能很快你就会发现陷入到一个熟悉的困境,”快速、精确和实现简单“,三者永远只能知足其二,必须舍掉其中一个。

    4.7.2 优化关联查询

    • 确保ON或者USING子句的列上有索引。
      • 在建立索引的时候就须要考虑关联的顺序。当表A和表B用列c关联的时候,入股优化器的关联顺序是B、A,那么就不须要在B表的对应列上建立索引。没有的索引智慧带来额外的负担。
      • 通常来讲,除非有其余理由,不然只须要在关联顺序的第二个表的响应列上建立索引。
    • 确保任何的GROUP BY和ORDER BY中的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化这个过程。
    • 当升级MySQL的时候须要注意:关联语法、运算符优先级等其余可能会发生变化的地方。由于之前是普通关联的地方可能会变成笛卡儿积,不一样类型的关联可能会生成不一样的结果。

4.7.3 优化子查询

  • 尽量使用关联查询代替。这并非绝对的,5.6及新版本或者MariaDB,能够直接使用子查询。

4.7.4 优化GROUP BY和DISTINCT

  • 在不少场景下,MySQL都使用一样的办法优化这两种查询,事实上,MySQL优化器会在内部处理的时候相互转换这两类查询。它们均可以使用索引来优化,这也是最有效的优化办法。
  • 当没法使用索引时,GROUP BY使用两种策略:使用临时表或者文件排序来作分组。能够经过提示SQL_BIG_RESULT和SQL_SMALL_RESULT来让优化器按照你但愿的方式运行。
  • 若是须要对关联查询作分组GROUP BY,而且是按照查找表中的某个列进行分组,那么一般采用查找表的标识列分组的效率会比其余列更高
    • SELECT后面出现的非分组列必定是直接依赖于分组列,而且在每一个组内的值是惟一的,或者是业务上根本不在意这个值具体是什么。
    • 在分组查询的SELECT中直接使用非分组列一般不是什么好主意,由于这样的结果一般是不定的,当索引改变,或者优化器选择不一样的优化策略时均可能致使结果不同。
  • 若是没有经过ORDER BY子句显示地制定排序列,当查询使用GROUP BY子句的时候,结果集会自动按照分组的字段进行排序。若是不关心结果集的排序,而这种默认排序又致使了须要文件排序,则可使用ORDER BY NULL,让MySQL再也不进行文件排序。
  • 优化GROUP BY WITH ROLLUP:
    • 分组查询的一个变种就按要求MySQL对返回的分组结果在作一次超级聚合。可使用WITH ROLLUP子句来实现这种逻辑,但可能会不够优化。
      • 不少时候,若是能够在应用程序中作超级聚合是更好的,虽然这须要返回给客户端更多的结果。
      • 也能够在FROM子句中嵌套使用子查询,或者是经过一个临时表存放中间数据,而后和临时表执行UNION来获得最终结果
      • 最好的办法是尽量将WITH ROLLUP功能转移到应用程序中处理。

4.7.5 优化LIMIT分页

  • 在系统中须要进行分页操做的时候,一般会使用LIMIT加上偏移量的办法来实现,同时加上合适的ORDER BY子句。若是有对应的索引,一般效率会很不错,不然,MySQL须要作大量的文件排序操做。
  • 偏移量很是大的时候优化办法:
    • 在页面中限制分页的数量
    • 优化大偏移量的性能:
      • 尽量使用覆盖索引扫描,而不是查询全部的列。例如使用延迟关联
      • 有时能够将LIMIT查询转换为已知位置的查询,让MySQL经过范围扫描得到对应的结果。
      • 使用”书签”记录上一次取数据的位置。
      • 使用预先计算的汇总表,或者关联到一个冗余表,冗余表只包含主键列和须要作排序的数据列。

4.7.6 优化SQL_CALC_FOUND_ROWS

分页的时候,另外一个经常使用的技巧是在LIMIT语句中加上SQL_CALC_FOUND_ROWS提示,这样就能够得到去掉LIMIT之后知足条件的行数,所以能够做为分页的总数。加上这个提示后,MySQL无论是否须要都会扫描全部知足条件的行,而后抛弃掉不须要的行,而不是在知足LIMIT的行数后就终止扫描。因此该提示的代价可能很是高。

  • 设计方案1:将具体的页数换成“下一页”按钮,假设每页显示20条记录,那么使用LIMIT返回21条并只显示20条,若是第21条存在,那么显示“下一页”按钮,不然说明没有更多的数据,也就无须显示
  • 设计方案2:先获取并缓存较多的数据,而后每次分页都从这个缓存中获取。
  • 其余设计方案:有时候也能够考虑使用EXPLAIN的结果中的rows列的值来做为结果集总数的近似值(实际上Google的搜索结果总数也是个近似值)。当须要精确结果时,再单独使用COUNT(*)来知足需求,这时候若是可以使用覆盖索引则一般也会比SQL_CALC_FOUND_ROWS快得多。

4.7.7 优化UNION查询

  • MySQL老是经过建立并填充临时表的方式来执行UNION查询。所以不少优化策略都无法很好的使用。常常须要手动地将WHERE、LIMIT、ORDER BY子句下推UNION的各个子查询中,以便优化器能够充分利用这些条件进行优化
  • 除非确实须要服务器消除重复的行,不然就必定要使用UNION ALL。
    • 若是没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这会致使对整个临时表作惟一性检查,这样作的代价很是高。
    • 即便有ALL关键字,MySQL仍然会使用临时表存储结果。

4.7.8 静态查询分析

Percona Toolkit contains pt-query-advisor, a tool that parses a log of queries, analyzes
the query patterns, and gives annoyingly detailed advice about potentially bad practices
in them.

4.7.9 使用用户自定义变量

  • 用户自定义变量是一个用来存储内容的临时容器,在链接MySQL的整个过程当中都存在。在查询中混合使用过程化和关系化逻辑的时候,该特性很是有用。

  • 使用方法:

    SET @one       := 1;
    SET @min_actor := (SELECT MIN(actor_id) FROM sakila.actor);
    SET @last_week := CURRENT_DATE - INTERVAL 1 WEEK;
    SELECT ... WHERE col <= @last_week;
    -- 具备“左值”特性,在给一个变量赋值的同时使用这个变量
    SELECT actor_id, @rownum := @rownum + 1 As rownum ...复制代码
  • 没法使用的场景:

    • 使用自定义变量的查询,没法使用查询缓存
    • 不能在使用常量或者标识符的地方使用自定义变量,例如表名、列名和LIMIT子句中。
    • 用户自定义变量的生命周期是在一个链接中有效,因此不能用它们来作链接中的通讯。
    • 若是使用链接池或者持久化链接,自定义变量可能让看起来毫无关系的代码交互(若是是这样,一般是代码或链接池bug)
    • 在5.0以前的版本,是大小写敏感的,因此要注意代码在不一样MySQL版本间的兼容性问题。
    • 不能显示地声明自定义变量的类型。肯定未定义变量的具体类型的时机在不一样MySQL版本中也可能不同。若是但愿是整形/浮点/字符串,最好初始化时0/0.0/‘’。MySQL的用户自定义变量是一个动态类型,在赋值的时候会改变。
    • MySQL优化器在某些场景下可能会将这些变量优化掉,这可能致使代码不按预想的方式运行。
    • 赋值的顺序和赋值的时间点并不老是固定的,这依赖于优化器的决定。实际状况可能让人困惑。
    • 赋值符号:=的优先级很是低,因此要注意,赋值表达式应该使用明确的括号。
    • 使用未定义变量不会产生任何语法错误,若是没注意到这一点,很是容易犯错。
  • 应用场景:

    • 优化排名语句:

      -- 查询获取演过最多电影的前10位演员,而后根据出演电影次数作一个排名,若是出演次数同样,则排名相同。
      mysql> SET @curr_cnt := 0, @prev_cnt := 0, @rank := 0;
      -> SELECT actor_id,
      -> @curr_cnt := cnt AS cnt,
      -> @rank := IF(@prev_cnt <> @curr_cnt, @rank + 1, @rank) AS rank,
      -> @prev_cnt := @curr_cnt AS dummy
      -> FROM (
      -> SELECT actor_id, COUNT(*) AS cnt
      -> FROM sakila.film_actor
      -> GROUP BY actor_id
      -> ORDER BY cnt DESC
      -> LIMIT 10
      -> ) as der;复制代码
    • 避免重复查询刚刚更新的数据:

      -- 在更新行的同时又但愿获取获得该行的信息。虽然看起来仍然须要两个查询和两次网络来回,但第二个查询无须访问任何数据表,速度会快不少
      UPDATE t1 SET lastUpdated = NOW() WHERE id = 1 AND @now := NOW();
      SELECT @now;复制代码
    • 统计更新和插入的数量

      -- 使用了INSERT ON DUPLICATE KEY UPDATE的时候,想统计插入了多少行的数据,而且有多少数据是由于冲突而改写成更新操做。
      -- 实现该办法的本质以下,当每次因为冲突致使更新时对变量@x自增一次,而后经过对这个表达式乘以0来让其不影响要更新的内容
      INSERT INTO t1(c1, c2) VALUES(4, 4), (2, 1), (3, 1)
      ON DUPLICATE KEY UPDATE
      c1 = VALUES(c1) + ( 0 * ( @x := @x +1 ) );复制代码
    • 肯定取值的顺序

      • 一个最多见的问题,没有注意到在赋值和读取变量的使用多是在查询的不一样阶段。

        -- WHERE和SELECT是在查询执行的不一样阶段被执行的,而WHERE是在ORDER BY文件排序操做以前执行。
        mysql> SET @rownum := 0;
        mysql> SELECT actor_id, @rownum := @rownum + 1 AS cnt
        -> FROM sakila.actor
        -> WHERE @rownum <= 1;
        +----------+------+
        | actor_id | cnt  |
        +----------+------+
        | 1        | 1    |
        | 2        | 2    |
        +----------+------+复制代码
      • 尽可能让变量的赋值和取值发生在执行查询的同一个阶段。

        mysql> SET @rownum := 0;
        mysql> SELECT actor_id, @rownum AS rownum
        -> FROM sakila.actor
        -> WHERE (@rownum := @rownum + 1) <= 1;复制代码
      • 将赋值运距放到LEAST(),这样就能够彻底不改变排序顺序的时候完成赋值操做。这个技巧在不但愿对子句的执行结果有影响却又要完成变量复制的时候颇有用。这样的函数还有GREATEST(), LENGTH(), ISNULL(), NULLIF(), IF(), 和COALESCE()。

        -- LEAST()老是返回0
        mysql> SET @rownum := 0;
        mysql> SELECT actor_id, first_name, @rownum AS rownum
        -> FROM sakila.actor
        -> WHERE @rownum <= 1
        -> ORDER BY first_name, LEAST(0, @rownum := @rownum + 1);复制代码
  • 编写偷懒的UNION:

    假设须要编写一个UNION查询,其第一个子查询做为分支条件先执行,若是找到了匹配的行,则跳过第二个分支。在某些业务场景中确实会有这样的需求,好比如今一个频繁访问的表中查找“热”数据,找不到再去另一个较少访问的表中查找“冷数据“。(区分热冷数据是一个很好提升缓存命中率的办法)。

    -- 在两个地方查找一个用户,一个主用户表,一个长时间不活跃的用户表,不活跃的用户表的目的是为了实现更高效的归档。
    -- 旧的UNION查询,即便在users表中已经找到了记录,上面的查询仍是会去归档表中再查找一次。
    SELECT id FROM users WHERE id = 123
    UNION ALL
    SELECT id FROM users_archived WHERE id = 123;
    -- 用一个偷懒的UINON查询来抑制这样的数据返回,当第一个表中没有数据时,咱们才在第二个表中查询。一旦在第一个表中找到记录,就定义一个变量@found,经过在结果列中作一次赋值来实现,而后将赋值放在函数GREATEST中来避免返回额外的数据。为了明确结果来自哪个表,新增了一个包含表名的列。最后须要在查询的末尾将变量重置为NULL,保证遍历时不干扰后面的结果。
    SELECT GREATEST(@found := −1, id) AS id, 'users' AS which_tbl
    FROM users WHERE id = 1
    UNION ALL
    SELECT id, 'users_archived'
    FROM users_archived WHERE id = 1 AND @found IS NULL
    UNION ALL
    SELECT 1, 'reset' FROM DUAL WHERE ( @found := NULL ) IS NOT NULL;复制代码
  • 用户自定义变量的其余用处:

    • 不只是在SELECT语句中,在其余任何类型的SQL语句中均可以对变量进行赋值。例如,能够像前面使用子查询的方式改进排名语句同样来改进UPDATE语句。
    • 有时须要使用一些技巧来得到但愿的结果。由于优化器可能会把变量看成一个编译时常量来对待,而不是对其进行赋值。将函数放在相似LEAST()这样的函数中一般能够避免这样的问题。另外一个办法是在查询被执行前检查变量是否被赋值。
  • 其余用法:

    • 查询运行时计算总数和平均值
    • 模拟GROUP语句中的函数FIRST()和LAST()
    • 对大量数据作一些数据计算。
    • 计算一个大表的MD5散列值
    • 编写一个样本处理函数,当样本中的数值超过某个边界值的时候将其变为0
    • 模拟读/写游标
    • 在SHOW语句的WEHRE子句中加入变量值。

4.8 案例学习

4.8.1 使用MySQL构建一个队列表

使用MySQL来实现对列表是一个取巧的作法,不少系统在高流量、高并发的状况下表现并很差。典型的模式是一个表包含多种类型的记录:未处理记录、已处理记录、正在处理的记录等等。一个或者多个消费者线程在表中查找未处理的记录,而后声称正在处理,当处理完成后,再将记录更新为已处理状态。通常的,例如邮件发送、多命令处理、评论修改等会使用相似模式,但

原有处理方式不合适的缘由:

  • 随着对列表愈来愈大和索引深度的增长,找到未处理记录的速度会随之变慢。
  • 通常的处理过程分两步,先找到未处理的记录而后加锁。找到记录会增长服务器的压力,而加锁操做则会让各个消费者进程增长竞争,由于这是一个串行化操做。

优化过程:

  • 将对列表分红两部分,即将已处理记录归档或者存放到历史表,这样始终保证对列表很小。

  • 找到未处理记录通常来讲都没问题,若是有问题则能够经过使用消息方式来通知各个消费者。

    • 可已使用一个带有注释的SLEEP()函数作超时处理。这让线程一直阻塞,直到超时或者另外一个线程使用KILL QUERY结束当前的SLEEP。所以,当再向对列表中新增一批数据后,能够经过SHOW PROCESSLIST,根据注释找到当前正在休眠操做的线程,并将其KILL。可使用函数GET_LOCK和RELEASE_LOCK()来实现通知,或者能够在数据库以外实现,如使用一个消息服务。

      SELECT /* waiting on unsent_emails */ SLEEP(10000), col1 FROM table;复制代码
  • 最后一个问题是如何让消费者标记正在处理的记录,而不至于让多个消费者重复处理一个记录。

    • 尽可能避免使用SELECT FOR UPDATE,这一般是扩展性问题的根源,这会致使大量的书屋阻塞并等待。不光是队列表,任何状况下都要避免。

    • 能够直接使用UPDATE来更新记录,而后检查是否还有其余的记录须要处理。(全部的SELECT FOR UPDATE均可以使用相似的方式改写)

      -- 该表的owner用来存储当前正在处理这个记录的链接ID,即由函数CONNECTION_ID()返回额ID,若是当前记录没有被任何消费者处理,则该值为0
      CREATE TABLE unsent_emails (
          id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
          -- columns for the message, from, to, subject, etc.
          status ENUM('unsent', 'claimed', 'sent'),
          owner INT UNSIGNED NOT NULL DEFAULT 0,
          ts TIMESTAMP,
          KEY (owner, status, ts)
      );
      -- 常见的处理办法。这里的SELECT查询使用到索引的两个列,理论上查找的效率应该更快。问题是,两个查询之间的“间隙时间”,这里的锁会让全部其余同一的查询所有被阻塞。全部这样的查询将使用相同的索引,扫描索引相同结果的部分,因此极可能被阻塞。
      BEGIN;
      SELECT id FROM unsent_emails
          WHERE owner = 0 AND status = 'unsent'
          LIMIT 10 FOR UPDATE;
      -- result: 123, 456, 789
      UPDATE unsent_emails
          SET status = 'claimed', owner = CONNECTION_ID()
          WHERE id IN(123, 456, 789);
      COMMIT;
      -- 改进后更高效的写法,无须使用SELECT查询去找到哪些记录尚未被处理。客户端的协议会告诉你更新了几条记录,因此能够直到此次须要处理多少条记录。
      SET AUTOCOMMIT = 1;
      COMMIT;
      UPDATE unsent_emails
          SET status = 'claimed', owner = CONNECTION_ID()
          WHERE owner = 0 AND status = 'unsent'
          LIMIT 10;
      SET AUTOCOMMIT = 0;
      SELECT id FROM unsent_emails
          WHERE owner = CONNECTION_ID() AND status = 'claimed';
      -- result: 123, 456, 789复制代码
  • 最后还需处理一种特殊状况:那些正在被进程处理,而进程自己却因为某种缘由退出的状况。

    • 只须要按期运行UPDATE语句将它都更新成原始状态,而后执行SHOW PROCESSLIST,获取当前正在工做的线程ID,并使用一些WHERE条件避免取到那些刚开始处理的进程

      -- 假设获取的线程ID有(十、20、30),下面的更新语句会将处理时间超过10分钟的记录状态更新成初始状态。
      -- 将范围条件放在WHERE条件的末尾,这个查询刚好能勾使用索引的所有列,其它的查询也都能使用上这个索引,这样就避免了再新增一个额外的索引来知足其它的查询
      UPDATE unsent_emails
          SET owner = 0, status = 'unsent'
        WHERE owner NOT IN(0, 10, 20, 30) AND status = 'cla
          AND ts < CURRENT_TIMESTAMP - INTERVAL 10 MINUTE;复制代码

该案例中的一些基础原则:

  • 尽可能少作事,能够的话就不要作任何事。除非不得已,不然不要使用轮询,由于这会增长负载,并且还会带来不少低产出的工做。
  • 尽量快地完成须要作的事情。尽可能使用UPDATE代替先SELECT FOR UPDATE再UPDATE的写法,由于事务的提交的速度越快,持有的锁时间就越短,能够大大减小竞争和加速串行执行效率。将已经处理完成和未处理的数据分开,保证数据集足够小。
  • 这个案例的另外一个启发是,某些查询是没法优化的;考虑使用不一样的查询或者不一样的策略去实现相同的目的。一般对于SELECT FOR UPDATE就须要这样的处理

有时,最好的办法就是将任务队列从数据库中迁移出来,Redis和memcached就是一个很好的队列容器。

6.8.2 计算两点之间的距离

不建议使用MySQL作太复杂的空间计算存储,PostgreSQL在这方面是一个不错的选择。一个典型的例子是计算以某个点为中心,必定半径内的全部点。例如查找某个点附近全部能够出租的房子,或者社交网站中”匹配“附近的用户。

假设咱们有以下表,这里经度和纬度的单位都是度:

CREATE TABLE locations (
  id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(30),
  lat FLOAT NOT NULL,
  lon FLOAT NOT NULL
);
INSERT INTO locations(name, lat, lon)
  VALUES('Charlottesville, Virginia', 38.03, −78.48),
  ('Chicago, Illinois', 41.85, −87.65),
  ('Washington, DC', 38.89, −77.04);复制代码

假设地球是圆的,而后使用两点所在最大圆(半正矢)公式来计算两点之间的距离。现有坐标latA和lonA、latB和lonB,那么点A和点B的距离计算公式以下:

ACOS(
COS(latA) * COS(latB) * COS(lonA - lonB)
+ SIN(latA) * SIN(latB)
)复制代码

计算的结果是一个弧度,若是要将结果转换成英里或公里,则须要乘以地球的半径。

SELECT * FROM locations WHERE 3979 * ACOS(
  COS(RADIANS(lat)) * COS(RADIANS(38.03)) * COS(RADIANS(lon) - RADIANS(-78.48))
  + SIN(RADIANS(lat)) * SIN(RADIANS(38.03))
) <= 100;复制代码

这类查询不只没法使用索引,并且还会很是消耗CPU时间,给服务器带来很大的压力,并且还得反复计算。

优化地方:

  • 看看是否真的须要这么精确的计算。其实该算法已经有不少不精确的地方:

    • 直线距离多是100英里,但实际上它们之间的行走距离极可能不是这个值。
    • 若是根据邮政编码来肯定某我的所在的地区,再根据这个地区的中心位置计算他和别人的距离,这自己就是一个估算。
  • 若是不须要过高的精度,能够认为地球是圆的。要想有更多的优化,能够将三角函数的计算放到应用中,而不要在数据库中计算。

  • 看看是否真须要计算一个圆周,能够考虑直接使用一个正方形代替。边长为200英里的正方形,一个顶点到中心的距离大概是141英里,这和实际计算的100英里相差并不太远。根据正方形公式来计算弧度为0.0253(100英里)的中心到边长的距离:

    SELECT * FROM locations
      WHERE lat BETWEEN 38.03 - DEGREES(0.0253) AND 38.03 + DEGREES(0.0253)
      AND lon BETWEEN −78.48 - DEGREES(0.0253) AND −78.48 + DEGREES(0.0253);复制代码

如今看看如何用索引来优化这个查询:

  • 增长索引(lat, lon),这样作的效果并不会很好,由于使用了范围查询。
  • 使用IN()优化。

新增两个列,用来存储坐标的近似值FLOOR(),而后在查询中使用IN()将全部点的整数值都放到列表中:

mysql> ALTER TABLE locations
-> ADD lat_floor INT NOT NULL DEFAULT 0,
-> ADD lon_floor INT NOT NULL DEFAULT 0,
-> ADD KEY(lat_floor, lon_floor);复制代码

如今能够根据坐标的必定范围的近似值来搜索,这个近似值包括地板值和天花板值,地理上分别对应的是南北:

-- 查询某个范围的全部点,数值须要在应用程序中计算而不是MySQL
mysql> SELECT FLOOR( 38.03 - DEGREES(0.0253)) AS lat_lb,
-> CEILING( 38.03 + DEGREES(0.0253)) AS lat_ub,
-> FLOOR(-78.48 - DEGREES(0.0253)) AS lon_lb,
-> CEILING(-78.48 + DEGREES(0.0253)) AS lon_ub;
+--------+--------+--------+--------+
| lat_lb | lat_ub | lon_lb | lon_ub |
+--------+--------+--------+--------+
| 36     | 40     | −80    | −77    |
+--------+--------+--------+--------+
-- 生成IN()列表中的整数:
SELECT * FROM locations
  WHERE lat BETWEEN 38.03 - DEGREES(0.0253) AND 38.03 + DEGREES(0.0253)
  AND lon BETWEEN −78.48 - DEGREES(0.0253) AND −78.48 + DEGREES(0.0253)
  AND lat_floor IN(36,37,38,39,40) AND lon_floor IN(-80,-79,-78,-77);复制代码

使用近似值会让咱们的计算结果有误差,因此咱们还须要一些额外的条件过滤在正方形以外的点,这和前面使用CRC32作哈希索引相似:先建一个索引过滤出近似值,在使用精确条件匹配全部的记录并移除不知足条件的记录。

事实上,到这时就无须根据正方形的近似来过滤数据,可使用最大圆公式或者毕达哥拉斯定理来计算:

SELECT * FROM locations
  WHERE lat_floor IN(36,37,38,39,40) AND lon_floor IN(-80,-79,-78,-77)
  AND 3979 * ACOS(
  COS(RADIANS(lat)) * COS(RADIANS(38.03)) * COS(RADIANS(lon) - RADIANS(-78.48))
  + SIN(RADIANS(lat)) * SIN(RADIANS(38.03))
) <= 100;复制代码

这时计算精度再次回到使用一个精确的圆周,不过如今的作法更快。只要可以高效地过滤掉大部分的点,例如使用近似整数和索引,以后再作精确数学计算的代价并不大。只要不是使用大圆周的算法,不然速度会更慢。

该案例使用的优化策略:

  • 尽可能少作事,可能的话尽可能不作事。这个案例中就不要对全部的点计算大圆周公式;先使用简单的方案过滤大多数数据,而后再到过滤出来的更小的集合上使用复杂的公式运算。
  • 快速地完成事情。确保在设计中尽量地让查询都用上合适的索引,使用近似计算来避免复杂计算。
  • 须要的时候,尽量让应用程序完成一些计算。

4.8.3 使用用户自定义函数

  • 当SQL语句已经没法高效地完成某些任务的时候,若是须要更快的速度,那么C和C++是很好的选择。
  • 案例:须要根据两个随机的64位数字计算它们的XOR值,来看这两个数值是否匹配。大约有3500万条的记录须要在秒级中完成。
    • 通过简单的计算就知道,当前的硬件条件下,不可能在MySQL中完成。
    • 编写一个计算程序,以一个后台程序的方式运行在同一服务器上,而后编写一个用户自定义函数,经过简单的网络通讯协议和前面的程序进行交互。分布式运行该程序,能够达到在130毫秒内完成4百万次匹配计算。经过这样的方式,能够将密集型的计算放到一些通用服务器上,同时对外界彻底透明,看起来是MySQL完成了所有的工做。

4.9 总结

若是把建立高性能应用程序比做是一个环环相扣的”难题“,除了前面介绍的schema、索引和查询语句设计以外,查询优化应该是解开”难题“的最后一步。

理解查询是如何被执行的以及时间都消耗在哪些地方,这依然是前面介绍的响应时间的一部分。再加上一些诸如解析和优化过程的知识,就能够额更进一步地理解上一章讨论的MySQL如何访问表和索引的内容了。这也从另外一个维度理解MySQL在访问表和索引时查询和索引的关系。

优化一般须要三管齐下:不作、少作、快速地作。

相关文章
相关标签/搜索