在《MySQL的整体架构及功能详解》这篇文章中,我们讲解了MySQL的整体架构。那么,你是否有过类似的疑问:一条查询的SQL语句的底层到底是如何执行的?
这篇文章,我们在MySQL的整体架构的基础上,进一步详细讲解一条SQL语句在此架构中的运行过程。
SQL执行的流程图
为了更加聚焦核心流程,这里我们将MySQL的整体架构分为:客户端、Server层、存储引擎层。暂时省略了系统文件层。
结合上面的SQL执行的流程图,我们下面分步骤分析MySQL中SQL执行所经历的具体步骤。
客户端
通常,我们可以通过图形化界面、命令行、不同编程语言的类库等形式来作为连接MySQL的客户端。以执行下面的SQL为例:
ini
SELECT * FROM users WHERE id = 1;
在执行之前,先通过客户端与MySQL服务器建立连接,这时需要提供用户名和密码等参数,MySQL服务器中的连接器会对其进行身份认证、权限获取。
如果客户端与连接器建立的连接属于长连接,那么在连接有效期内,即便当前用户的权限被修改,也不会立即生效,只有当用户重新建立连接时才会使用最新的权限。
由于数据库的连接属于稀有资源,通常在实践中会基于长连接和连接池的形式充分利用数据库的连接。
连接器
大多数基于网络的B/S架构的工具或者服务都有类似连接器的结构。连接器的主要工作就是管理和客户端的连接,同时进行连接的权限认证。
当客户端发送SQL请求后,连接器负责处理客户端连接,并验证用户权限,之后所有的权限判断逻辑,都依赖于此时读到的权限。这也意味着,当处于连接状态时,对用户权限的修改是无法立即生效的。
为了避免线程被频繁的创建和销毁,影响性能,MySQL5.5
版本引入了线程池,会缓存创建的线程,不需要为每一个新建的连接,创建或销毁线程。可以使用线程池中少量的线程服务大量的连接。
其他一些常见的情况也归属连接器管理范围,比如当用户名密码不正确时,会提示类似 "Access denied for user" 的错误信息;当一个连接长时间未被使用(默认8小时)会被断开,此时客户端再次发送请求的话,就会收到一个错误提醒:"Lost connection to MySQL server during query"等。
在MySQL8.0+版本中,当客户端与连接器成功建立连接之后,连接器会将SQL请求交给Server层中的分析器和优化器。在之前版本中,还需要多一步查询缓存。
查询缓存
在MySQL的早期版本(MySQL 8.0之前)中,为了提高查询SQL语句的响应速度,会缓存特定SQL的整个结果集。当有相同SQL语句时,直接从缓存中返回结果,提高查询效率。
在这一步,当完成连接建立和权限校验之后,MySQL会拿上面SQL到查询缓存查找该SQL语句是否执存在,如果能在缓存中找到,就直接返回查询结果给客户端。如果查找不到,就继续执行后续操作。
但是,大多数情况下不建议使用查询缓存,因为其功能弊大于利。查询缓存的失效频次非常高,在遇到涉及表内容更新(如 INSERT
, UPDATE
, DELETE
等操作)时,会自动清除这些表相关的缓存,因为表内容变更后,缓存结果可能已经过时。这就会导致查询缓存在频繁写入操作的场景下失效较频繁,无法带来显著的性能提升,甚至可能带来额外的开销。
这一特性只适合类似系统配置表这类不经常更新的表结构,然而大多数场景下,都会有频繁的更新操作。因此不建议使用。MySQL官方在 MySQL 5.6
开始,默认禁用了查询缓存,在 MySQL 8.0
中删除了查询缓存的功能。
分析器
分析器(Parser) 的主要职责是将用户输入的SQL语句解析成MySQL可以理解和执行的结构化数据,同时检查语法是否正确。主要包括以下步骤和环节。
1. 词法分析(Lexical Analysis)
分析器的第一步是将 SQL 语句从字符串形式拆解成"词法单元"(Tokens),如关键字(SELECT、FROM、WHERE等)、标识符(表名、列名等)、运算符、常量等,也就是将这些内容分成具有特定意义的最小单位。
对于 SELECT * FROM users WHERE id = 1;
,分析器会将其分解成以下词法单元:
SELECT
------ 标识符,表示查询动作。*
------ 通配符,表示查询所有字段。FROM
------ 标识符,表示查询来源表。users
------ 表名。WHERE
------ 标识符,表示过滤条件。id
------ 列名称。=
------ 条件运算符。1
------ 整型字面值。;
------ SQL 语句结束符。
在获取这些 Tokens 后,分析器会进一步理解它们的语义关系。
2. 语法分析(Syntax Analysis)
分析器会根据 MySQL 的 SQL 语法规则,检查整个 SQL 语句的结构是否正确,并将其构建为一颗语法树(Parse Tree)。语法树是一个结构化的表示形式,用于描述 SQL 语句的层次结构及其组件关系。
对于 SELECT * FROM users WHERE id = 1;
,语法树包括如下节点:
- 根节点:表示这是一个
SELECT
类型的查询。 - 子节点:包含字段列表(
*
)、表名(users
)以及筛选条件(id = 1
)。
如果 SQL 语法不符合规则(如少了 WHERE
后面的条件),分析器会报错并终止执行。
3. 合法性检查
分析器还会进一步检查:
- 所查询的表
users
是否存在。 id
列是否存在于users
表中。*
是否有效(通常表示查询所有列)。
这些检查可能交由权限相关组件或其他模块处理,但分析器会在这个阶段确保 SQL 语句能够正常解析为查询逻辑。
完成词法和语法分析后,分析器会生成一个 SQL 的抽象表示(通常是语法树或其他内部结构),将其交给下一步的优化器(Optimizer) 去处理。
优化器(Optimizer)
在 MySQL 中,优化器的主要职责是生成一份高效的查询执行计划,即确定如何以最优方式从存储引擎中获取数据。优化器会确定具体的查询执行计划(例如选择使用索引查询、全表扫描等方式),找到执行效率最高的方式。
优化器会根据 SQL 的语法结构和表的元数据(如表结构、索引信息等)尝试评估多种执行方案,然后选择代价最低的路径。
以 SELECT * FROM users WHERE id = 1;
为例,优化器可能需要考虑以下执行路径:
- 全表扫描(Full Table Scan) :如果
users
表没有合适的索引,优化器可能选择直接扫描整个表,逐行判断id = 1
是否匹配。全表扫描的代价较高,但在小表或者表没有索引的情况下可能是唯一可选方案。 - 索引查找(Index Lookup) :如果
users
表的id
列上存在索引,优化器可能选择基于索引直接查找对应的记录,而不是扫描整个表。索引查找的代价通常较低,因为它只需要直接定位满足条件的记录。
优化器会根据存储引擎提供的统计信息(如索引的选择性和记录的行数)计算每种执行方案的成本,选择代价最低的一条执行路径。例如,如果 id
是主键或有唯一索引,优化器通常会优先使用索引查找。
如果 users
表有多个索引,优化器需要决定用哪个索引,并选择使用哪种具体的方式。优化器会分析索引的选择性(selectivity),即过滤效果。如果索引能显著减少行数(例如查找 id
的唯一值),优化器通常会优先选择此索引。
如果查询涉及多张表(如 JOIN
操作),优化器需要决定选择以何种顺序读取多张表的数据,比如:嵌套循环连接(Nested Loop Join)、索引辅助连接(Index Nested Loop Join)、Hash Join 等具体方式。
对于具有 ORDER BY
或 LIMIT
的查询(本例没有),优化器还需要决定具体策略,例如:是否使用索引来直接支持排序,是否可以利用覆盖索引避免额外排序,如果能通过索引直接返回结果,优化器会避免使用临时表或额外的排序步骤。
优化器会对每种执行方案进行成本估算,主要包括以下因素:
- 哪种执行方式需要读取较少的行(减少 I/O 开销)。
- 哪种方式可以尽量避免扫描大量的无关数据。
- 索引或全表扫描的总时间代价。
最终,优化器会选择成本最低的执行计划,交给执行器来执行。
经过上述步骤后,优化器会生成最终的执行计划(Execution Plan)。执行计划包括:
- 应该使用哪些索引。
- 扫描的范围。
- 是否需要排序或临时表。
- 表的读取顺序(对于多表查询)。
- 查询的最终返回路径。
可以通过 EXPLAIN
命令查看优化器生成的执行计划。例如:
ini
EXPLAIN SELECT * FROM users WHERE id = 1;
需要注意的是优化器不负责数据读取,优化器仅生成执行计划,实际数据查询的任务由执行器完成。优化器也不直接控制存储引擎缓存,存储引擎(如 InnoDB)决定如何管理自己的缓冲池。
执行器
执行器根据优化器生成的执行计划,调用存储引擎(如 InnoDB)接口,去获取数据。执行器与存储引擎之间实际上进行的是逐步交互,执行器会向存储引擎发出请求,存储引擎读取磁盘上的数据后,将记录返回给执行器。
通常包括以下步骤:
执行器首先会根据查询的目标表(这里是 users
)向存储引擎发出请求,准备读取数据。例如:如果 users
表使用的是 InnoDB 存储引擎,则执行器会通过 InnoDB 的接口交互。如果是其他存储引擎(如 MyISAM),则会调用相应的引擎接口。
在真正执行查询之前,执行器会检查当前用户是否有对表 users
的 查询权限(SELECT
权限)。如果用户没有权限,执行器会直接抛出错误并终止执行流程。
随后,根据执行计划,执行器会向存储引擎发出数据读取的请求。如果使用索引,存储引擎会通过索引定位记录,否则逐行扫描表。
执行器会对存储引擎返回的数据逐行进行过滤操作,检查是否满足 SQL 查询的条件。在本例中,id = 1
作为过滤条件,执行器会判断每行数据的 id
列的值是否等于 1
。如果记录符合条件,则该记录被保存为结果;否则直接跳过。
在索引查找中,存储引擎可能已经通过索引过滤掉了大部分无关记录,因此这一步可能显得轻量化。
执行器会将符合条件的记录逐步收集,最终将结果返回给客户端。具体步骤为:
- 执行器不断向存储引擎请求满足条件的记录。
- 每获取一条记录后,执行器立即传递给客户端(流式传输),而不是一次性构建完整结果再返回。
- 当所有数据处理完毕(或达到
LIMIT
提取的上限),执行器结束操作。
小结
最后我们再简单总结一下以SELECT * FROM users WHERE id = 1;为例,MySQL在执行时的整体流程:
- 客户端 :客户端发送 SQL 查询,将
SELECT * FROM users WHERE id = 1;
传递给 MySQL 服务端。 - 连接器:连接器验证客户端权限和身份,并建立会话以接收和管理传来的 SQL 请求。
- 查询缓存:检查 Server 层查询缓存(8.0 之前支持),如果命中缓存则直接返回结果,否则继续执行后续步骤。
- 分析器:分析器解析 SQL 语句,进行词法分析和语法分析,生成语法树,验证表名和字段合法性。
- 优化器:优化器基于语法树和表的元数据计算代价,选择最优的执行计划(如使用索引或全表扫描)。
- 执行器:执行器根据优化器执行计划向存储引擎发送请求,逐步获取符合条件的记录,并最终将结果返回给客户端。