MySQL | 从SQL到数据的完整路径

一、引文

最近我正在学习 MySQL 的面试相关八股,打算单独开一个 MySQL 专栏来记录自己每天的学习,内容主要来自小林coding和 JavaGuide。今天了解到了 MySQL 的架构和执行流程,没想到一条简单的 sql 语句居然在 MySQL 中经历了这么多的流程。

二、MySQL 执行流程

MySQL 执行流程大概分为服务层和存储引擎层两部分,服务层中主要涉及到建立连接、查询缓存、SQL语句解析、预处理、优化、执行几个阶段,存储引擎层则是把执行结果拿到的数据返回。

1.服务层

(1)连接器

连接器这一环主要是让客户端(如 Java 程序、TablePlus等)基于 TCP 连接到 MySQL 的服务端。第一步肯定是先启动 MySQL 服务,然后基于 TCP 协议完成三次握手,连接层要做的事情第一个就是校验你的用户名和密码是否正确,只有你输入正确时,才会建立连接,同时记录你这个账号的权限,并在此后该连接的过程中都会基于刚连接时保存的权限一直执行该权限的逻辑。

在 MySQL 中也有长连接和短连接的概念,所谓的短连接就是每建立一次 MySQL 客户端连接,只能执行一条 SQL 语句,然后就立马断开连接。而长连接就是一次客户端连接中可以执行多条 SQL 语句。

长连接与短连接的性能差异极大,在 MySQL 中频繁地创建销毁连接都是极其消耗资源的,原因主要出在了巨大的"握手"开销上,如网络层面的 TCP 三次握手、MySQL 协议握手、TLS/SSL握手以及每次创建连接 MySQL 都要去系统表查询用户权限并加载到内存中。
与此同时 MySQL 的连接与内存是密切相关的。默认情况下,连接器每建立一次新的连接,MySQL 都会对应创建一个新的工作线程,即便这个线程是 Sleep 状态,也会占用相应的内存。为避免大量线程创建导致内存溢出以及 CPU 频繁切换线程,MySQL 还提供了线程池功能,让少量线程服务于大量连接,进而减小内存损耗。

一个连接占用的内存主要分为两部分:固定内存临时内存

(1) 固定内存 (Thread Static Memory)

这是连接建立后立即分配的,直到连接断开才释放:

  • 线程栈 (Thread Stack) :存储线程执行时的局部变量、函数调用信息。由参数 thread_stack 控制(默认 256KB 左右)。

  • 连接信息:存储用户信息、权限、当前数据库名、状态变量等。

  • 网络缓存 (Net Buffer) :用于存放客户端发送的 SQL 语句和服务器返回的结果。由 net_buffer_length 控制。

(2) 会话级临时内存 (Session Private Memory)

这是最容易导致内存激增的部分。当连接开始执行复杂的 SQL 时,MySQL 会根据需要临时分配内存:

  • 排序缓冲区 (Sort Buffer) :执行 ORDER BYGROUP BY 时使用。

  • 连接缓冲区 (Join Buffer):执行多表关联查询时使用。

  • 临时表内存 (Memory Temporary Table):执行复杂查询产生的中间结果。

  • 结果集缓存:在数据发回客户端之前暂存数据的内存。

关键点: 这些内存是按需分配 的。如果一个 SQL 不需要排序,就不会分配 sort_buffer。但如果设置得太大,成千上万个连接同时请求时,内存会瞬间被榨干。
管理与回收机制

(1) 空闲连接的管理

如果一个连接执行完任务后没有关闭(长连接),它会进入 Sleep 状态。

  • 保留资源:它依然占用线程栈和基础的网络缓存。

  • 释放资源 :它会释放掉执行 SQL 时临时申请的 sort_buffer 等。

  • 超时断开 :MySQL 通过 wait_timeout 参数控制空闲连接的寿命。如果一个连接空闲时间超过这个值,MySQL 会强制杀掉该连接并回收内存。

(2) 线程缓存 (Thread Cache)

为了避免频繁创建和销毁线程(这是很消耗资源的),MySQL 实现了一个 Thread Cache

  • 当客户端断开连接时,MySQL 不会直接销毁线程,而是把它放入缓存池中。

  • 当下个连接进来时,直接从池里捞出一个现成的线程使用。

  • thread_cache_size 参数控制缓存的数量。

