• MySQL之 查询执行的基础总结


    本节内容总结自《High Performance MySQL》

    查询执行的基础

    当向MySQL发送一个请求时,MySQL做了什么
    1. 客户端发送一条查询给服务器。
    2. 服务器先检查缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一个阶段。
    3. 服务器端进行SQL解析,预处理,再由优化器生成对应的执行计划。
    4. MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询。
    5. 服务器将结果返回给客户端。
    MySQL客户端/服务器通信协议
    1. MySQL客服端和服务器之间的通信方式
      • MySQL客服端和服务器之间的通信协议是半双工的。这意味着我们无法将一个消息切分成小块独立发送。客户端用一个单独的数据包将查询传给服务器。相反服务器响应给用户的数据通常由多个数据包组成。当服务器开始响应客户端请求时,客户端必须完整的接收整个返回结果,而不能简单地只取前面的几条语句,然后让服务器暂停发送数据,因此有时在查询中加上LIMIT是必要的。
    2. 使用缓存减小服务器的压力
      • MySQL通常需要等所有的数据都已经发送给客户端才能释放这条查询所占用的资源,所以接收全部结果并缓存通常可以减少服务压力,让查询能够早点结束,早点释放相应的资源,实际上服务器端默认也是这么做的。因此当使用多数连接MySQL的库函数从MySQL获取数据时,其结果看起来都像是从MySQL服务器获取数据,实际上都是从这个库函数的缓存获取数据,但当结果集非常大时这样并不好,因为库函数会花很长时间和内存来存储所有的结果集,这种情况下可以不使用缓存来记录而直接处理查询,这样做的缺点是,对于服务器来说,需要查询完成才能释放资源。
    3. 查询状态:任何时刻的任何一个线程都有一个状态,该状态表示了MySQL当前正在做什么。最简单的方式就是使用SHOT FULLPROCESSLIST命令(该命令返回结果中的Command列就表示当前的状态)
    4. 状态分类
      1. Sleep:线程正在等待客户端发送新的请求。
      2. Query:线程正在执行查询或者正在将结果发送给客户端。
      3. Locked:在MySQL服务器层该线程正在等待表锁。在存储引擎级别实现的锁,例如InnoDB的行锁,并不会体现在线程状态中。对于MyISAM和其他没有行锁的引擎中这是一个典型的状态。
      4. Analyzing and statistics:线程正在收集存储引擎的统计信息。并生成查询的执行计划。
      5. Copying to tmp table [on disk]:线程正在执行查询,并且将其结果集都复制到一个临时表中。后面还有 on disk 标记,这表示MySQL正在将一个内存临时表放到磁盘上。这种状态一般在做GROUP BY 操作,或者文件排序操作,获取UNION操作。
      6. Sorting result:线程正在对结果集进行排序。
      7. Sending data:这表示多种情况:线程可能在多个状态间传送数据,或者正在生成结果集,或者在向客户端返回数据。
    查询缓存
    • 在解析一个查询语句之前,如果查询缓存是打开的,那么MySQL会有先检查这个查询是否命中查询缓存中的数据。这个检查是通过一个对大小写敏感的哈希查找实现的。查询和缓存中的查询即使只有一个字节不同,也不会匹配缓存结果。如果缓存命中,MySQL会检查一次用户权限。如果权限没有问题,MySQL会跳过所有其他阶段,直接从缓存拿数据结果并返回给客户端。这种情况下,查询不会被解析,不用生成执行计划,不会被执行。
    查询优化处理
    • 查询生命周期的下一步是将一个SQL转换成一个执行计划,MySQL再依照这个执行计划和存储引擎进行交互。这包括多个子阶段:解析SQL,预处理,优化SQL执行计划,这个过程中任何错误都会终止查询。
    语法解析器和预处理
    1. 语法解析器:MySQL通过关键字将SQL语句进行解析,并生成一颗对应的解析树。MySQL解析器将使用MySQL语法规则验证和解析查询。例如,他将验证是否使用错误的关键字,或者使用关键字的顺序是否正确等,再或者他还会验证表达字符串的引号是否前后正确匹配等。
    2. 预处理器:根据MySQL规则进一步检查解析树是否合法,会检查数据表和数据列是否存在,还会解析名字和别名是否有歧义。下一步预处理器会验证权限。
    查询优化器
    1. 优化器:优化器的作用就是找出一个查询所有执行计划中"最好"的执行计划。MySQL使用基于成本的优化器,他尝试预测一个查询使用某种执行计划时的成本,并选择其中最小的一个。这个成本计算是由一系列的统计信息和相对应的权重计算出来的:每个表或者索引的页面个数,索引的基数,索引和数据行的长度,索引的分布情况等,它假设读取任何数据都需要一次磁盘I/O。可以通过查询当前会话的Last_query_cost的值得知当前查询的成本。
    2. 导致优化器选择错误执行计划的原因
      1. 统计信息不准确。例如InnoDB因为其MVCC的架构,并不能维护一个数据表的行数的精确统计信息。
      2. 执行计划中的成本估算不等同于实际执行的成本。
      3. MySQL的最优可能和你想的最优不一样。
      4. MySQL从不考虑其他并发执行的查询,这可能会影响到当前查询的速度。
      5. MySQL也并不是任何时候都是基于成本优化的。有时会基于一些固定的规则,例如,如果存在全文搜索MATCH()子句,则存在全文索引的时候就使用全文索引。即使有使用使用别的索引和WHERE条件可以远比这种方式快。
      6. MySQL不会考虑不受其控制的操作的成本,例如,执行存储过程或者用户自定义函数的成本。
      7. 优化器有时候可能无法去估算所有有可能的执行计划,导致错过了最优的执行计划。
    3. MySQL能够处理的优化类型
      1. 重新定义关联表的顺序:数据表的关联并不总是按照在查询中指定的顺序进行。
      2. 将外连接转化成内连接:并不是所有的OUTER JOIN语句都必须以外连接的方式执行。例如WHERE条件,库表结构都可能会导致会让外连接等价于一个内连接。MySQL能意识到这点并重写查询,让其可以调整关联顺序。
      3. 使用等价变换规则:MySQL可以使用一些等价变化来简化并规范表达式。它可以合并和减少一些比较,还可以移除一些恒成立和一些恒不成立的判断。
      4. 优化COUNT(),MIN()和MAX():索引和列是否可以为空通常可以帮助MySQL优化这类表达式。例如要找到一列的最小值,只需要查询对应索引的最左端的记录。优化器在生成执行计划的时候就可以利用这一点,在B-Tree索引中将这个表达式作为一个常数对待,最大值也类似于上述最小值。如果MySQL使用了这种优化可以在执行计划中看到"Select tables optimized away"。类似的,没有任何WHERE条件的COUNT(*)查询通常也会使用存储引擎提供的一些优化(例如,MyISAM维护了一个变量来存放数据表的行数)
      5. 预估并转化为常数表达式:当MySQL检测到一个表达式可以转换成常数时,就会一直把该表达式作为一个常数进行优化处理。
        1. 例如一个用户自定义变量在查询中没有发生变化的时候就可以转换为一个常数。
        2. 数学表达式是一个常数时。
        3. 有时候甚至一个查询也可以转换为一个常数。例如在索引列上执行MIN()函数等。
        4. 甚至是主键或者唯一键查找语句在WHERE语句中指定了常数条件之后,MySQL在查询开始阶段就先查找到这些值,这样优化器就会将其转换为常数表达式。
      6. 覆盖索引扫描:当索引中的列包含所有查询中需要使用的列的时候,MySQL就可以使用索引返回需要的数据,而无需回表查询对应的数据行。
      7. 提前终止查询:在发现已经满足查询需求的时候,MySQL总是能够立刻终止查询。下面是一些典型的例子。
        1. 当使用LIMIT子句的时候。
        2. 当MySQL发现了一个不成立的条件时,会立刻返回一个空结果。
        3. 当需要检索"不同取值"或"判断存在性"时,MySQL执行过程中如果发现某些特殊的条件以致可以得出答案时会提前终止查询。
      8. 等值传播:如果两个列的值通过等式关联,那么MySQL能够把其中一个列的WHERE条件传递到另一个列上。
      9. 列表IN()的比较:MySQL将IN()列表中的数据先进行排序,然后通过二分查找的方式来确定列表中的值是否满足条件。
      10. 如果能够确认优化器给出的不是最佳选择,并且清楚背后的原理,那么也可以在查询中添加hint提示来影响优化器的选择,也可以重写查询,或者重新设计库表结构和索引。
    数据和索引的统计信息
    • 统计信息由存储引擎实现。MySQL查询优化器在生成查询的执行计划时,需要向存储引擎获取相应的统计信息。存储引擎则提供给优化器对应的统计信息,包括:每个表或者索引有多少个页面,每个表的每个索引的基数是多少,数据行和索引长度,索引的分布信息等。优化器根据这些信息选择一个最优的执行计划。
    MySQL如何执行关联查询
    1. MySQL中可以认为任何查询都是一次关联查询:MySQL对所有类型的查询都以同样的方式(嵌套循环)进行。例如,MySQL在FROM子句中遇到子查询时,先执行子查询,并将子查询结果放到一个临时表中,然后将这个临时表当作一个普通表对待,执行嵌套循环查询。MySQL遇到右外连接的时候,会将其改写成等价的左外连接。MySQL不支持全外连接,因为全外连接无法通过嵌套循环和回溯的方式完成查询。
    2. 嵌套循环查询:MySQL对任何关联都执行嵌套循环关联操作,即MySQL先在一个表中循环取出单条数据,然后再嵌套循环到下一个表中寻找匹配的行,直到找到所有的表中匹配的行位置。然后根据各个表匹配的行,返回查询中需要的各个列。
    执行计划
    • MySQL并不会生成查询字节码来执行查询。MySQL生成查询的一颗指令树,然后通过存储引擎执行来完成这颗指令树并返回结果。最终的执行计划中包含了重构查询的全部信息。如果对某个查询执行EXPLAIN EXTENDED后,再执行SHOW WARNINGS,就可以看到重构出的查询。
    • 在MySQL中任何多表查询都可以用一颗左侧深度优先的树表示,MySQL总是从一个表开始一直嵌套循环,回溯完成所有的表关联。
    关联查询优化器
    • 关联查询优化器决定了多表关联时的顺序。关联查询优化器通过评估不同顺序时的成本来选择一个代价最小的关联顺序。
    • 如果可能,优化器会遍历每一个表然后逐个做嵌套循环计算每一颗树可能的执行计划树的成本,然后返回一个最优的计划。如果关联的表超过optimizer_search_depth个时,优化器就会选择使用贪婪搜索模式来查找最优的关联顺序。
    • 关联查询优化器总会选择使用小表来驱动大表,这样的关联顺序会让查询进行更少的嵌套循环和回溯操作。也可以使用STRAIGHT JOIN关键字重写查询,让优化器按照你认为的最优的关联顺序执行。
    排序优化
    1. MySQL的排序规则:当不能使用索引生成排序结果的时候,MySQL需要自己进行排序,如果数据量小则在内存中进行,如果数据量大则需要使用磁盘,MySQL将这个过程统一称为文件排序。如果排序的数据量小于排序缓冲区,MySQL使用内存进行快速排序操作。如果内存不够排序,那么MySQL会将数据分块,对每个单独的块使用快速排序进行排序,并将各个块的排序结果放在磁盘上,然后将各个排好序的块进行合并,最后返回排序结果。
    2. MySQL中的两种排序算法:在MySQL中,当查询所需要列的总长度不超过参数max_length_for_sort_data时,MySQL使用单次传输排序,可以通过调整这个参数来影响MySQL排序算法的选择。
      1. 两次传输排序:读取行指针和需要排序的字段,对其进行排序,然后再根据排序结果读取所需要的数据行。这个操作需要从数据表中读取两次数据,尤其是第二次读取数据的时候,因为是读取排序列进行排序后的所有记录,这会产生大量的随机I/O,所以两次数据传输的成本非常高。不过这样做的优点是,在排序的时候存储了尽量少的数据,这就让排序缓冲区中容纳尽可能多的行数进行排序。
      2. 单次传输排序:先读取查询所需要的所有列,然后再根据给定的列进行排序,最后直接返回排序结果。对于I/O密集型的应用,这样做的效率高了很多。这个算法只需要一次顺序I/O读取所有的数据,而不需要任何的随机I/O。缺点是需要返回的列非常大,会占用大量额外的空间。
    3. MySQL中文件排序占用的内存:MySQL在进行文件排序时需要使用的临时存储空间可能会比想象的要大很多。原因在于MySQL在排序时,对每一个排序记录都会分配一个足够长的定长空间来存放。这个定长空间必须足以容纳其中最长的字符串,例如VARCHAR列则需要分配其完整长度。如果使用UTF-8字符集,MySQL将会为每一个字符预留三个字节。
    4. 关联查询中的排序:MySQL会分两种情况来处理这样的文件排序。如果ORDER BY子句中的所有列都来自关联的第一个表,那么MySQL在关联处理第一个表的时候就进行文件排序(此时可以在执行计划的Extra列中看到Using filesort)。否则,MySQL都会先将关联的结果存放到一个临时表中,然后在所有的关联都结束后,再进行文件排序操作(此时可以看到Using temporary;Using filesort)。
    查询执行引擎
    • MySQL的执行计划是一个数据结构,MySQL的查询执行引擎则根据这个执行计划来完成整个查询。MySQL只是简单的根据执行计划给出的指令逐步执行。有大量的操作需要通过调用存储引擎实现的接口来完成。MySQL在优化阶段就为每个表出创建了一个handler实例,优化器根据这些实例的接口可以获取表的相关信息,包括表的所有列名,索引统计信息等。并不是所有的操作都由handler完成。例如MySQL需要进行表锁的时候,handler可能会实现自己的级别的,更细粒度的锁,但这不能代替服务器层的表锁。如果是所有存储引擎共有的特性则由服务器层实现,比如时间和日期函数,视图,触发器等。
    返回结果给客户端
    • MySQL将结果集返回给客户端是一个增量,逐步返回的过程。当MySQL开始生成第一条结果时,MySQL就可以开始向客户端逐步返回结果集了。这样做有两个好处:①:服务器端无需存储太多的结果,也就不会因为要返回太多结果而消耗太多内存。②:这样的处理也让MySQL客户端第一时间获取返回的结果。
    • 结果集中的每一行都会以一个满足MySQL客户端/服务器通信协议的封包发送,再通过TCP协议进行传输,在TCP传输过程中,可能对MySQL的封包进行缓存然后批量传输。
    时间并不会因为你的迷茫和迟疑而停留,就在你看这篇文章的同时,不知道有多少人在冥思苦想,在为算法废寝忘食,不知道有多少人在狂热地拍着代码,不知道又有多少提交一遍又一遍地刷新着OJ的status页面…… 没有谁生来就是神牛,而千里之行,始于足下!
  • 相关阅读:
    ubuntu下安装oracle
    网站框架策划时的小技巧--页面原型篇
    中国电商价格欺诈何时休?
    系统升级日记(4):如何快速的修改Infopath中的各种URL
    系统升级日记(3)- 升级SharePoint解决方案和Infopath
    系统升级日记(2)- 升级到SharePoint Server 2013
    系统升级日记(1)- 升级到SQL Server 2012
    【译】《C# Tips -- Write Better C#》
    [.NET] 一步步打造一个简单的 MVC 电商网站
    反骨仔的 2016 年度全文目录索引
  • 原文地址:https://www.cnblogs.com/bianjunting/p/14361384.html
Copyright © 2020-2023  润新知