一、整体架构

1.1层次图(简图)

第一层:连接层

包含了服务于C/S程序或者是这些程序所需要的服务,如:连接处理、身份验证、安全性等等。

第二层:核心服务层(SQLLayer)

MySQL的核心部分,在MySQL处理底层数据之前的所有工作都是在这一层完成的,包括查询缓存、权限判断、SQL解析、执行计划优化以及提供所有内置的函数(如日期、时间、数学运算、加密)等等。各个存储引擎提供的功能都集中在这一层,如存储过程、触发器、视图等。

第三层:存储引擎层(StorageEngineLayer)

底层数据存取操作实现部分,由多种存储引擎共同组成。它们负责存储和获取所有存储在MySQL中的数据,每个存储引擎都有自己的特点。服务器通过存储引擎API与引擎交互,这个API接口隐藏了各个存储引擎不同的地方,将各个引擎的功能进行统一封装,同时对查询层尽可能的透明。存储引擎不解析SQL,互相之间也不能通信。仅仅是简单的响应服务器的请求。

第四层:数据存储层

主要是将数据存储在运行于设备的文件系统之上,并完成与存储引擎的交互。

1.2架构图(完整结构图)

1.Connectors

不同语言中与MySQL服务端进行交互的客户端,如php、java、python等。各种语言有自己的实现。

2.ManagementServeices&Utilities:

各类系统管理和控制工具,如配置相关服务、集群相关服务、数据迁移工具、备份及恢复工具、服务端资源管理工具、通用日志模块等。

3.ConnectionPool

管理服务端的连接、会话、线程等需要缓存的重型资源。连接池负责监听对MySQL服务端的各种请求,每当接收到连接请求,会进行身份验证、连接安全检查。如验证通过,便将其转发到线程管理模块。线程管理模块会为每一个连接请求分配(或创建)一个连接线程为其单独服务。而连接线程的主要工作就是负责MySQL服务端与客户端的通信、接受客户端的命令请求、传递服务端的结果信息等。线程管理模块还要负责管理维护这些连接线程,包括线程的创建、线程的缓存等。

4.SQLInterface

执行器,接收客户端的SQL,并且返回需要查询的结果。提供包括执行DDL、DML、存储过程、视图、触发器之类的功能。

5.Parser

SQL传递到解析器的时候会被解析器验证和解析。

主要功能: a.将SQL语句进行词法、语法、语义分析,分解成语法树,然后按照不同的操作类型进行分发,以后SQL语句的传递和处理就是基于这个结构的。 b.如果在解析中遇到错误,中断流程并返回错误信息

6.Optimizer

SQL语句在查询之前会使用查询优化器对查询进行优化。结合客户端请求的SQL语句和数据库的统计信息,分析得出一个最优的执行计划,告诉后面的流程如何取得这个SQL语句的结果。

7.Cache&Buffer

查询缓存:将客户端提交给MySQL的Select请求的返回结果集缓存到内存中,与该请求的hash值做映射。该请求的数据来源表发生任何数据变化后,MySQL会自动使该请求的的查询缓存失效。在读多写少时,查询缓存对性能的提高是非常显著的,当然内存消耗也很大。如果查询缓存有命中的查询结果,会直接中断流程返回给客户端,节省大量时间。这个缓存机制是由一系列小缓存组成的。比如表缓存,记录缓存,字段缓存,权限缓存等。

8.存储引擎接口

封装各类存储引擎,将数据处理的特殊实现封装为统一的接口对查询层开放,也提供了一系列标准的管理和服务支持,这些标准与存储引擎本身无关。借助它MySQL可以实现其底层数据存储引擎的插件式管理。这个模块实际上只是一个抽象类,但正是因为它成功地将各种数据处理高度抽象化,才成就了今天MySQL可插拔存储引擎的特色。注意:存储引擎是基于表的,而不是数据库。

二、查询执行流程

2.1流程图

MySQL整个查询执行过程,分为6个大步骤,如上图:连接建立、查询缓存、SQL解析、优化SQL查询、调用引擎,返回结果

  1. 连接建立:客户端向MySQL服务器发送一条查询请求,与Connectors交互,进行身份认证、连接分配等操作。如验证通过,会暂时存放在连接池中并由ManagementServeices&Utilities管理。当该请求从等待队列进入到处理队列,管理器会将该请求传递给SQLInterface

  2. 查询缓存:SQLInterface接收到请求后,首先对请求进行hash处理并检查查询缓存,如果命中缓存,则立刻返回存储在缓存中的结果,否则进入后续流程,传递给Parser

  3. SQL解析:Parser对SQL进行解析(词法、语法、语义),将SQL转化为语法树,并传递给预处理器

  4. SQL预处理:预处理器会进行表/字段存在性验证、权限验证等操作,完成后传递给Optimizer

  5. SQL优化:Optimizer结合语法树的语义和目标数据库统计信息生成MySQL认为的最佳执行计划

  6. SQL执行:MySQL根据执行计划,调用存储引擎的API来执行查询

  7. 返回结果:将结果返回给客户端,同时如为select请求则缓存查询结果

