一条SQL查询语句在MySQL中是如何执行的

一、MySQL整体架构

MySQL的架构主要分为两层:Server层存储引擎层

  • Server层 :负责连接管理、SQL解析、优化以及执行等核心流程。当客户端发起连接时,连接器 负责建立连接、验证身份与权限。同时,内部的连接池/线程管理模块会为每个客户端请求分配一个线程,避免了频繁创建和销毁线程的开销。

  • 存储引擎层:负责数据的实际存储与提取。MySQL采用插件式的存储引擎架构,其中最常用的是InnoDB。

1.1 一条SQL查询语句的执行流程

下面,我们以一条查询语句为例,详细拆解它在MySQL内部经历的各个阶段。

第一步:连接器

客户端与MySQL建立连接,始于连接器。

  1. 建立TCP连接 :MySQL基于TCP协议传输,因此第一步是完成经典的TCP三次握手

  2. 身份验证 :连接器验证你提交的用户名和密码。如果验证失败,客户端会收到Access denied for user的错误信息,并结束执行。

  3. 获取权限 :验证成功后,连接器会立即从系统权限表中读取该用户的所有权限,并缓存在当前连接会话中。这里有一个关键点:后续此连接中的所有操作,其权限判断都基于此次读取的快照。这意味着,即使连接建立后管理员修改了你的权限,当前已建立的连接也不会生效,必须重新连接。

关于连接管理的几个问题:

  • 空闲连接会一直占用资源吗?

    不会。MySQL通过wait_timeout参数控制空闲连接的最大时长,默认是8小时。如果空闲连接超时,连接器会自动断开它。你也可以手动执行kill connection命令来断开。

  • 断开连接后客户端会立刻知道吗?

    不会。连接断开是服务端单方面操作的,客户端要等到下一次发送请求时,才会收到类似Lost connection to MySQL server during query的报错。

  • MySQL的连接数有限制吗?

    有限制。由max_connections参数控制,该参数可以手动调整。我的MySQL服务默认是151个。一旦连接数超过这个限制,系统会拒绝后续连接请求,并返回Too many connections错误。

长连接 vs 短连接

MySQL的连接也分为短连接和长连接。由于长连接可以减少建立和断开连接的开销,所以一般推荐使用长连接。

但长连接也有隐患。MySQL在执行操作时会临时使用内存来管理连接对象,这些资源只有在连接断开时才会彻底释放。如果长连接累积过多且执行了大量大操作,会导致MySQL内存占用飙升,严重时可能被操作系统强制杀掉,导致MySQL服务异常重启。

如何解决长连接占用内存的问题?

  • 定期断开长连接:在使用完一个批次的操作后,主动断开连接,释放内存。

  • 重置连接(MySQL 5.7+) :MySQL 5.7版本提供了mysql_reset_connection()接口函数。在代码中调用此函数,可以在不重新建立TCP连接的情况下,重置连接状态并释放临时内存,恢复到刚刚建立连接时的状态。

第二步:查询缓存(已废弃)

连接建立后,客户端就可以向MySQL发送SQL语句了。MySQL收到SQL后,首先会解析出语句的第一个字段,判断其类型。

如果是SELECT查询语句,MySQL之前会先去查询缓存中查找。查询缓存以Key-Value形式保存在内存中,Key是SQL查询语句,Value是该语句的查询结果。如果命中缓存,结果会直接返回给客户端;如果未命中,则继续执行后续流程,并在执行结束后将结果存入查询缓存。

为什么从MySQL 8.0开始,查询缓存被移除了?

因为它的弊端非常明显。对一个表的任何更新操作(增、删、改),都会导致该表所有相关的查询缓存被全部清空。如果缓存了一个结果集很大的数据,还没等被使用就被清空,就完全是做无用功了。对于更新频繁的表,缓存命中率极低,维护缓存的成本反而更高。因此,从MySQL 8.0开始,查询缓存功能已被彻底移除,这条路径也就走不通了。

