首页 > 编程笔记 > MySQL笔记 阅读:1

SQL语句的执行过程(非常详细)

MySQL 8.0 中 SQL 语句的执行流程如下图所示:


图 1 MySQL 8.0 中 SQL 语句的执行流程

MySQL 5.7 中 SQL 语句的执行流程如下图所示:


图 2 MySQL 5.7中SQL语句的执行流程

从图 1 和图 2 中可以看出,MySQL 8.0 和 MySQL 5.7 中 SQL 语句执行流程的主要区别在于是否存在查询缓存。

下面以 MySQL 5.7 为例,SQL 语句的执行流程分为 8 步:
  1. 客户端连接服务器端,需要经过一系列的权限验证;
  2. 权限验证通过后,根据SQL语句到查询缓存中进行查找,如果存在结果,则直接返回结果,否则进入下一步;
  3. 查询缓存中没有SQL语句,进入解析器;
  4. 解析器处理完后,进入优化器;
  5. 优化器进行优化后,生成对应的执行计划;
  6. 执行器按照执行计划逐步执行查询;
  7. 执行器与存储引擎进行交互,读取数据、过滤数据;
  8. 返回结果,并且将结果插入查询缓存中。

上面只是简单地描述了 SQL 语句的执行流程,大家对其中的细节还不是很清晰。接下来详细讲解每一步都做了哪些事情。当然,步骤 1) 和步骤 8) 就不多说了。

查询缓存

MySQL 接收到一个查询请求后,会先到查询缓存中查看之前是否执行过这条语句。之前执行过的语句及其结果可能会以 key-value 的形式被直接缓存在内存中,其中,key 表示查询语句,value 表示查询结果。

如果 SQL 语句能够匹配到查询缓存中的 key,那么这个 value 会被直接返回给客户端。如果 SQL 语句不在查询缓存中,就进入解析器。

SQL 语句执行完后,会将结果插入查询缓存中。如果 SQL 语句能命中查询缓存,那么 MySQL 不需要执行后面的复杂操作即可直接返回结果,效率会很高。但是,查询缓存的命中率往往不高,因此,从 MySQL 8.0 开始舍弃了这项功能。

为什么查询缓存的命中率不高呢?查询缓存就是提前把查询对应的结果缓存起来,下次不需要从磁盘中重新获取数据就可以直接拿到结果。

需要说明的是,MySQL 中的查询缓存不是缓存查询计划,而是缓存查询对应的结果。这就意味着查询匹配的鲁棒性大大降低,只有相同的 SQL 语句才会命中查询缓存。如果两个查询请求在任何字符上有不同之处,如空格、注释、字母大小写等,则都不会命中缓存,因此查询缓存的命中率不高。

如果查询请求中包含某些系统函数、用户自定义变量和函数、一些系统表,如 mysql、information_schema、performance_schema 数据库中的表,那么这个请求不会被缓存。

以某些系统函数为例,同一函数的两次调用可能会得到不同的结果,如函数 NOW(),每次调用都会给出最新的当前时间。如果在一个查询请求中调用了这个函数,那么,即使查询请求的文本信息都一样,不同时间的两次查询也会得到不同的结果。如果在第一次查询时缓存了结果,那么在第二次查询时直接使用第一次查询的结果是错误的。

此外,既然是缓存,就会有失效的时候。MySQL 的缓存系统会监测涉及的每张表,只要该表结构或其中的数据被修改,如对该表使用了 INSERT、UPDATE、DELETE、TRUNCATE TABLE、ALTER TABLE、DROP TABLE 或 DROP DATABASE 语句,使用该表的所有高速缓存查询都将变为无效并从高速缓存中删除。对于更新压力大的数据库来说,查询缓存的命中率会非常低。总之,查询缓存往往弊大于利。

如果用户使用的是 MySQL 8.0 以前的版本,则建议在静态表里使用查询缓存。所谓静态表,就是极少更新的表,如系统配置表、字典表等,这类表中的查询比较适合使用查询缓存。好在 MySQL 也提供了这种按需使用的方式。可以将 my.cnf 配置文件中 query_cache_type 变量的值设置为 DEMAND,意思是只有当 SQL 语句中包含 SQL_CACHE 关键字时才进行缓存。

query_cache_type 变量有 3 个值,分别是 0、1、2,它们的具体含义如下:
设置 query_cache_type=2,这样一来,对于默认的 SQL 语句都不使用查询缓存。而对于用户确定使用查询缓存的 SQL 语句,则可以用 SQL_CACHE 关键字显式指定,如下所示:
SELECT SQL_CACHE * FROM test WHERE id = 5;

可以使用如下 SQL 语句查看当前的 MySQL 实例是否开启了查询缓存。我们分别在 MySQL 5.7 和 MySQL 8.0 中进行查看,从结果中可以看到,在 MySQL 8.0 中已经没有了查询缓存。
# 在 MySQL 5.7 中查看
mysql> SHOW GLOBAL VARIABLES LIKE 'query_cache_type';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| query_cache_type | OFF   |
+------------------+-------+
1 row in set (0.00 sec)