2.2各流程细节

2.2.1连接建立

具体步骤:

客户端建立与MySQL服务端的连接,是由Connectors来完成的。Connectors负责跟客户端建立连接、获取权限、维持和管理连接。每当接收到一个新的客户端连接请求,Connectors都会基于用户名、主机信息和密码先对其进行身份验证。用户身份验证通过后,Connectors还会到系统权限表里查出该用户拥有的权限,之后这个连接的权限判断都将依赖于此时读到的权限,一个用户成功建立连接后,即使管理员对这个用户的权限做了修改,也不会影响已经存在连接的权限,修改完后,只有再新建的连接才会使用新的权限设置。连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,Connectors会负责维持连接,直到达到由wait_timeout参数设置的连接超时上限时间。每个客户端连接成功后,都会在服务端进程中拥有一个线程,该连接的查询只会在这个单独的线程中执行,同时每个线程服从系统调度,只能轮流使用CPU。服务器会负责维护线程,始终会保持一定数量的线程,避免每次新建/断开连接时都需要重新新建/销毁线程。

MySQL客户端/服务端通信方式:

MySQL客户端/服务端通信协议是半双工的,即在任一时刻,要么是服务器向客户端发送数据,要么是客户端向服务器发送数据,这两个动作不能同时发生。半双工的优势在于使得通信方式变得更简单高效。但缺陷在于无法进行流量控制,一旦一端开始发送消息,另一端必须要接收完整个消息才能响应它,无法将一个消息切成小块独立发送。

客户端用一个单独的数据包将查询请求发送给服务器,所以当查询语句很长的时候,需要调整max_allowed_packet参数,默认4M,避免因查询过大,服务端拒绝接收更多数据并抛出异常。

而服务器响应给用户的数据通常会很多,由多个数据包组成。但是当服务器响应客户端请求时,客户端必须完整的接收整个返回结果,而不能简单的只取前面几条结果,然后让服务器停止发送。因而在实际开发中,尽量保持查询简单且只返回必需的数据,减小通信间数据包的大小和数量,避免客户端因接收过多的无效数据长时间卡顿,这也是查询中尽量避免使用SELECT*以及加上LIMIT限制的原因之一。

长连接和短连接:

长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接;短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。

建立连接的过程通常是比较复杂的,建议在使用中要尽量减少建立连接的动作,尽量使用长连接。但全部使用长连接后,MySQL内存占用会涨得特别快,这是因为MySQL在执行过程中临时使用的内存是管理在连接对象里的。这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能会导致OOM,被系统强行杀掉,MySQL异常重启。通常会使用两个办法解决长连接内存占用问题:

  1. 定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。

  2. MySQL5.7以上版本,可以在每次执行一个比较大的操作后,通过执行mysql_reset_connection来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

2.2.2查询缓存

具体步骤:

在正式解析一个查询语句前,如果开启了查询缓存,MySQL会先检查查询缓存中是否包含这个查询。如果存在,会在检查用户权限通过后直接返回缓存中的结果,以此提升查询速度。查询缓存是一个引用表,类似于Dict或HashMap,通过一个哈希值(这个哈希值结合查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息计算得来)作为key,查询结果作为value存储在内存中。所以两个查询在任何字符上的不同(例如:空格、注释、大小写),都会导致缓存不会命中。同时如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL库中的系统表,其查询结果都不会被缓存,因为无法预测函数的执行结果是否为幂等的,比如NOW()、CONNECION_ID()。而且查询缓存仅服务于查询语句,非查询语句不会走查询缓存,也不会存储结果。

MySQL查询缓存系统会跟踪查询中涉及的每个表,如果这些表在数据或结构上发生变化,那么和这张表相关的所有缓存数据都将失效。如果查询缓存非常大或者碎片很多,这个操作就可能带来很大的系统开销,甚至导致系统僵死,而且查询缓存对系统的额外消耗也不仅仅在写操作,读操作也不例外:

  1. 任何的查询语句在开始之前都必须经过检查,即使这条SQL语句永远不会命中缓存

  2. 如果查询结果可以被缓存,那么执行完成后,会将结果存入缓存

  3. 两个SQL语句,只要相差哪怕是一个字符,那么两个SQL都会写入到查询缓存,且value一样。

