题目
MySQL 组成部分,一条 SQL 查询语句怎么执行的
解释
类似问题如 “MySQL 架构”
答案
@[toc] --- ## MySQL 逻辑架构 MySQL 最重要、最与众不同的特性就是它的**可插拔存储引擎架构**(pluggable storage engine architecture),这种架构的设计将查询处理及其他系统任务和数据的存储/提取分离开来。 如果应用程序更改带来了需要更改底层存储引擎的需求,或者需要添加一个或多个存储引擎来支持新需求,则无需进行重大的编码或流程更改即可使工作正常进行。 MySQL 服务器架构通过提供适用于跨存储引擎的一致且易于使用的 API,使应用程序免受存储引擎底层复杂性的影响。 MySQL 的逻辑架构图如下: ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20210604223153.png) 我们可以大致把 MySQL 的逻辑架构分成 Server 层和存储引擎层: 1)大多数 MySQL 的核心服务功能都在 Server 层,包括连接,查询解析、分析、优化、缓存以及所有的内置函数(例如,日期、时间、数学和加密函数),所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图等。 2)第二层就是存储引擎(支持 InnoDB、MyISAM、Memory 等多个存储引擎)。存储引擎负责 MySQL 中数据的存储和提取,响应上层服务器的请求。每个存储引擎自然是有它的优势和劣势,不同的存储引擎之间无法相互通信,所以我们需要根据不同的场景来选择合适的存储引擎。 服务器通过 API 与存储引擎进行通信。这些接口屏蔽了不同存储引擎之间的差异,使得这些差异对上层的查询过程透明。存储引擎 API 包含几十个底层函数,用于执行诸如 “开始一个事务” 或者 “根据主键提取一行记录” 等操作。 需要注意的是,在 MySQL 5.1 及之前的版本,MyISAM 是默认的存储引擎,而在 MySQL 5.5.5 后,InnoDB 成为了默认的存储引擎。 ## 一条 SQL 查询语句是如何执行的 ### 连接器(Connector) **MySQL 连接器为客户端程序提供到 MySQL 服务器的连接**。 说得更细节一点的话,连接器其实会做两个事情,一个是管理 MySQL 连接,一个是权限验证。 ### 查询缓存(Query Cache) 连接建立完成后,我们就可以输入 select 语句进行查询了。执行逻辑就来到了第二步:查询缓存。 查询缓存存储了 SELECT 语句的文本以及响应给客户端的相应结果。这样,如果服务器稍后接收到相同的 SELECT 语句,服务器会先从查询缓存中检索结果,而不是再次解析和执行该语句。查询缓存在 session 之间共享,因此可以发送一个客户端生成的结果集以响应另一个客户端发出的相同查询。 如果当前的查询恰好命中了查询缓存,那么**在返回查询结果之前 MySQL 会检查一次用户权限**。这仍然是无须解析查询SQL语句的,因为在查询缓存中已经存放了当前查询需要访问的表信息。 那么既然涉及到缓存,就必然绕不开缓存一致性问题了。值得庆幸的是,不需要我们进行额外操作,查询缓存并不会返回陈旧数据! > The query cache does not return stale data. When tables are modified, any relevant entries in the query cache are **flushed**. 当表被修改时,查询缓存中的任何相关条目都会被 flushed,注意,这里的 flushed 翻译为**清空**而不是刷新。 看起来好像还不错?不用我们手动操作,失效缓存就能够被自动清空。 然而,很不幸的是,正是由于这个特性,**从 MySQL 5.7.20 开始,官方不再推荐使用查询缓存,并在 MySQL 8.0 中直接删除了查询缓存!** > The query cache is deprecated as of MySQL 5.7.20, and is removed in MySQL 8.0. 其实不难理解,举个例子,对于一个流量很大的论坛项目来说,查询帖子表的需求每时每刻都存在,帖子也几乎每时每刻都在增加,那只要这个表一更新,这个表上所有的查询缓存都会被清空,这对于 MySQL 数据库的压力之大,可想而知了吧。费个劲把查询结果存起来,还没来得及使用呢,就被一个更新全清空了。 对于 MySQL 8.0 之前的版本来说,你可以将参数 `query_cache_type` 设置成 `DEMAND`,这样所有的 SQL 语句都不会再使用查询缓存。而对于你确定要使用查询缓存的语句,可以用 `SQL_CACHE` 显式指定,像下面这个语句一样: ```sql mysql> select SQL_CACHE * from t1 where id = 1; ``` ### 解析器(Parser) 如果没有命中或者没有开启查询缓存,MySQL 服务器接下来要做的就是将一条 SQL 语句转换成一个执行计划,再依照这个执行计划和存储引擎进行交互。这包括多个子阶段:解析 SQL、预处理、优化 SQL 执行计划。这个过程中任何错误(例如语法错误)都可能终止查询。 其中解析 SQL 和预处理就是解析器做的事情,优化 SQL 执行计划就是优化器做的事情。这里我们先说解析器。 > 这里《高性能 MySQL - 第 3 版》书中分得更细致点,解析器用来解析 SQL,**预处理器**则用来预处理,我暂且把它们都归为解析器吧 所谓解析 SQL 就是说,MySQL 通过关键字对 SQL 语句进行解析,并生成一棵对应的 “**解析树**”,用于根据语法规则来验证语句是否正确。例如,它将验证是否使用错误的关键字,或者使用关键字的顺序是否正确等,再或者它还会验证引号是否能前后正确匹配。 而预处理则会进一步检查解析树是否合法,例如,检查数据表和数据列是否存在,检查表名和字段名是否正确等。 ### 优化器(Optimizer) 现在,解析树是合法的了,MySQL 已经知道你要做什么了。不过,一条查询可以有很多种执行计划,最后都返回相同的结果,那到底该选择哪种执行计划呢? 举个简单的例子: ```sql mysql> select * from t1 where id = 10 and name = "good"; ``` 对于上面这个语句,可以先查找 name = good 再查找 id = 10,也可以先查找 id = 10 再查找 name = good,这两种不同的执行计划可能耗费的时间成本是不一样的。 那么**优化器的作用就是找到这其中最好的执行计划**。需要注意的是,这里的执行计划是一个数据结构,而不是和很多其他的关系型数据库那样会生成对应的字节码。 另外,优化器并不关心表使用的是什么存储引擎,但存储引擎对于优化查询是有影响的。优化器会请求存储引擎提供容量或某个具体操作的开销信息,以及表数据的统计信息等。 当优化器阶段完成后,这个语句的执行计划就确定下来了,就可以进入执行器阶段了。 ### 执行器 和命中查询缓存一样,在开始执行 SQL 语句之前,执行器会先判断一下当前用户对这个表有没有执行查询的权限,如果没有,就会返回没有权限的错误。 权限认证完成后,MySQL 就会根据执行计划给出的指令逐步执行。在根据执行计划逐步执行的过程中,有大量的操作需要通过调用存储引擎实现的接口来完成,这些接口也就是我们称为 “handler API” 的接口。 查询中的每一个表由一个 handler 的实例表示。实际上,MySQL 在优化阶段就为每个表创建了一个 handler 实例,优化器根据这些实例的接口可以获取表的相关信息,包括表的所有列名、索引统计信息,等等。 ### 小结 总结下一条查询语句的执行过程: ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20210606190231.png) 1. MySQL 客户端与服务器间建立连接,客户端发送一条查询给服务器; 2. 服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果;否则进入下一阶段; 3. 服务器端进行 SQL 解析、预处理,生成合法的解析树; 4. 再由优化器生成对应的执行计划; 5. MySQL 根据优化器生成的执行计划,调用相应的存储引擎的 API 来执行,并将执行结果返回给客户端。