常见指令:

sql 复制代码
show processlist // 查看 MySQL 服务端被多少客户端连接的情况
sql 复制代码
show variables like 'wait_timeout' // 查看 MySQL 中客户端连接的最大空闲时长
sql 复制代码
show variables like 'max_connections' // 查看 MySQL 中最大客户端连接数量
sql 复制代码
kill connection id // 删除 MySQL 的某个客户端连接,提供连接对应的 id 即可

(2)查询缓存

连接器建立连接之后,客户端就可以发送 SQL 语句给 MySQL 服务端了,拿到 SQL 语句先解析第一个字段查看是什么类型的 SQL 语句。如果发现是 SELECT,那就路由到查询缓存里,查询缓存里面的数据是k-v形式的键值对,key为 SQL 查询语句,value 为该查询语句返回的结果。如果拿着客户端发来的 SQL 语句命中了缓存,那就可以直接返回 value,反之再走下面的解析器层。

也许是受到了 Redis 的影响,让我一开始觉得这功能设计的很好。但仔细一想会发现,MySQL 里面的表不同于 专门做缓存的 Redis,MySQL 里面的数据肯定是更容易更新的,一旦表中数据更新那就意味着查询缓存失效,需要重建缓存。如果遇到一个重建条件非常苛刻的缓存,好不容易重建完,表又更新了,这缓存还一次没用呢,是不是就十分浪费资源。所以从 MySQL 8 起查询缓存这一环就直接被去除了。

(3)解析器

解析器主要做两件事,第一件事是词法分析,这一步主要是把输入转化成若干个Token,其中Token包含key和非key。比如,一个简单的SQL如下所示:

sql 复制代码
SELECT name FROM userInfo

分析之后,会得到4个Token,其中有2个Keyword,分别为select和from:

| 关键字 | 非关键字 | 关键字 | 非关键字 |

select name from userInfo

第二件事就是语法分析,拿到上一步词法分析的结果后,再判断 SQL 语句是否符合语法规范,如果没问题那就构建语法树,方便后续获取 SQL 类型、表名、字段名。如果写错了(如 selec),会报错:You have an error in your SQL syntax

(4)预处理器

如果写了一个语法完全正确的树,但是表或者字段不存在,还是在解析的时候报错,因为解析器处理之后,还有一个预处理器,它用来判断解析树的语义是否正确,也就是表名和字段名是否存在。预处理后生成一个新的解析树。此外它还可以进行扩展字段,比如将 select * 中的 * 符号,扩展为表上的所有列;

(5)优化器

这是 MySQL 的"大脑"。当一个 SQL 有多种执行路径时,优化器会决定使用哪一种。

  • 选择索引:如果表有多个索引,决定用哪个。

  • 多表关联(Join):决定表的连接顺序(先查小表还是先查大表)。

  • 目的 :寻找执行成本最低(通常是 IO 最少)的方案,生成执行计划

由于距离学 MySQL 已经过去好几个月了,早就已经忘记了 B+ 树索引、回表、覆盖索引这些概念了,所以当时看上图的案例有点晕,如果大家有相同的感觉可以跟我在下面一起复习:
Q:两种 B+ 树索引的区别

在 InnoDB 存储引擎中,根据叶子节点存放内容的不同,索引分为两类:

主键索引的 B+ 树(又称:聚簇索引 - Clustered Index)

  • 键值 :表的主键 id

  • 叶子节点存什么 :存放的是完整的整行行记录

  • 特点 :索引即数据。只要找到了主键 id,就能直接获取到这行数据的所有字段(product_no, name, price 等)。

二级索引的 B+ 树(又称:辅助索引 - Secondary Index)

  • 键值 :你在哪列建索引,键值就是哪列。比如图片中的 name

  • 叶子节点存什么 :存放的是对应记录的主键值

  • 特点 :它不包含整行数据。如果你通过 name 查到了某条记录,你只能顺便得到它的 id

Q:什么是"回表"?

通常情况下,如果你执行:
SELECT price FROM product WHERE name = 'apple';

  1. 查询会在 二级索引 (name) 的 B+ 树中找到 apple

  2. 从二级索引的叶子节点拿到对应的 主键 id(假设是 1)。

  3. 关键动作 :拿着 id=1 再去 主键索引 的 B+ 树里查一遍,为了拿到 price 字段。