因此并不是什么情况下查询缓存都会提高系统性能,缓存创建和失效都会带来额外消耗,特别是写密集型应用,只有当缓存带来的资源节约大于其本身消耗的资源时,才会给系统带来性能提升。可以尝试打开查询缓存,并在数据库设计上做一些优化:

  1. 用多个小表代替一个大表,注意不要过度设计,避免缓存结果过大。

  2. 批量插入代替循环单条插入,避免查询缓存频繁失效。

  3. 合理控制缓存空间大小,一般来说其大小设置为几十M比较合适,避免系统内存消耗过快。

  4. 可以通过SQL_CACHE和SQL_NO_CACHE来手动控制某个查询语句是否需要进行缓存。如某些需要实时数据、运行频率很低的查询可以加上SQL_NO_CACHE

  5. 可以将查询缓存设为DENAND类型,只缓存select语句中通过SQL_CACHE指定需要缓存的查询。

最终MySQL8.0版本移除了查询缓存:

  1. 查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。清空后再查询又要重新构建缓存

  2. 普遍情况下,查询缓存的命中率非常低,其资源消耗和收益不成正比

  3. 查找查询缓存和将结果存入到查询缓存都需要额外的性能消耗

2.2.3SQL解析

词法分析:

MySQL会以空格来对SQL语句进行分词,识别出其中包含的关键字和非关键字,然后校验这些词语的正确性。如果发现错误,则中断流程并返回错误信息。

语法分析:

根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这SQL语句是否满足MySQL语法,比如是否包含必要的关键字、关键字的顺序是否正确、运算符是否使用正确等。如果发现错误,则中断流程并返回错误信息。

语义分析:

通过语法分析后,MySQL会对SQL语句进行语义分析,了解用户希望在哪个库、哪张表、哪些字段上执行查询、有哪些查询条件、是否需要分组去重排序、需要返回几条结果等操作。最终SQL语句会被转化为MySQL定义的语法树。如果发现错误,则中断流程并返回错误信息。

2.2.4SQL预处理

预处理器在接收到语法树后,会先根据连接中存储的用户权限进行检查、解析名字和别名、检查列引用没有歧义、校验目标库、表、字段是否存在以及当前连接是否具有目标库、表、字段的对应操作权限。如果发现错误,则中断流程并返回错误信息。

2.2.5SQL优化

具体步骤:

优化器会将语法树转化成查询计划。一条查询往往有很多种执行方式,最后都返回相应的结果。优化器的作用就是找到这其中最好的执行计划。MySQL使用基于成本的优化器,它会尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的执行计划。由于生成执行计划的过程会消耗较多的时间,优化器还会缓存语句的执行计划,当相似的语句再次进入优化器时,就可以直接使用已缓存的执行计划,从而跳过本次生成执行计划的整个过程,提高执行速度。

查询成本是指:要做多少页的随机查询才能得到最后的查询结果,MySQL会根据一些列的统计信息计算得到,这些统计信息包括:每张表或者索引的页面个数、索引的条数、索引和数据行的长度、索引的分布情况等。

静态优化和动态优化:

查询优化器是非常复杂的,它使用了很多优化方式将查询语句转换成为一个查询执行计划。总体来说有两种优化方式:静态优化和动态优化。

静态优化可以简单地通过检查语法树进行。如将表达式转化为常量值。执行一次后会一直有效。可以理解成“编译时优化”。

动态优化是基于具体的情景的,并依赖于多种因素。如WHERE条件中的值或索引中对应的数据行数。这个过程在每次查询都需要重新估计,可以理解为是“运行时优化”。

