当咱们输入一条 SQL 查询语句时,发生了什么?

当咱们输入一条 SQL 查询语句时,发生了什么?

 

咱们常常说,看一个事儿千万不要直接陷入细节里,你应该先鸟瞰其全貌,这样可以帮助你从高维度理解问题。一样,对于 MySQL 的学习也是这样。平时咱们使用数据库,看到的一般都是一个总体。好比,你有个最简单的表,表里只有一个 ID 字段,在执行下面这个查询语句时:mysql

 复制代码sql

mysql> select * from T where ID= 10 ;数据库

咱们看到的只是输入一条语句,返回一个结果,殊不知道这条语句在 MySQL 内部的执行过程。后端

因此今天我想和你一块儿把 MySQL 拆解一下,看看里面都有哪些“零件”,但愿借由这个拆解过程,让你对 MySQL 有更深刻的理解。这样当咱们碰到 MySQL 的一些异常或者问题时,就可以直戳本质,更为快速地定位并解决问题。缓存

下面我给出的是 MySQL 的基本架构示意图,从中你能够清楚地看到 SQL 语句在 MySQL 的各个功能模块中的执行过程。服务器

当咱们输入一条 SQL 查询语句时,发生了什么?

 

MySQL 的逻辑架构图架构

大致来讲,MySQL 能够分为 Server 层和存储引擎层两部分。函数

Server 层包括链接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及全部的内置函数(如日期、时间、数学和加密函数等),全部跨存储引擎的功能都在这一层实现,好比存储过程、触发器、视图等。工具

而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。如今最经常使用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。学习

也就是说,你执行 create table 建表的时候,若是不指定引擎类型,默认使用的就是 InnoDB。不过,你也能够经过指定存储引擎的类型来选择别的引擎,好比在 create table 语句中使用 engine=memory, 来指定使用内存引擎建立表。不一样存储引擎的表数据存取方式不一样,支持的功能也不一样,在后面的文章中,咱们会讨论到引擎的选择。

从图中不难看出,不一样的存储引擎共用一个 Server 层,也就是从链接器到执行器的部分。你能够先对每一个组件的名字有个印象,接下来我会结合开头提到的那条 SQL 语句,带你走一遍整个执行流程,依次看下每一个组件的做用。

链接器

第一步,你会先链接到这个数据库上,这时候接待你的就是链接器。链接器负责跟客户端创建链接、获取权限、维持和管理链接。链接命令通常是这么写的:

 复制代码

mysql -h $ip -P $port -u $user - p

输完命令以后,你就须要在交互对话里面输入密码。虽然密码也能够直接跟在 -p 后面写在命令行中,但这样可能会致使你的密码泄露。若是你连的是生产服务器,强烈建议你不要这么作。

链接命令中的 mysql 是客户端工具,用来跟服务端创建链接。在完成经典的 TCP 握手后,链接器就要开始认证你的身份,这个时候用的就是你输入的用户名和密码。

  • 若是用户名或密码不对,你就会收到一个 "Access denied for user" 的错误,而后客户端程序结束执行。
  • 若是用户名密码认证经过,链接器会到权限表里面查出你拥有的权限。以后,这个链接里面的权限判断逻辑,都将依赖于此时读到的权限。

这就意味着,一个用户成功创建链接后,即便你用管理员帐号对这个用户的权限作了修改,也不会影响已经存在链接的权限。修改完成后,只有再新建的链接才会使用新的权限设置。

链接完成后,若是你没有后续的动做,这个链接就处于空闲状态,你能够在 show processlist 命令中看到它。文本中这个图是 show processlist 的结果,其中的 Command 列显示为“Sleep”的这一行,就表示如今系统里面有一个空闲链接。

当咱们输入一条 SQL 查询语句时,发生了什么?

 

客户端若是太长时间没动静,链接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。

若是在链接被断开以后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query。这时候若是你要继续,就须要重连,而后再执行请求了。

数据库里面,长链接是指链接成功后,若是客户端持续有请求,则一直使用同一个链接。短链接则是指每次执行完不多的几回查询就断开链接,下次查询再从新创建一个。

创建链接的过程一般是比较复杂的,因此我建议你在使用中要尽可能减小创建链接的动做,也就是尽可能使用长链接。

可是所有使用长链接后,你可能会发现,有些时候 MySQL 占用内存涨得特别快,这是由于 MySQL 在执行过程当中临时使用的内存是管理在链接对象里面的。这些资源会在链接断开的时候才释放。因此若是长链接累积下来,可能致使内存占用太大,被系统强行杀掉(OOM),从现象看就是 MySQL 异常重启了。

怎么解决这个问题呢?你能够考虑如下两种方案。

  1. 按期断开长链接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开链接,以后要查询再重连。
  2. 若是你用的是 MySQL 5.7 或更新版本,能够在每次执行一个比较大的操做后,经过执行 mysql_reset_connection 来从新初始化链接资源。这个过程不须要重连和从新作权限验证,可是会将链接恢复到刚刚建立完时的状态。

