跳至主要內容

select 执行流程

AruNi_Lu数据库MySQL约 2684 字大约 9 分钟

本文内容

前言

对于一个 CRUD Boy,每天最多使用的 SQL 语句非 select 莫属了吧。那么你知道一条 select 语句是如何执行的吗?

下面就以最常见的 MySQL 为例,来探究探究一条 select 语句到底要历经哪些坎坷,才能把数据呈现给我们。

1. 客户端连接服务器

想要执行 select 语句,首先肯定要与 MySQL 服务器进行连接,这个连接是通过 连接器 来完成的。

首先,运行中的服务器程序和客户端程序本质上都是计算机上的一个进程,所以客户端与服务器连接的过程本质上就是 进程间通信 的过程。

在 Linux 中,进程间的通信方式有管道、共享内存、信号量、消息队列、Socket 通信等。前面的几种都是在同一台主机上进行通信,如果需要跨网络与不同主机的进程进行通信,则需要使用 Socket 通信

ps:Socket 也支持同主机上的进程间通信。

在真实环境中,数据库服务器进程和客户端进程大概率是不在同一台主机上的,所以需要通过网络进行通信。

MySQL 采用的 Socket 类型是 TCP 字节流通信,MySQL 服务器在启动时会默认申请 3306 端口号,之后就在这个端口号上等待客户端进程的连接。

我们一般使用如下命令与 MySQL 服务器进行连接:

mysql -h$ip -u$username -P$port -p$password
  • -h:MySQL 服务器的 IP 地址,如果是本地连接可以省略;
  • -u:用户名,管理员为 root;
  • -P:指定端口号(注意是大写),使用默认端口可以省略;
  • -p:连接密码(注意是小写),命令中可以不填(比较安全),在交互对话中进行填写。

发起上面的命令后,就会进行 TCP 三次握手与服务端建立连接,然后就会校验客户端输入的用户名和密码,校验成功后就连接成功了,接下来就可以与 MySQL 服务器进行通信了,即可以发送 SQL 语句了。

客户端连接到 MySQL 后,一直不发起请求,会怎么样?

先来了解一下数据库里的 长连接短连接

  • 长连接:连接成功后,如果客户端有 N 个请求,则都会 复用 一个连接;
  • 短连接:每次执行完几次请求就断开连接,下次请求需要 重新建立 一个连接。

建立连接的过程是比较复杂的,因此我们一般都是使用 长连接

在长连接的情况下,连接完成后,如果客户端没有任何操作,那么连接器就会 自动断开连接。这个时间由参数 wait_timeout 控制,默认值是 8 小时。

连接器断开连接后,如果客户端再次发送请求,就会收到一个错误提醒:Lost connection to MySQL server during query。这时候就需要重新建立连接,再发送请求。

长连接的缺陷:可能会导致 MySQL 占用的内存涨的非常快。因为 MySQL 在 执行过程中临时使用的内存是管理在连接对象里面的,这些资源 在连接断开时才会释放。所以长期累积下来,可能导致内存占用太大,被 OS 强行杀掉(OOM),看来是就像是 MySQL 异常重启了。

解决方案

  • 定期断开长连接
  • 每次执行一个较大的操作后,通过 执行 mysql_reset_connection 来初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

2. select 执行流程

建立连接后,便可向 MySQL 服务器执行 SQL 语句,获取结果。

先从整体的角度来看看一条 select 请求会经历哪些过程,如下图所示:

查询语句执行流程

从上面可以看出一条 select 语句大致的执行流程如下:

  1. 首先会去 查询缓存 里面看之前是否执行过这条语句。查询缓存以 kv 形式存储在内存中,key 为 SQL 语句,value 为查询结果;

    注:由于缓存命中率不高,所以查询缓存在 MySQL 8.0 已移除。

  2. 来到 解析器,主要是进行 SQL 语法的分析,生成对应的语法树;

  3. 再到 执行器 执行 SQL 语句,不过在执行之前,会先经过 预处理器(判断字段是否存在)和 优化器(基于成本生成执行计划);

  4. 最后执行器会按照执行计划调用 存储引擎层,由存储引擎去真正地访问表中的数据

  5. 执行器获取到查询结果,返回给客户端。

可以发现,MySQL 执行请求的过程被划分为了 Server 层存储引擎层。Server 层不涉及真实数据的存取,而存储引擎层具有存取真实数据的功能。