这个回到主键索引再查一遍 的过程,就叫 "回表"。回表会增加磁盘 I/O,降低性能。
Q:什么是覆盖索引?

覆盖索引 并不是一种索引类型,而是一种查询现象

当一个索引包含(覆盖)了查询语句中需要的所有字段时,MySQL 就可以直接从这个索引中返回数据,而不需要回表

结合你图片中的例子:

查询语句:SELECT id FROM product WHERE id > 1 AND name LIKE 'i%';

  • 查询需要的字段id(在 SELECT 里)和 name(在 WHERE 里)。

  • 二级索引 name 的内容 :它本身是按 name 排序的,且它的叶子节点里存的就是 id

  • 结论 :这个 name 索引已经包含了你查询所需要的全部信息(nameid)。

此时:

MySQL 引擎只需要扫描 name 这棵 B+ 树,就能过滤出满足 i% 条件的记录,并且直接从这棵树里把 id 拿出来返回。它根本不需要去翻主键索引那棵树。 这就叫"覆盖索引"。
复习了上述概念我们也就能明白为什么优化器最后帮我们决定使用普通索引,做覆盖索引优化。而不是说直接查主键索引。一来是主键索引的叶子节点存储的信息过多,查询主键索引导致的磁盘 I/O 也就更多,自然更耗时间,相较之下二级索引的叶子节点里面只存了主键和索引字段。另一个原因就是我们查的是主键ID,它已经存在于二级索引的叶子节点了,因此没必要回表,节省了大笔开销。
常见指令:

sql 复制代码
explain + 查询 SQL 语句 // 输出这条 SQL 语句的执行计划

(6)执行器

开始执行 SQL。

  • 权限校验:在执行前,再次确认用户对该表是否有执行权限。

  • 调用接口:根据优化器生成的执行计划,循环调用存储引擎提供的 API 接口。

索引下堆:索引下堆是一种查询优化策略,它能够减少二级索引查询过程中的回表操作,其本质在于把应该由 Server 层做的判断交给了存储引擎层。在小林coding中案例如下:

使用索引下堆前后的关键我用红线标注了起来,主要就是 reward 是否等于 1000000 这个判断由 Server 层转交给了存储引擎层,它的好处在于不用为了让 Server 层判断这一条件而特意回表,可以看到如果该条件不成立完全可以在存储引擎层就直接跳过该二级索引,进而节省了一部分的回表操作开销。

2.存储引擎层

存储引擎是数据真正存放的地方。

  • 常见的引擎:InnoDB(默认,支持事务)、MyISAM。

  • 执行器每调用一次引擎接口,引擎就会去磁盘或内存(Buffer Pool)中查找数据,并将结果返回给执行器。

相关推荐
在西安放羊的牛油果2 小时前
Connect 源码深度解析
前端·架构·代码规范
hINs IONN2 小时前
maven导入spring框架
数据库·spring·maven
CV艺术家2 小时前
mysql数据迁移到达梦数据库
java·数据库
2301_822703202 小时前
大学生体质健康测试全景测绘台:基于鸿蒙Flutter的多维数据可视化与状态管理响应架构
算法·flutter·信息可视化·架构·开源·harmonyos·鸿蒙
独特的螺狮粉2 小时前
生命科学实验室经费极简记账簿:基于鸿蒙Flutter的极简主义状态响应与流式布局架构
flutter·华为·架构·开源·harmonyos
枫叶林FYL2 小时前
【自然语言处理 NLP】工具学习与Agent架构:从函数调用到多智能体协作
数据库
提子拌饭1332 小时前
红细胞代偿性增殖与睡眠剥夺的对照演算引擎:基于鸿蒙Flutter的微观流体力学粒子渲染架构
flutter·华为·架构·开源·harmonyos·鸿蒙
LcGero2 小时前
Cocos Creator 的 NPC AI 架构实现
人工智能·ai·架构·npc
不愿透露姓名的大鹏2 小时前
Oracle Undo空间爆满急救指南(含在线切换+更优方案+避坑指南)
linux·运维·数据库·oracle