第三步:解析器

在正式执行SQL之前,MySQL必须"读懂"这条语句。这个工作由解析器完成,主要包含两步:

  1. 词法分析 :将输入的SQL字符串识别出一个个"Token"(关键词、标识符等)。例如,SELECT username FROM userinfo会被解析出SELECTusernameFROMuserinfo四个Token,其中SELECTFROM是关键词。

  2. 语法分析 :根据词法分析的结果,语法解析器会检查SQL语句是否符合MySQL的语法规则。如果没问题,就会构建一棵SQL语法树,方便后续模块获取SQL类型、表名、字段名、WHERE条件等信息。

注意 :如果输入的SQL语句存在语法错误,例如写错了关键词,就会在这个阶段报错。但是,表或字段是否存在,要等到下一阶段------预处理器,才会检测。

第四步:执行SQL

执行SQL语句是整个流程的核心,可以细分为三个阶段:预处理优化执行

1. 预处理器

  • 检查语义:检查SQL查询语句中涉及的表和字段,判断它们是否真实存在。

  • 扩展通配符 :将SELECT *中的*,扩展为表中所有列的名称。

2. 优化器

优化器的职责是确定一个最高效的执行方案。例如,当表中存在多个索引时,优化器会基于查询成本(如CPU、I/O成本)来决定使用哪个索引。

我们可以通过在查询语句前加上EXPLAIN命令,来查看优化器选择的执行计划。执行计划中的key字段,就表示实际使用到的索引。如果keyNULL,则说明没有使用索引,将会进行全表扫描。

当一条查询既可以使用主键索引,也可以使用二级索引时,优化器会综合评估,选择它认为成本更低的那个方案。

3. 执行器

优化器确定了执行方案后,MySQL就真正开始执行语句了。这个"实干"环节由执行器负责。

执行器的核心工作是与存储引擎进行交互 ,并且这种交互是以记录(Row)为单位的。执行器会根据执行计划,不断调用存储引擎提供的接口,逐行获取数据,并进行条件判断和结果收集。

索引下推

在了解具体的交互过程之前,有必要先介绍一个重要的执行优化手段------索引下推

索引下推是 MySQL 5.6 引入的一项优化,核心思想是:将部分 WHERE 条件的判断下推到存储引擎层进行过滤,从而减少 Server 层与存储引擎之间的交互次数。

我们通过一个例子来理解。

假设有一张用户表 user,建有一个联合索引 (age, city)

sql 复制代码
CREATE TABLE user (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    age INT,
    city VARCHAR(50),
    KEY idx_age_city (age, city)
);

现在执行一条查询:

sql 复制代码
SELECT * FROM user WHERE age BETWEEN 18 AND 25 AND city = '北京';

由于联合索引是 (age, city),而条件中的 city 是索引的第二列,根据最左前缀原则,索引只能匹配到 age BETWEEN 18 AND 25 这个范围,city = '北京' 无法在索引树中直接过滤。

在索引下推关闭的情况下:

  1. 存储引擎根据 age BETWEEN 18 AND 25 找到所有符合条件的索引记录。

  2. 每找到一条,就进行一次回表,获取完整的行数据,返回给执行器。

  3. 执行器拿到数据后,再判断 city = '北京' 是否满足,不满足则丢弃。

这意味着,即使最终只有 100 条数据满足 city = '北京',但如果 age BETWEEN 18 AND 25 命中了 10000 条记录,就会发生 10000 次回表,其中 9900 次都是浪费的。

在索引下推开启的情况下(默认开启):

  1. 存储引擎根据 age BETWEEN 18 AND 25 找到索引记录后,直接在存储引擎层 判断 city = '北京' 是否满足。

  2. 只有满足 city = '北京' 的索引记录,才会进行回表,获取完整的行数据,返回给执行器。