接下来就看看各个阶段主要做了什么事情。

2.1 查询缓存

上面说到,在 MySQL 8.0 中已移除了查询缓存,来看看为什么呢?

查询缓存的设计是这样的:

  • 将之前查询的 SQL 语句作为 key,查询结果作为 value 存放在内存中;
  • 新的查询会先经过查询缓存,通过 Hash 索引判断缓存是否命中,若命中则可直接返回缓存结果。

这看上去和常见的缓存类似,没什么问题。但是查询缓存的 更新策略 是这样的:

  • 如果这张表有 更新操作,那么这个表的 所有查询缓存就会被清空

由于这种更新策略,当刚缓存了一系列的数据时,还没被使用呢,刚好这个表有个更新操作,那么这个缓存也就被清空了。

正是因为更新会导致查询缓存的命中率非常低,所以 MySQL 8.0 直接将查询缓存删除了。

2.2 解析阶段

查询缓存被删除后,SQL 语句的第一站就是 解析器,它会对 SQL 语句做解析。主要做两件事:

  • 根据 SQL 语句 构建出语法树,方便后面获取 SQL 语句的类型、表名、字段名、where 条件等;
  • 判断 SQL 语句是否满足 MySQL 的语法,即 检查是否有 SQL 语法错误。如果有误则会报错。

不过需要注意,表不存在或者字段不存在,不是在解析器里判断的,而是在下面的执行阶段做的

2.3 执行阶段

通过了解析阶段后,SQL 语句就来到了执行阶段,每条 select 语句在执行阶段主要会经历以下三个阶段:

  • prepare:预处理阶段;
  • optimize:优化阶段;
  • execute:真正的执行阶段。

阶段一:预处理阶段

在预处理阶段,预处理器 主要做两件事:

  • 判断表是否存在或字段是否存在
  • select * 中的 * 替换为表上所有的列

阶段二:优化阶段

在真正执行一条 select 语句之前,MySQL 在优化阶段的 优化器 会找出所有可以用来执行此语句的方案,然后 选择一个成本最低的方案,作为执行计划

如果想要知道优化器选择了哪种方案,可以在 SQL 语句前加上 explain 命令,让它输出 SQL 语句的执行计划,执行计划中会有扫描方式、使用的索引等信息。

在 MySQL 中执行成本主要由两个方面组成:

  • I/O 成本:当我们的数据是存储在磁盘上时,需要将数据从磁盘加载到内存中,这个加载时间就称为 I/O 成本;
  • CPU 成本:读取记录、判断记录是否满足搜索条件、对结果进行排序等等操作消耗的时间称为 CPU 成本;

阶段三:真正的执行阶段

选择出成本最低的执行方案后,才开始真正的执行阶段,执行阶段由 执行器 完成。

在执行的过程中,执行器会调用存储引擎层的 API,和存储引擎交互,交互是以记录为单位的

存储引擎层 每查出一条记录,就要返回给 Server层判断该记录是否满足查询条件每查询出一条记录就要返回判断):

  • 满足条件则将记录发送给客户端;

    是的没错,Server 层每从存储引擎读到一条符合条件的记录就会发送给客户端,之所以客户端显示的时候是直接显示所有记录的,是因为客户端是等查询语句查询完成后,才会显示出所有的记录。

  • 不满足则跳过,继续读取下一条记录。

当存储引擎把把表中的记录都读完时,就会像执行器返回读取完毕的信息,执行器收到后,就会停止查询。

停止查询后,执行器会提示客户端已经完成了查询操作,客户端就会显示出所有的查询记录了。

一条 SQL 语句到此也就执行完毕了。

3. 总结

通过了上面的学习,现在应该知道一条 select 语句经历了哪些:

  1. 查询缓存(MySQL 8.0 已移除):缓存命中则直接返回结果,否则继续往下走;
  2. 解析阶段:解析器对 SQL 进行语法解析,生成语法树、判断语法是否有误;
  3. 执行阶段
    • 预处理阶段:检查表或字段是否存在,将 * 替换成所有列;
    • 优化阶段:基于成本选择最优的执行计划;
    • 真正执行阶段:执行器根据执行计划从存储引擎读取记录,返回给客户端;

现在再来回看一开始的那张图,是不是就清晰多了:

查询语句执行流程

4. 参考文章

上次编辑于: