Bootstrap

【MySQL】简单解析一条SQL查询语句的执行过程

1. MySQL 的逻辑架构图

在这里插入图片描述

  • MySQL 架构主要分为 Server 层和存储引擎层。
  • Server 层集成了连接器、查询缓存、分析器、优化器和执行器等核心组件,负责提供诸如日期、时间、数学和加密等内置函数,以及实现存储过程、触发器、视图等跨存储引擎的功能。
  • 存储引擎层则负责数据的实际存储与提取,采用插件式设计,支持 InnoDB、MyISAM、Memory 等多种引擎,其中 InnoDB 自 MySQL 5.5.5 版本起成为默认存储引擎。

2. SQL 查询语句执行详解

2.1. 连接器:建立连接的关键环节

客户端通过如下命令发起数据库连接请求。

mysql -h$ip -P$port -u$user -p

完成 TCP 握手后,连接器依据输入的用户名和密码进行身份验证。

若用户名或密码错误,系统会立即返回 “Access denied for user” 错误,连接随即终止。若认证成功,连接器会从权限表中检索该用户的权限信息,并在后续该连接的操作中以此为依据进行权限判定。即便管理员在用户建立连接后修改了其权限,已有的连接权限也不受影响,新权限仅对新建立的连接生效。

连接成功后,若客户端长时间无操作,此连接便进入空闲状态,可使用 show processlist 命令查看,其中 Command 列为 “Sleep” 的记录即表示空闲连接。

在这里插入图片描述

若空闲时长超过 wait_timeout(默认 8 小时),连接器会自动断开连接。此时若客户端再次发送请求,会收到 “Lost connection to MySQL server during query” 错误提示,需重新连接并执行操作。

在实际应用场景中,由于建立连接的过程相对复杂,会消耗一定资源,因此建议尽量减少连接的建立次数,优先采用长连接。但长连接可能引发内存问题,因为 MySQL 在执行过程中临时使用的内存由连接对象管理,若连接持续不关闭,这些内存资源无法释放,可能导致内存占用过高,甚至引发系统强制终止 MySQL 进程(OOM),表现为 MySQL 异常重启。

解决此问题有两种途径:一是定期主动断开长连接,比如在程序中判断执行过一个占用大量内存的查询操作后断开连接;二是在 MySQL 5.7 及以上版本,可利用 mysql_reset_connection 命令重新初始化连接资源,此操作无需重新连接和权限验证,就能使连接恢复到初始创建时的状态。

2.2. 查询缓存:双刃剑般的存在

查询缓存的设计初衷是存储已执行过的 SQL 查询语句及其结果,以键值对(key-value)形式存在,其中查询语句为键,查询结果为值。当 MySQL 接收到查询请求时,会首先在查询缓存中查找是否存在相同的查询语句。例如,对于查询语句 select * from T where ID=10,若之前执行过且结果已缓存,系统会直接返回缓存中的结果,显著提升查询效率。

然而,查询缓存存在明显的局限性。只要对某个表进行更新操作,该表相关的所有查询缓存都会被清空。在实际业务中,如电商订单管理系统,数据表频繁更新,查询缓存的命中率极低。因此,在大多数情况下,不建议启用查询缓存

可通过将 query_cache_type 设置为 DEMAND 来禁用默认的查询缓存,对于确实需要缓存的特定查询,可使用 SQL_CACHE 进行显式指定。例如 mysql> select SQL_CACHE * from T where ID=10

值得注意的是,自 MySQL 8.0 版本起,已彻底移除查询缓存功能。

2.3. 分析器:语法语义的把关者

若查询未命中缓存,便进入分析器阶段。

首先进行词法分析,分析器会将输入的 SQL 语句(由多个字符串和空格组成)分解识别。以 select * from T where ID=10 为例,分析器能够识别出 “select” 为查询关键字,“T” 是表名,“ID” 是列名。

完成词法分析后,紧接着进行语法分析,依据语法规则检查语句的合法性。比如,若语句 elect * from t where ID=1(少打了开头的字母 “s”),分析器会提示 “You have an error in your SQL syntax” 错误,并准确指出错误位置在 “use near” 后的内容。

对于表中不存在的列引发的错误,也是在分析器阶段被检测到。例如执行 select * from T where k=1,若表 T 中不存在字段 k,分析器在语法分析过程中检查列的有效性时,会报出 “Unknown column ‘k’ in ‘where clause’” 错误。

2.4. 优化器:效率提升的幕后推手

经过分析器确定查询意图后,优化器开始发挥作用。

当表存在多个索引或语句涉及多表关联(join)时,优化器的重要性尤为凸显。

例如,对于查询语句 mysql> select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20,优化器需要权衡是先从表 t1 中取出 c = 10 的记录的 ID 值,再关联到表 t2 判断 d 的值是否为 20;还是先从表 t2 中取出 d = 20 的记录的 ID 值,再关联到表 t1 判断 c 的值是否为 10。

这两种执行路径虽然逻辑结果一致,但执行效率可能存在较大差异,优化器的核心任务就是选择最优的执行方案,确定后将执行计划传递给执行器。

2.5. 执行器:数据查询的执行者

执行器首先检查用户是否具有对查询表的执行权限。例如,执行 mysql> select * from T where ID=10,若用户没有相应权限,会返回 “ERROR 1142 (42000): SELECT command denied to user ‘b’@‘localhost’ for table ‘T’” 错误。若用户有权限,则根据表的引擎定义调用相应引擎接口执行查询。

以表 T 中 ID 字段无索引为例,执行器首先调用 InnoDB 引擎接口获取表的第一行数据,判断该行的 ID 值是否为 10,若不是则跳过该行,若是则将该行存入结果集。

接着继续调用引擎接口获取 “下一行” 数据,并重复上述判断逻辑,直至遍历完表的所有行。最后,执行器将满足条件的行组成的记录集返回给客户端。

在数据库的慢查询日志中,rows_examined 字段记录了执行器调用引擎获取数据行的累加次数,反映了语句执行过程中的扫描行数。

但需注意,在某些情况下,执行器调用一次引擎接口,在引擎内部可能扫描了多行,所以引擎扫描行数与 rows_examined 并不一定完全相同。

3. 示例解析

3.1. 案例说明

假设有一个名为 employees 的表,包含 idnameagedepartment 等字段,其中 id 为主键且有索引,department 字段也有索引。执行查询语句 select * from employees where department = 'IT' and age > 30

3.2. 执行流程拆解

首先,连接器验证客户端连接请求,确认用户有权限访问数据库和 employees 表。

接着,查询缓存检查是否存在该查询的缓存结果,若不存在则进入分析器阶段。分析器识别出关键字、表名和列名,并检查语法和语义,确保语句合法且列存在。

然后,优化器根据索引情况和数据分布决定执行计划。由于 departmentage 都有索引,优化器会评估先根据 department 索引筛选出 IT 部门的记录,再对这些记录检查 age 是否大于 30 更高效,还是反之更优,最终确定最佳执行路径。

最后,执行器按照优化器确定的计划调用存储引擎接口。例如,如果选择先按 department 索引查找,执行器会通过引擎接口获取满足 department = 'IT' 的第一行数据,检查 age 是否大于 30,符合条件则存入结果集,然后继续获取下一行数据重复判断,直至遍历完所有符合 department = 'IT' 条件的记录。最终将结果集返回给客户端。

;