这样一来,回表次数从 10000 次降到了 100 次,大大减少了 I/O 开销。

可以用 EXPLAIN 查看执行计划,如果 Extra 列中出现了 Using index condition,就表示本次查询使用了索引下推。

了解索引下推之后,我们再来看三种具体场景下执行器与存储引擎的交互过程。

(1)主键索引查询

假设执行语句:SELECT * FROM user WHERE id = 10;

  1. 执行器调用存储引擎的接口,请求读取表中第一行数据。

  2. 存储引擎根据主键索引(B+树),快速定位到 id = 10 的数据页,并返回该行记录。

  3. 执行器接收到该行数据后,判断其是否满足 WHERE 条件(此处为 id = 10)。由于主键查询最多只返回一条记录,执行器在拿到数据后,立即将该行加入结果集。

  4. 执行器再次请求下一行,存储引擎返回"已无更多记录"的信号,执行器结束查询,并将结果集返回给客户端。

(2)普通索引查询(回表)

假设执行语句:SELECT * FROM user WHERE age = 20;age 字段上建有普通索引)

  1. 执行器请求存储引擎,根据普通索引 idx_age,读取满足 age = 20 的第一条记录。

  2. 存储引擎在 idx_age 索引树上找到该记录,获取到对应的主键 ID (例如 id = 5)。

  3. 存储引擎再根据这个主键 ID,回表 到主键索引树进行查找,得到完整的行数据(id = 5, name = '张三', age = 20),并返回给执行器。

  4. 执行器判断该行是否满足所有条件(此处为 age = 20),满足则加入结果集。

  5. 执行器继续请求下一条满足 age = 20 的记录,存储引擎重复上述回表过程,直到所有符合条件的记录被读取完毕。

  6. 执行器将最终结果集返回给客户端。

(3)全表扫描(无索引)

假设执行语句:SELECT * FROM user WHERE age = 20;(但 age 字段没有索引)

  1. 执行器请求存储引擎,读取表中的第一行数据。

  2. 存储引擎从数据文件的第一页开始,读取第一行记录,返回给执行器。

  3. 执行器判断该行的 age 字段是否等于 20。如果满足,则加入结果集;如果不满足,则丢弃。

  4. 执行器接着请求下一行,存储引擎继续顺序读取下一行记录。

  5. 这个过程会一直循环,直到存储引擎将整个表的数据全部扫描完毕,并返回"已无更多记录"的信号。

  6. 执行器将所有满足条件的行组装成结果集,返回给客户端。

相关推荐
ttwuai6 小时前
GoFrame 连接 MySQL 报错排查:parseTime、时区和 utf8mb4 怎么配
数据库·mysql·adb
辉灰笔记6 小时前
MySQL8 意外关机/误移data目录导致服务启动失败 PID报错/权限报错 修复文档
linux·数据库·mysql·centos
喜欢的名字被抢了7 小时前
MySQL核心机制:事务、锁与存储引擎
数据库·sql·mysql·教程
消失的旧时光-19438 小时前
MySQL 索引原理系列(二·补充):MySQL 为什么不用红黑树,而用 B+树?
数据库·b树·mysql·红黑树
万亿少女的梦1689 小时前
基于Spring Boot、Vue.js和MySQL的超市管理系统设计与实现
java·vue.js·spring boot·mysql·管理系统
weixin_307779139 小时前
Linux下Docker Compose里运行的MySQL数据库故障诊断Shell脚本
linux·运维·mysql·docker·自动化
辉灰笔记11 小时前
第一篇:MySQL8.0生产备份实战|XtraBackup全自动全量+增量备份(钉钉告警+异地Binlog归档)
数据库·mysql·钉钉·运维开发
wzy062312 小时前
VIP + Orchestrator + 自定义脚本 MySQL 高可用方案
mysql·orchestrator
TPBoreas1 天前
MySQL性能优化面试全攻略
mysql·面试·性能优化