查询缓存

链接创建完成后,你就能够执行 select 语句了。执行逻辑就会来到第二步:查询缓存。

MySQL 拿到一个查询请求后,会先到查询缓存看看,以前是否是执行过这条语句。以前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。若是你的查询可以直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端。

若是语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你能够看到,若是查询命中缓存,MySQL 不须要执行后面的复杂操做,就能够直接返回结果,这个效率会很高。

可是大多数状况下我会建议你不要使用查询缓存,为何呢?由于查询缓存每每弊大于利。

查询缓存的失效很是频繁,只要有对一个表的更新,这个表上全部的查询缓存都会被清空。所以极可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库来讲,查询缓存的命中率会很是低。除非你的业务就是有一张静态表,很长时间才会更新一次。好比,一个系统配置表,那这张表上的查询才适合使用查询缓存。

好在 MySQL 也提供了这种“按需使用”的方式。你能够将参数 query_cache_type 设置成 DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。而对于你肯定要使用查询缓存的语句,能够用 SQL_CACHE 显式指定,像下面这个语句同样:

 复制代码

mysql> select SQL_CACHE * from T where ID= 10 ;

须要注意的是,MySQL 8.0 版本直接将查询缓存的整块功能删掉了,也就是说 8.0 开始完全没有这个功能了。

分析器

若是没有命中查询缓存,就要开始真正执行语句了。首先,MySQL 须要知道你要作什么,所以须要对 SQL 语句作解析。

分析器先会作“词法分析”。你输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 须要识别出里面的字符串分别是什么,表明什么。

MySQL 从你输入的 "select" 这个关键字识别出来,这是一个查询语句。它也要把字符串“T”识别成“表名 T”,把字符串“ID”识别成“列 ID”。

作完了这些识别之后,就要作“语法分析”。根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否知足 MySQL 语法。

若是你的语句不对,就会收到“You have an error in your SQL syntax”的错误提醒,好比下面这个语句 select 少打了开头的字母“s”。

 复制代码

mysql> elect * from t where ID= 1 ;

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 'elect * from t where ID= 1 ' at line 1

通常语法错误会提示第一个出现错误的位置,因此你要关注的是紧接“use near”的内容。

优化器

通过了分析器,MySQL 就知道你要作什么了。在开始执行以前,还要先通过优化器的处理。

优化器是在表里面有多个索引的时候,决定使用哪一个索引;或者在一个语句有多表关联(join)的时候,决定各个表的链接顺序。好比你执行下面这样的语句,这个语句是执行两个表的 join:

 复制代码

mysql> select * from t1 join t2 using(ID) where t1 .c= 10 and t2 .d= 20 ;

  • 既能够先从表 t1 里面取出 c=10 的记录的 ID 值,再根据 ID 值关联到表 t2,再判断 t2 里面 d 的值是否等于 20。
  • 也能够先从表 t2 里面取出 d=20 的记录的 ID 值,再根据 ID 值关联到 t1,再判断 t1 里面 c 的值是否等于 10。

这两种执行方法的逻辑结果是同样的,可是执行的效率会有不一样,而优化器的做用就是决定选择使用哪个方案。

优化器阶段完成后,这个语句的执行方案就肯定下来了,而后进入执行器阶段。若是你还有一些疑问,好比优化器是怎么选择索引的,有没有可能选择错等等,不要紧,我会在后面的文章中单独展开说明优化器的内容。

执行器

MySQL 经过分析器知道了你要作什么,经过优化器知道了该怎么作,因而就进入了执行器阶段,开始执行语句。

开始执行的时候,要先判断一下你对这个表 T 有没有执行查询的权限,若是没有,就会返回没有权限的错误,以下所示。

 复制代码

mysql> select * from T where ID= 10 ;

ERROR 1142 ( 42000 ): SELECT command denied to user 'b' @ 'localhost' for table 'T'

若是有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。

好比咱们这个例子中的表 T 中,ID 字段没有索引,那么执行器的执行流程是这样的:

  1. 调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是否是 10,若是不是则跳过,若是是则将这行存在结果集中;
  2. 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
  3. 执行器将上述遍历过程当中全部知足条件的行组成的记录集做为结果集返回给客户端。

至此,这个语句就执行完成了。

对于有索引的表,执行的逻辑也差很少。第一次调用的是“取知足条件的第一行”这个接口,以后循环取“知足条件的下一行”这个接口,这些接口都是引擎中已经定义好的。

你会在数据库的慢查询日志中看到一个 rows_examined 的字段,表示这个语句执行过程当中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。

在有些场景下,执行器调用一次,在引擎内部则扫描了多行,所以 引擎扫描行数跟 rows_examined 并非彻底相同的 。

欢迎你们加入Java后端高级技术群:479499375。

相关文章
相关标签/搜索