# 在 MySQL 8.0 中查看
mysql> SHOW GLOBAL VARIABLES LIKE 'query_cache_type%';
Empty set (0.02 sec)

在 MySQL 5.7 中,可以使用如下 SQL 语句来查看查询缓存的命中率:
mysql> SHOW STATUS LIKE '%Qcache%';
+-------------------------+----------+
| Variable_name           | Value    |
+-------------------------+----------+
| Qcache_free_blocks      | 1        |
| Qcache_free_memory      | 1031832  |
| Qcache_hits             | 0        |
| Qcache_inserts          | 0        |
| Qcache_lowmem_prunes    | 0        |
| Qcache_not_cached       | 1280     |
| Qcache_queries_in_cache | 0        |
| Qcache_total_blocks     | 1        |
+-------------------------+----------+
8 rows in set (0.00 sec)

可以看到,结果中有 8 个参数,它们的具体含义如下表所示:

系统状态变量 含义说明
Qcache_free_blocks 查询缓存中目前剩余的块数;值较大说明内存碎片过多,可能需要整理。
Qcache_free_memory 查询缓存当前剩余内存大小;用于判断缓存是否够用,可按需调整。
Qcache_hits 命中查询缓存的次数;值越大,缓存效果越理想。
Qcache_inserts 未命中而插入缓存的次数;值越大说明缓存利用率越低(系统刚启动时空缓存属正常)。
Qcache_lowmem_prunes 因内存不足而被移出缓存的查询次数;可据此调整缓存大小。
Qcache_not_cached 因 query_cache_type 设置而未被缓存的查询数量。
Qcache_queries_in_cache 当前缓存中的查询数量。
Qcache_total_blocks 当前缓存中的块数量。

如果在查询缓存中没有找到 SQL 语句,接下来就进入解析器。

解析器

解析器负责对 SQL 语句进行词法分析和语法分析。如果没有命中查询缓存,就要开始真正解析 SQL 语句了。

SQL 语句的解析过程如下图所示:


图 3 SQL 词句的解析过程

首先进行词法分析,也就是将字符序列转换为单词(Token)序列的过程。这里会判断是否存在终结符。如果存在终结符,那么词法分析会返回一个一个的 Token。接着进行语法分析,即分析词法分析返回的 Token。

语法分析会对 SQL 语句进行一些语法检查,如单引号有没有闭合,之后根据 MySQL 定义的语法规则,将 SQL 语句分解成一个数据结构。我们把这个数据结构叫作抽象语法树(Abstract Syntax Tree,AST)。

输入的 SQL 语句是由多个字符串和空格组成的,MySQL 需要识别出里面的字符串分别是什么、代表什么,例如,MySQL 根据输入的“SELECT”关键字识别出这是一条查询语句。它也要把字符串“T”识别为“表名T”,把字符串“ID”识别为“列ID”。

MySQL 完成这些识别以后,就要进行语法分析。根据词法分析的结果,语法分析器会根据语法规则判断输入的这条 SQL 语句是否符合 MySQL 语法。如果 SQL 语句不合理,就会收到“You have an error in your SQL syntax”的错误提醒。例如,下面这条 SQL 语句将 FROM 写成了 ROM:
SELECT username ROM test WHERE id = 1 AND 1 = l;

错误提醒如下所示:
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds
to your MySQL server version for the right syntax to use near 'ROM test where id = 1' at line 1

如果 SQL 语句正确,就会生成一棵如下图所示的语法树”:


图 4 SQL 语句的语法树

至此,解析器的工作任务基本完成,接下来进入优化器。

优化器

在优化器中会确定 SQL 语句的执行路径,例如,是根据全表检索,还是根据索引检索等。

一条 SQL 语句可以有多个执行计划,但最终都返回相同的结果。优化器的作用就是找到最优的执行计划。例如,当表中有多个索引的时候,优化器决定使用哪个索引;当一条语句中有多表关联(JOIN)的时候,优化器决定这些表的连接顺序。执行如下 SQL 语句。这条语句的含义是执行两张表的 JOIN 操作,其中,表 test1 中的 id 列上存在索引,而表 test2 中的 id 列上不存在索引。

上述 SQL 语句既可以先在表 test1 中全表扫描 id 值,再根据 id 值关联查询表 test2,也可以先在表 test2 中全表扫描 id 值,再根据 id 值关联查询表 test1。

这两个执行计划返回的结果是一样的,但执行效率会有所不同,因为第一个执行计划会全表扫描表 test1 和 test2,而第二个执行计划只会全表扫描表 test2,继而根据索引查询表 test1。优化器的作用就是决定选用哪个执行计划。经过优化器的优化,即可确定这条 SQL 语句的执行计划,接下来进入执行器。

执行器

在执行 SQL 语句之前需要判断用户是否拥有权限,如果用户拥有权限,就执行该 SQL 语句并返回结果。

例如,如果用户对表拥有操作权限,就打开表继续执行。在打开表的时候,执行器会根据表的存储引擎定义去使用这个存储引擎提供的接口。

例如,在表 test 中,id 字段没有设置索引,执行器的执行流程如下:
至此,这条 SQL 语句就执行完成了。

相关文章