zoukankan      html  css  js  c++  java
  • 高性能MySql学习笔记第六章:查询性能优化

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

    • 是否向数据库请求了不需要的数据

      • 有些查询会请求超过实际需要的数据,这会给MySQL服务器带来额外的负担,并增加网络开销,也会消耗应用服务器的CPU和内存资源。有一些典型的案例:
        1. 查询不需要的记录:一个常见的错误是误以为MySQL只返回了需要的数据,实际上是返回了全部的数据后再进行计算的一些查询。最简单有效的方法是加上LIMIT
        2. 多表关联时返回全部列
        3. 总是取出全部列:取出全部列,会让优化器无法完成索引覆盖扫描这类优化,还会带来额外的I/O、内存和CPU消耗。但是获取并缓存所有的列的查询,相比多个独立的只获取部分列的查询可能就更有好处。
        4. 重复查询相同的数据:不断执行相同的查询,但是每次都返回相同的数据。
    • MySQL是否在扫描额外的记录

      • 对于MySQL,最简单的衡量查询开销的三个指标是:响应时间、扫描的行数、返回的行数
      • EXPLAIN 语句中的type列反应了访问类型。访问类型有很多种,详见下表的解释。type类型从下到上逐渐变好,使用的索引至少要达到range级别。
        type类型 解释
        null --
        system const的特例,仅返回一条数据的时候。
        const 查找主键索引,返回的数据至多一条(0或者1条)。 属于精确查找
        eq_ref 查找唯一性索引,返回的数据至多一条。属于精确查找
        ref 查找非唯一性索引,返回匹配某一条件的多条数据。属于精确查找、数据返回可能是多条
        range 查找某个索引的部分索引,一般在where子句中使用 < 、>、in、between等关键词。只检索给定范围的行,属于范围查找
        index 查找所有的索引树,比ALL要快的多,因为索引文件要比数据文件小的多。
        ALL 不使用任何索引,进行全表扫描,性能最差。
      • 一般MySQL使用如下三种方式应用WHERE条件,从好到坏依次为:
        1. 在索引中使用WHERE条件来过滤不匹配的记录。这是在存储引擎层完成的。
        2. 使用索引覆盖扫描(在Extra列中出现了Using index),直接从索引中过滤不需要的记录并返回命中的结果。这是MySQL服务器层完成的,但是不需要回表查询记录。
        3. 从数据表中返回数据,然后过滤不满足条件的记录(在Extra列中出现了Using where)。这在MySQL服务器层中完成。
      • 如果发现查询需要扫描大量的数据但是只返回少数的行,可以通过尝试下面的技巧去优化:
        1. 使用索引覆盖扫描,把所有需要用到的列都放到索引中。
        2. 改写库表结构,例如使用单独的汇总表。
        3. 重写复杂的查询。

    2. 重构查询的方式

    • 一个复杂查询还是多个简单查询
      • 一般而言,如果一个查询能够胜任时还写成多个独立查询是不明智的。
    • 切分查询
      • 切分查询是将大量的数据以行为单位切分成小查询。比如删除数据的情况,如果用一个大的语句一次性删除的话,可能会锁住很多数据,阻塞很多查询。这时我们可以采用分批删除,将一个大的DELETE语句切分成多个较小的查询。
    • 分解关联查询
      • 很多应用会将关联查询分解为多个单次单表的查询语句,然后将结果在应用程序中关联。这样虽然需要多次访问,但是有以下的一些优势:
        1. 让缓存的效率更高。
        2. 执行单个查询可以减小锁的竞争。
        3. 在应用层做数据的关联,可以更容易对数据库进行拆分,做到高性能和可拓展。
        4. 可以减小冗余记录的查询。

    3. 查询执行的基础

    • MySQL服务端/客户端通信协议

      • MySQL服务器和客户端之间的通信是半双工的,也就是同一时刻,要么是服务器正在向客户端发送数据,要么是客户端正在向服务器发送数据。
      • 大多数MySQL的库函数在获取数据时,都是将全部结果获取后缓存到内存中,再供应用服务获取。
      • 查询状态:可以使用SHOW FULL PROCESSLIST 命令查询所有线程当前的状态。如果是root帐号,能看到所有用户的当前连接,如果是其他普通帐号,则只能看到自己占用的连接。SHOW PROCESSLIST可以列出当前100条。该命令返回结果中的Command列表示了线程当前的状态,关于该列值的函数可详见MySQL官方文档。书中简单列了如下几个:
        1. Sleep。线程正在等待客户端发送新的请求。
        2. Query。线程正在执行查询或将结果发送给客户端。
        3. Locked。在MySQL服务器层,该线程正在等待表锁。在存储引擎级别实现的锁,例如InnoDB的行锁,并不会体现在线程状态中。
        4. Analyzing and statistics。 正在收集存储引擎统计信息,生成查询计划。
        5. Sorting Data。正在对结果集排序。
        6. Copying to tmp table [on disk]。正在执行查询,并将结果集复制到一个临时表中。[on disk]表示正在将一个内存临时表放到磁盘上。
    • 查询缓存

      • 如果打开了查询缓存,则MySQL在每次查询前会优先检查这个查询是否名字查询缓存中的数据。这个检查是通过一个对大小写敏感的哈希查找实现的。查询和缓存中的查询即使只有一个字节不同,那也不会匹配查询缓存结果。
    • 查询优化处理

      • 语法解析器和预处理

        1. 语法解析器通过关键字将SQL语句进行解析,生成一颗对应的“解析树”,并校验语法规则。
        2. 预处理器进一步验证数据表和数据列等是否存在,并验证权限等。
      • 查询优化器

        • 一条查询可以有很多种执行方式,最后都返回相同的结果。优化器的作用就是找到这其中最好的执行计划。

        • MySQL优化器是基于成本的优化器,可以通过查询当前会话的Last_query_cost的值来得知MySQL计算的当前查询的成本。

        • 有一些原因会导致MySQL优化器选择错误的执行计划,比如:统计信息不准确,不考虑其他并发的查询,估算的成本不等同于实际执行的成本等。

        • MySQL的优化策略可分为两种,一种是静态优化,一种是动态优化。动态优化与查询的上下文有关,比如WHERE条件中的取值,索引条目等。动态优化每次执行时都会重新评估。

        • 如下为MySQL优化器的一些优化类型

          1. 重新定义关联表的顺序。
          2. 将外连接转换为内连接。
          3. 使用等价变换规则。如将WHERE条件中的(5 = 5 AND a > 5)改写为a > 5
          4. 优化COUNT()、MIN()、MAX()。对于MIN/MAX可以根据索引直接找到最左端/右端的记录。对于全表的COUNT(*)则可以利用存储引擎中的一些统计信息。
          5. 预估并转化为常数表达式。
          6. 覆盖索引扫描。
          7. 子查询优化。减小子查询访问的数量。
          8. 提取终止查询。如使用LIMIT子句。
          9. 列表IN()的比较。IN()并不完全等同与多个OR条件的子句。MySQL会将IN()列表中的数据先进行排序,然后通过二分查找的方式确定是否满足条件,这是一个更低复杂度的操作。
      • 数据和索引的统计信息

        • MySQL查询优化器位于服务器层。但是优化器依赖的统计信息是由存储引擎提供的。
      • 排序优化

        • 排序是一个成本很高的操作,应尽可能避免排序。当不能使用索引生成排序结果时,MySQL服务器需要自行排序,如果数据量小则在内存中进行,数据量大则需要使用磁盘。但是MySQL将这个过程统一称为文件排序(filesort),即使是完全内存排序,没有用到磁盘文件。
        • MySQL有两种排序算法:两次传输排序单次传输排序
          1. 两次传输排序是在旧版本使用的,其首先读取行指针和排序字段,对其进行排序后,再根据行指针读取所需数据。这样做的优点是内存可以容纳更多的行数进行排序。缺点则是,根据排序后的结果读取数据行,可能产生大量的随机IO。
          2. 单次传输排序是再新版本使用的,其先查询所有需要的列,再根据指定列排序后,直接返回排序结果。这样做的优点是数据读取是顺序IO,且只需要读取一次。缺点是读取出来的数据可能非常大,将在排序时占用更多的空间。
    • 查询执行引擎

      • 在解析和优化阶段,MySQL将生成查询对应的执行计划,交由执行引擎执行。这里的执行计划是一个数据结构,而不是字节码。

    4. MySQL查询优化器的局限性

    • 关联子查询

      • MySQL对于WHERE条件中包含IN的子查询语句表现往往比较糟糕。
    • UNION的限制

      • MySQL无法将限制条件从外层下推到内层。比如UNION查询,如果希望各个子句能够根据LIMIT只取部分结果集,或者能够先排好序的话,只能在UNION的各个子句中分别使用这些条件。比如如下这个查询,想将两个子查询的结果合并后取10条。MySQL 的做法是两个子查询的所有结果放到一个临时表,再取前10条记录。

        这条查询会将snapshots 表的2000条记录和 changes 表的1000条记录都放到一个临时表中,再取前10条。这样的性能将会比较差。一种优化的方式是将每一个子查询都加上LIMIT 10的条件,这样临时表中就只会有20条记录。如下所示:
    • 并行执行

      • MySQL无法利用多核特性来并行执行查询。
    • 松散索引扫描

      • MySQL不支持松散索引扫描。
      • 关于松散索引扫描,可以这样理解。假设我们有一个联合索引(a, b),有如下的查询:SELECT * FROM tbl WHERE b = 3;。由于查询中只指定了字段b,根据联合索引的最左匹配原则,MySQL无法使用这个索引,最终通过全表扫描的方式得到结果。但是按照索引的物理结构,其实一个更快的查询方式是,先扫描a列第一个值中对应b列的数据,以此类推扫描a列剩余值中对应b列的数据。这样避免了全表扫描,可以利用索引加快寻找b列的速度。这种优化的方式就称为松散索引扫描。(但是MySQL默认不支持)
      • 有一些特殊的方式可以在 MySQL 中使用松散索引扫描。比如在查询语句中添加左侧索引列的ORDER BY 或者 GROUP BY 条件。这使得MySQL优化器会因此选择索引。比如上述语句可重写为:SELECT * FROM tbl WHERE b = 3 ORDER BY a;或:SELECT * FROM tbl WHERE b = 3 GROUP BY a;

    5. 查询优化器的提示

    如果对优化器选择的执行计划不满意,可以使用优化器提供的提示来控制最终的执行计划。

    • HIGH PRIORITYLOW PRIORITY

      • 这个提示告诉 MySQL,当多个语句同时访问某一个的时候,哪些语句优先级高,哪些语句优先级低。这两个提示只对使用表锁的存储引擎有效。HIGH PRIORITY将语句调度到所有正在等待表锁的语句之前,LOW PRIORITY则相反。
    • DELAYED

      • 这个提示对于INSERTREPLACE有效。MySQL 会将使用该提示的语句立即返回给客户端,并将写入的数据放入缓冲区,待表空闲时批量将数据写入。日志系统,或是其他需要写入大量数据但是客户端不需要等待单条语句完成 IO 的应用很适合使用这样的提示。
    • STRAIGHT_JOIN

      • 这个提示可以放在SELECT 语句的SELECT关键字之后或任何两个关联表的名字之间。第一个用法是让查询中所有的表按照语句中出现的顺序进行关联。第二种则是固定其前后两个表的关联顺序。当明确某个关联顺序始终是最佳时,可以采取这个提示。
    • SQL_SMALL_RESULTSQL_BIG_RESULT

      • 这两个提示只对 SELECT 语句有效。其告诉优化器对GROUP BY 或者DISTINCT查询如何使用临时表排序。SQL_SMALL_RESULT表示结果集很小,可以使用内存排序。SQL_BIG_RESULT 表示结果集很大,建议使用磁盘做排序。
    • SQL_BUFFER_RESULT

      • 这个提示是告诉优化器将查询结果放入一个临时表,然后尽快释放表锁。
    • FOR UPDATELOCK IN SHARE MODE

      • 这两个不是真正的优化器提示。这两个主要控制SELECT语句的锁机制,但只对实现了行级锁的存储引擎有效。这两个提示会对符合查询条件的数据行加锁。唯一内置的支持这两个提示的引擎是 InnoDB
    • USE INDEXIGNORE INDEXFORCE INDEX

      • 这几个提示会告诉优化器使用或不使用哪些索引来查询记录。USE INDEXFORCE INDEX基本相同,但是`FORCE INDEX``会告诉优化器全表扫描的成本远远高于索引扫描。

    6. 优化特定类型的查询

    • COUNT()查询

      • COUNT()可以统计某个列值的数量,也可以统计行数。
      • *MYISAM 只对没有任何条件的COUNT(*)才有优化,其他情况和其他存储引擎没有不同。
      • 通常来说,COUNT()都需要扫描大量的行才能获得精确的结果,因此是非常难优化的。除了前面的方法,在 MySQL 层面还能做的就只有覆盖索引扫描了。如果还不够,就需要从应用架构上考虑,比如增加汇总表。
    • LIMIT分页

      • LIMIT查询一个非常大的问题是,当偏移量很大的时候,比如LIMIT 10000, 100这样的查询,MySQL 需要查询10100条记录,然后只返回最后100条,而前面的10000条数据都将被抛弃,这样的代价非常高。
      • 一种优化的方式是将 LIMIT 转换为已知位置的查询,比如:SELECT * FROM tbl WHERE pos > 10000 ORDER BY pos ASC LIMIT 100。一般情况下也可以使用自增主键列替代这里的 pos 列,但是要注意有索引可以使用。
    • UNION查询

      • MySQL总是通过创建并填充临时表的方式来执行UNION查询。因此很多优化策略在UNION查询中无法很好的使用,常常需要手工将WHERE、LIMIT、ORDER BY等子句"下推到"UNION``各个子查询中,以便优化器可以充分利用这些条件优化。
      • 除非确实需要服务器消除重复的行,否则一定要使用UNION ALL。如果没有ALL关键字,MySQL 会给临时表加上DISTINCT选项,进而导致对整个临时表做唯一性检查,这样的代价非常高。
    • 使用用户自定义变量

      • 用户自定义变量的一些限制
        1. 使用自定义变量的查询,无法使用查询缓存。
        2. 不能在使用常量或者标识符的地方使用自定义变量,比如:表名、列名和 LIMIT 子句中。
        3. 用户自定义的变量的生命周期是在一个链接中有效。如果使用连接池或者持久化链接,自定义变量有可能导致 BUG。
        4. MySQL 优化器在某些场景下可能会将这些变量优化掉。
      • 统计更新和插入的数量
        • 当使用了INSERT ON DUPLICATE KEY UPDATE时,如果想知道插入了多少行数据,有多少数据是因为冲突而改写成更新操作的。可以利用自定义变量实现:INSERT INTO t1(c1, c2) VALUES(4, 4), (2, 1), (3, 1) ON DUPLICATE KEY UPDATE c1 = VALUES(c1) + (0 * ( @x := @x + 1) );。由于每次冲突会导致更新时对变量@x 自增一次。然后通过对这个表达式乘以0来让其不影响要更新的内容,由此可以得知因为冲突而改写成更新操作的记录数量。

    本章有较大篇幅介绍了关联查询和子查询一类的优化。但是在我个人的工作实践中,关联查询是在数据库查询中绝对不可能采用的大忌,故而记录时对该部分没有过多重视

  • 相关阅读:
    volley框架使用
    Insert Interval
    candy(贪心)
    Best Time to Buy and Sell Stock
    Best Time to Buy and Sell Stock III
    distinct subsequences
    edit distance(编辑距离,两个字符串之间相似性的问题)
    trapping rain water
    word break II(单词切分)
    sudoku solver(数独)
  • 原文地址:https://www.cnblogs.com/lianggx6/p/15518919.html
Copyright © 2011-2022 走看看