- 文档
- 教程
- 知识库
【免费下载链接】CS-Base
图解计算机网络、操作系统、计算机组成、数据库,共 1000 张图 + 50 万字,破除晦涩难懂的计算机基础知识,让天下没有难懂的八股文!🚀 在线阅读:https://xiaolincoding.com
<output_article_start_tag>
一条 select 语句在 MySQL 中到底发生了什么?——CS-Base 图解 MySQL 执行流程全拆解
本文基于 CS-Base 仓库的 《执行一条 select 语句,期间发生了什么?》 一文,逐层拆解 MySQL 执行一条SELECT查询语句的完整旅程:从客户端建立连接、身份鉴权,到查询缓存命中、SQL 解析,再到预处理、优化与执行三个阶段。读完本文,你将能清晰回答"MySQL 执行一条 select 语句,期间发生了什么",并掌握连接器、解析器、预处理器、优化器、执行器各自的职责边界,以及索引下推、回表、覆盖索引等关键概念在真实执行链路中的落点。
一、MySQL 的整体架构:Server 层与存储引擎层
在剖析执行流程之前,先建立全局视角。MySQL 的架构分为两层:
- Server 层:负责建立连接、分析和执行 SQL。MySQL 大多数核心功能模块都在这里实现,主要包括连接器、查询缓存、解析器、预处理器、优化器、执行器。此外,所有内置函数(日期、时间、数学、加密函数等)以及所有跨存储引擎的功能(存储过程、触发器、视图等)都在 Server 层实现。
- 存储引擎层:负责数据的存储和提取。支持 InnoDB、MyISAM、Memory 等多个存储引擎,不同存储引擎共用一个 Server 层。从 MySQL 5.5 版本开始,InnoDB 成为默认存储引擎,我们常说的索引数据结构就由存储引擎层实现。InnoDB 支持且默认使用 B+ 树索引,数据表中创建的主键索引和二级索引默认都使用 B+ 树索引。
关于 B+ 树索引的细节,可进一步阅读仓库中的 索引常见面试题、从数据页的角度看 B+ 树 与 为什么 MySQL 采用 B+ 树作为索引。
接下来,就按一条 SQL 查询语句的执行顺序,依次看每个功能模块的作用。
二、第一步:连接器(建立连接、校验身份、管理会话)
在 Linux 上使用 MySQL,首先要连接 MySQL 服务,然后才能执行 SQL:
# -h 指定 MySQL 服务的 IP 地址;连接本机 MySQL 服务时可省略该参数 # -u 指定用户名,管理员角色名为 root # -p 指定密码;为安全起见,建议不要在命令行直接写密码,而是通过交互对话输入 mysql -h$ip -u$user -p连接过程需要先经过TCP 三次握手(MySQL 基于 TCP 协议传输)。如果 MySQL 服务未启动,会收到连接失败报错;如果服务正常运行,完成 TCP 连接建立后,连接器开始校验用户名和密码:
- 用户名或密码不对,收到
Access denied for user错误,客户端程序结束执行; - 用户名和密码都正确,连接器会获取该用户的权限并保存起来,此后该连接内的任何操作,都基于连接建立时读到的权限做权限判断。
因此:一个用户建立连接后,即使管理员中途修改了该用户的权限,也不会影响已存在连接的权限;只有新建连接才会使用新的权限设置。
2.1 查看连接数:show processlist
想知道当前 MySQL 服务被多少个客户端连接,执行:
show processlist;结果中每个用户对应一行,Command列为Sleep表示该连接空闲(连上后没有再执行任何命令),Time列显示空闲时长(如 736 秒)。
2.2 空闲连接会一直占用吗:wait_timeout
MySQL 通过wait_timeout参数控制空闲连接的最大空闲时长,默认 8 小时(28800 秒):
mysql> show variables like 'wait_timeout'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | wait_timeout | 28800 | +---------------+-------+ 1 row in set (0.00 sec)超过该时长,连接器会自动断开空闲连接。也可以手动断开:
mysql> kill connection +6; Query OK, 0 rows affected (0.00 sec)注意:空闲连接被服务端主动断开后,客户端并不会立刻感知,直到客户端发起下一个请求,才会收到ERROR 2013 (HY000): Lost connection to MySQL server during query。
2.3 连接数限制:max_connections
MySQL 支持的最大连接数由max_connections参数控制,默认值通常是 151:
mysql> show variables like 'max_connections'; +-----------------+-------+ | Variable_name | Value | +-----------------+-------+ | max_connections | 151 | +-----------------+-------+ 1 row in set (0.00 sec)超过该值后,系统会拒绝新的连接请求,并报错Too many connections。
2.4 短连接 vs 长连接
MySQL 连接与 HTTP 类似,也有短连接和长连接之分:
// 短连接 连接 mysql 服务(TCP 三次握手) 执行sql 断开 mysql 服务(TCP 四次挥手) // 长连接 连接 mysql 服务(TCP 三次握手) 执行sql 执行sql 执行sql .... 断开 mysql 服务(TCP 四次挥手)长连接可以减少反复建立、断开连接的开销,一般推荐使用长连接。但长连接会带来内存占用增多的隐患:MySQL 在执行查询过程中临时使用内存管理连接对象,这些连接对象资源只有在连接断开时才释放。长连接累积很多时,MySQL 服务占用内存过大,有可能被系统强制杀掉,导致服务异常重启。
解决长连接占用内存有两种方式:
- 定期断开长连接:断开连接即释放连接占用的内存资源。
- 客户端主动重置连接:MySQL 5.7 实现了
mysql_reset_connection()函数接口(注意是接口函数而非命令)。客户端执行完大操作后,在代码中调用该函数重置连接,达到释放内存的效果。这个过程不需要重连、不需要重新做权限验证,但会把连接恢复到刚刚创建完成时的状态。
2.5 连接器小结
- 与客户端进行 TCP 三次握手建立连接;
- 校验客户端的用户名和密码,不对则报错;
- 校验通过后读取该用户的权限,后续权限逻辑判断都基于此时读取到的权限。
三、第二步:查询缓存(Query Cache,MySQL 8.0 已移除)
连接器工作完成后,客户端即可向 MySQL 发送 SQL 语句。MySQL 收到 SQL 后,会解析出 SQL 语句的第一个字段,判断语句类型:
- 如果是查询语句(select 语句),先去**查询缓存(Query Cache)**中查找,看看之前是否执行过这条命令;
- 查询缓存以 key-value 形式保存在内存中,key 为 SQL 查询语句,value 为 SQL 查询结果;
- 命中缓存则直接返回 value 给客户端;未命中则继续往下执行,执行完后将查询结果存入查询缓存。
但查询缓存其实挺鸡肋:对更新频繁的表,查询缓存命中率很低——只要表有更新操作,该表的查询缓存就会被清空。刚缓存了查询结果很大的数据、还没被使用时,表一更新缓存就被清空,等于缓存了个寂寞。
因此:
- MySQL 8.0 直接删除了查询缓存,8.0 开始执行 select 语句不再经过查询缓存阶段;
- MySQL 8.0 之前的版本,可通过将参数
query_cache_type设置为DEMAND关闭查询缓存。
注意:这里说的查询缓存是Server 层的查询缓存,MySQL 8.0 移除的也是它,并不是 InnoDB 存储引擎中的 Buffer Pool。Buffer Pool 的相关原理可参考仓库的 揭开 Buffer_Pool 的面纱。
四、第三步:解析 SQL(解析器)
在正式执行 SQL 前,MySQL 会先对 SQL 语句做解析,交由解析器完成。解析器做两件事:
- 词法分析:根据输入的字符串识别出关键字,构建出 SQL 语法树,方便后续模块获取 SQL 类型、表名、字段名、where 条件等;
- 语法分析:根据词法分析结果,按照语法规则判断输入 SQL 是否满足 MySQL 语法。
如果 SQL 语法不对,会在解析器阶段报错。例如把from写成form,MySQL 解析器就会报语法错误。
但注意:表不存在或字段不存在,并不是在解析器里判断的。《MySQL 45 讲》曾说是在解析器里做的,但结合 MySQL 源码(5.7 与 8.0)分析,解析器只负责构建语法树和检查语法,不会去查表或字段是否存在。那这个工作由谁做?——预处理阶段(prepare)。
五、第四步:执行 SQL(prepare → optimize → execute)
经过解析器后,进入执行 SQL 查询语句的流程。每条SELECT查询语句流程主要分为三个阶段:
- prepare 阶段:预处理阶段;
- optimize 阶段:优化阶段;
- execute 阶段:执行阶段。
5.1 预处理器(prepare)
预处理阶段做两件事:
- 检查 SQL 查询语句中的表或字段是否存在;
- 将
select *中的*符号扩展为表上的所有列。
例如下面这条语句,test表不存在,就会在 prepare 阶段报错:
mysql> select * from test; ERROR 1146 (42S02): Table 'mysql.test' doesn't exist关于"表/字段是否存在不是在解析器判断"的结论,可用 MySQL 8.0 源码佐证:报错"表不存在"时的函数调用栈显示,错误是在get_table_share()函数里报的,而该函数在 prepare 阶段被调用。
- 对 MySQL 8.0:表或字段存在性判断放在 prepare 阶段;
- 对 MySQL 5.7:判断工作在词法分析&语法分析之后、prepare 阶段之前做。
两者结论一致:都不在解析器里做。之所以 5.7 与 8.0 位置不同,是因为 MySQL 5.7 代码结构不佳,8.0 代码结构变化很大,将这项工作放入了 prepare 阶段。
5.2 优化器(optimize)
预处理之后,还需要为 SQL 查询语句制定一个执行计划,这个工作交由优化器完成。
优化器负责将 SQL 查询语句的执行方案确定下来。比如表里有多个索引时,优化器会基于查询成本考虑,决定选择使用哪个索引。以开头的查询语句select * from product where id = 1为例,很简单,就是选择使用主键索引。
想查看优化器选择了哪个索引,可在查询语句最前面加explain命令,输出执行计划。执行计划中的key表示执行过程中使用了哪个索引,比如key为PRIMARY表示使用了主键索引。
如果执行计划中key为 null,说明没有使用索引,会进行全表扫描(type = ALL),这是效率最低档次的扫描方式。
优化器选择索引的经典例子——覆盖索引:假设 product 表原本只有主键索引,现在将name设置为普通索引(二级索引),于是 product 表同时拥有主键索引(id)和普通索引(name)。执行查询:
select id from product where id > 1 and name like 'i%';这条语句既可使用主键索引,也可使用普通索引,但执行效率不同。这是覆盖索引场景:查询结果直接在二级索引就能找到(二级索引 B+ 树叶子节点存储的是主键值),没必要再走主键索引,因为查询主键索引 B+ 树的成本比二级索引 B+ 树大。优化器基于查询成本,会选择代价小的普通索引。
执行计划中可以看到使用了普通索引(name),Extra为Using index,表明使用了覆盖索引优化。关于覆盖索引、回表、二级索引 B+ 树结构,可参考 索引常见面试题 和 从数据页的角度看 B+ 树 的详细图解。
5.3 执行器(execute)
经历优化器后,执行方案确定,MySQL 真正开始执行语句,由执行器完成。执行过程中,执行器与存储引擎交互,交互以数据行为单位。
下面用三种执行方式说明执行器与存储引擎的交互过程:主键索引查询、全表扫描、索引下推。
5.3.1 主键索引查询
以select * from product where id = 1;为例。查询条件用到主键索引且是等值查询,主键 id 唯一,不会有 id 相同的记录,优化器决定选用访问类型为const,即使用主键索引查询一条记录。执行流程:
- 执行器第一次查询,调用
read_first_record函数指针指向的函数。因访问类型为 const,该指针指向 InnoDB 引擎索引查询接口,把条件id = 1交给存储引擎,让存储引擎定位符合条件的第一条记录; - 存储引擎通过主键索引的 B+ 树结构定位到 id = 1 的记录:记录不存在则向执行器上报"找不到"错误,查询结束;记录存在则将记录返回给执行器;
- 执行器从存储引擎读到记录后,判断记录是否符合查询条件:符合则发送给客户端,不符合则跳过;
- 执行器查询过程是 while 循环,会再查一次。这次因不是第一次查询,调用
read_record函数指针指向的函数;因访问类型为 const,该指针被指向一个永远返回 -1 的函数,调用后执行器退出循环,查询结束。
5.3.2 全表扫描
以select * from product where name = 'iphone';为例。查询条件没有用到索引,优化器选用访问类型为ALL(全表扫描)。执行流程:
- 执行器第一次查询,调用
read_first_record函数指针指向的函数。因访问类型为 all,该指针指向 InnoDB 引擎全扫描接口,让存储引擎读取表中第一条记录; - 执行器判断读到的记录 name 是否为 iphone:不是则跳过;是则将记录发给客户端。(Server 层每从存储引擎读到一条记录就会发送给客户端;客户端之所以最终直接显示所有记录,是因为客户端等查询语句查询完成后才显示所有记录);
- 执行器 while 循环继续查询,调用
read_record函数指针指向的函数(访问类型为 all,仍指向 InnoDB 引擎全扫描接口),接着让存储引擎读取下一条记录,执行器继续判断,不符合条件即跳过,符合则发送到客户端; - 重复上述过程,直到存储引擎读完表中所有记录,向执行器返回读取完毕信息;
- 执行器收到查询完毕信息,退出循环,停止查询。
5.3.3 索引下推(Index Condition Pushdown)
这部分非常适合讲索引下推(MySQL 5.6 推出的查询优化策略),这样能清楚知道"下推"这个动作下推到了哪里。
索引下推能够减少二级索引查询时的回表操作,提高查询效率——它把 Server 层部分负责的事情,交给存储引擎层处理。
举例:假设用户表对age和reward字段建立了联合索引(age, reward),执行查询:
select * from t_user where age > 20 and reward = 100000;联合索引遇到范围查询(>、<)就会停止匹配:age 字段能用到联合索引,但 reward 字段无法利用到索引。具体原因可参考 索引常见面试题 中"联合索引范围查询"小节的分析。
不使用索引下推(MySQL 5.6 之前)时,执行器与存储引擎的流程:
- Server 层首先调用存储引擎接口,定位到满足查询条件的第一条二级索引记录(即 age > 20 的第一条记录);
- 存储引擎根据二级索引 B+ 树快速定位到该记录,获取主键值,然后进行回表操作,将完整记录返回给 Server 层;
- Server 层判断该记录 reward 是否等于 100000:成立则发送给客户端,否则跳过;
- 继续向存储引擎索要下一条记录,存储引擎定位后获取主键值,再次回表,返回完整记录给 Server 层;
- 如此往复,直到存储引擎读完所有记录。
可见,没有索引下推时,每查询到一条二级索引记录都要回表,然后 Server 再判断 reward 是否等于 100000。
使用索引下推后,判断 reward 是否等于 100000 的工作交给存储引擎层:
- Server 层首先调用存储引擎接口,定位到满足查询条件的第一条二级索引记录(age > 20 的第一条记录);
- 存储引擎定位到二级索引后,先不执行回表,而是先判断该索引中包含的列(reward 列)的条件是否成立:条件不成立,直接跳过该二级索引;条件成立,则执行回表,将完整记录返回给 Server 层;
- Server 层判断其他查询条件(本例无其他条件)是否成立:成立则发送给客户端,否则跳过,然后向存储引擎索要下一条记录;
- 如此往复,直到存储引擎读完所有记录。
使用了索引下推后,虽然 reward 列无法使用到联合索引,但因为它包含在联合索引(age, reward)里,所以直接在存储引擎过滤出满足 reward = 100000 的记录后,才去回表获取完整记录,相比不使用索引下推节省了大量回表操作。
如何判断是否使用了索引下推:当执行计划里的Extra部分显示Using index condition,说明使用了索引下推。关于联合索引范围查询与索引下推的更完整分析,可继续阅读 索引常见面试题 中的对应小节(Q1~Q4 示例、key_len 判断等)。
六、总结:一条 select 语句的完整旅程
执行一条 SQL 查询语句,期间发生了什么?
- 连接器:建立连接、管理连接、校验用户身份;
- 查询缓存:查询语句命中缓存则直接返回,否则继续往下执行(MySQL 8.0 已删除该模块);
- 解析 SQL:通过解析器进行词法分析、语法分析,构建语法树,方便后续模块读取表名、字段、语句类型;
- 执行 SQL,共三个阶段:
- 预处理阶段:检查表或字段是否存在;将
select *中的*扩展为表上的所有列; - 优化阶段:基于查询成本,选择查询成本最小的执行计划;
- 执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端。
- 预处理阶段:检查表或字段是否存在;将
MySQL 执行一条 select 语句,核心脉络可以浓缩为:连接器建立连接并鉴权 → 查询缓存(8.0 起已移除)→ 解析器构建语法树 → 预处理器校验表/字段并展开*→ 优化器制定执行计划 → 执行器按计划与存储引擎逐行交互并返回结果。
七、延伸阅读
本文来自 CS-Base(图解计算机网络、操作系统、计算机组成、数据库)仓库的 MySQL 基础篇,仓库中还有大量与本文主题强相关的文章,可继续深入:
- 索引常见面试题:覆盖索引、回表、联合索引最左匹配、范围查询、索引下推、索引区分度等完整讲解;
- 从数据页的角度看 B+ 树:从数据页、页目录、B+ 树结构看 InnoDB 数据组织与查询过程;
- 为什么 MySQL 采用 B+ 树作为索引:B+ 树与 B 树、二叉树、Hash 的对比;
- 索引失效有哪些:索引失效的典型场景与原因;
- MySQL 一行记录是怎么存储的:行格式、表空间、数据页的存储细节;
- 揭开 Buffer_Pool 的面纱:区分 Server 层查询缓存与 InnoDB Buffer Pool。
</output_article_end_tag>
- 文档
- 教程
- 知识库
【免费下载链接】CS-Base
图解计算机网络、操作系统、计算机组成、数据库,共 1000 张图 + 50 万字,破除晦涩难懂的计算机基础知识,让天下没有难懂的八股文!🚀 在线阅读:https://xiaolincoding.com
相关推荐
Vue.js 源码分析:new Vue 实例化时到底发生了什么(初始化全流程拆解)
Vue.js 源码分析:new Vue 实例化时到底发生了什么(初始化全流程拆解) 导读 本文聚焦 Vue.js 源码中 new Vue options 这一入
文档教程前端JavaGuide项目解析:深入理解MySQL中SQL语句的执行过程
JavaGuide项目解析:深入理解MySQL中SQL语句的执行过程 本文基于JavaGuide开源项目,深度解析MySQL中SQL语句的完整执行流程,涵盖查询
文档教程后端数据库学习不再难:CS-Base 项目中 MySQL 图解的底层逻辑解析
数据库学习不再难:CS Base 项目中 MySQL 图解的底层逻辑解析 你是否还在为 MySQL 底层原理晦涩难懂而烦恼?是否面对 B+树、MVCC 等概念感
文档教程知识库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考