优化器可能执行的优化策略:

  1. 在表里面有多个索引的时候,决定使用哪个索引

  2. 联合查询时重新定义表的关联顺序:如多张表关联查询时,并不一定按照SQL中指定的顺序进行。如果关联表太多,则无法搜完所有结果,因为算法复杂度为O(n!)。此时MySQL会采用贪心算法。是否使用贪心算法的边界值可以根据optimizer_search_depth去指定。也可以通过STRAIGHT_JOIN手动指定关联顺序

  3. 外连接可能会变为内连接:如果WHERE条件和数据表结构可以将外连接查询等价于内连接,MySQL可以识别这些情况,并重写联合查询。

  4. 等效传递:表关联时,如果WHERE条件同时适用于左右表,会把左表的查询条件拿给右表一起用,减少右表的结果扫描

  5. 应用数学等价公式简化条件表达式,可以展开和减少常量,并排除不可能的情况和常量表达式:如5=5ANDa>5被简化成a>5

  6. 当发现某个查询或者表达式的结果是可以提前计算出来的时候,就会优化成常数

  7. 优化COUNT()、MIN()和MAX()函数:如寻找某列的最小值,如果该列有索引,只需要查找B+Tree索引最左端,反之则可以找到最大值。而COUNT()查询在没有指定WHERE条件时也可以在某些存储引擎下被优化,如MyISAM会一直保存数据表的准确行数

  8. 优化IN()条件:对于很多数据库来说,IN查询等价于多个OR条件组合。但MySQL中IN查询不是简单遍历判断,优化器会先将IN()条件中的值进行排序,后续会用二分法去判断数据是否存在于IN()条件中。算法复杂度会从O(n)降低为O(logn)

  9. 尝试进行索引覆盖:如果能做到索引覆盖,就不用花费额外的磁盘IO读取数据

  10. 子查询优化:尝试将一些子查询转换为更有效的变体形式,从而简化它们为索引查询而不是相互独立的查询

  11. 提前终止查询:如使用LIMIT、DISTINCT、NOTEXISTS时,查找到满足数量的结果集后会立即终止查询。或检测到后续数据均不可能满足某个条件后,如主键已超出指定范围,中止整个查询

  12. 优化排序:如果排序结果的量小,就用内存进行快速排序,如果排序结果的量大,就用文件进行排序。

    1. 老版本MySQL会使用两次传输排序,即先读取行序号和需要排序的字段在内存中对其排序,然后再根据排序结果去读取数据行。优点是排序时占用内存小,缺点是排序完成后需要根据行序号进行随机读取数据,较慢。

    2. 新版本采用的是单次传输排序,也就是一次读取所有的数据行,然后根据给定的列排序。优点是数据顺序读取,很快,缺点是可能会占用大量内存,甚至需要使用文件排序。但对于I/O密集型应用,单次传输排序的效率会高很多。

  13. ……

有非常多的原因会导致MySQL选择错误的执行计划:

  1. 统计信息不准确:MySQL依赖存储引擎提供的统计信息来评估成本,但有的偏差可能非常大。InnoDB因为其MVCC的架构,并不能维护一个数据表的行数的精确统计信息

  2. 执行计划中的成本估算不等同实际执行的成本:即使统计信息精准,优化器给出的执行计划也可能不是最优的。如某个执行计划虽然需要读取更多的页面,但这些页面都是顺序读或者页面都已经在内存中的话,那么它的访问成本将很小。但优化器并不知道哪些页面在内存中,哪些在磁盘上。所以查询实际执行过程中到底需要多少次物理I/O是无法得知的,它只能假设所有页面都需要从磁盘IO读取

  3. MySQL的最优可能和用户需要的最优不一样:用户希望执行时间尽可能短,但是MySQL只是基于其成本模型选择最优的执行计划,有时候并不是最快的执行方式。

  4. MySQL不会考虑其他并发执行的查询,而这也可能会影响到当前查询的速度:如锁等待、统筹算法等

  5. MySQL也并不是任何时候都是基于成本的优化,有时也基于一些固定的规则:如有一个全文匹配条件(MATCH)则只会使用全文索引。即使有其他更快的索引和非全文条件查询条件,MySQL不去使用更快的方式进行查询

  6. MySQL不会考虑不受其控制的操作的成本:如执行存储过程、用户自定义函数的成本

  7. 优化器有时候无法估算所有可能的执行计划,所以它可能错过实际上最优的执行计划。

2.2.6SQL执行

在生成对应的执行计划后,执行器会根据执行计划给出的指令逐步执行得出结果。整个执行过程的大部分操作均是通过调用存储引擎API来完成,即handlerAPI。查询过程中的每一张表由一个handler实例表示。MySQL在SQL优化阶段就为查询所涉及到的每一张表都创建了一个handler实例,优化器可以根据这些实例的接口来获取表的相关信息,包括表的所有列名、索引统计信息等。存储引擎API提供了非常丰富的功能,它们组合完成一次查询的大部分操作,如全表扫描API、读取索引API、条件判断API等。

数据库的慢查询日志中有rows_examined字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行次数。但在有些场景下,执行器调用一次,在引擎内部却扫描了多行,因此引擎实际扫描行数跟rows_examined其实并不完全相同。

2.2.7返回结果

查询执行的最后一个阶段就是将结果返回给客户端。即使查询不到数据,MySQL仍然会返回这个查询的相关信息,比如该查询影响到的行数以及执行时间等。如果查询缓存被打开且这个查询可以被缓存,MySQL也会将结果存放到缓存中。

结果集返回客户端是一个增量且分步返回的过程。有可能MySQL在生成第一条结果时,就开始向客户端逐步返回结果集。这样服务端就无须存储太多结果而消耗过多内存,也可以让客户端第一时间获得返回结果。需要注意的是,结果集中的每一行都会以一个满足客户端/服务器通信协议的数据包发送,再通过TCP协议进行传输,如果结果过大,会对数据包进行缓存然后批量发送。