MySQL45讲-1-一条SQL查询语句是如何执行的?

这个专栏记录自己学习MySQL45讲的记录

MySQL结构

通常我们在使用MySQL时,不太关心底层是如何执行的,但是我们如果了解MySQL的结构,在排查问题时也会更加方便。

MySQL45讲-1-一条SQL查询语句是如何执行的?

大体来说,MySQL可以分为Server层和存储引擎层两部分,Server层包括连接器、查询缓存、分析器、优化器、执行器等,用来执行核心功能以及实现内置函数,所有的跨存储引擎的功能都在这一层上实现,如存储过程、触发器、视图等。

存储引擎层负责数据的存储和提取,并且是可拔插式的插件模式,可方便地切换存储引擎,目前最常用的是InnoDB引擎,并从MySQL5.5.5版本成为了默认的存储引擎。

连接器

连接器负责和客户端建立连接,获取权限,维持和管理连接。

连接命令一般如下所示:

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

输完命令之后,你就需要在交互对话里面输入密码。虽然密码也可以直接跟在 -p 后面写在命令行中,但这样可能会导致你的密码泄露。

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

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

这就意味着,一个用户成功建立连接后,即使我们用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。

连接完成后,即使没有后续动作,连接器也会帮我们维持这个连接,默认维持8个小时的连接,连接断开后,客户端再次发送请求将不被接收,需要重新连接方可建立。

由于连接的建立一般比较复杂,所以一般要尽量使用长连接,但是也不能全部都使用长连接,可能会使MySQL内存溢出,导致异常重启。

所以,在生产环境中,一般使用如下方案:

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

查询缓存

连接建立完成后,就可以执行select查询,MySQL得到SQL语句后,会先去缓存中是否执行过该语句,之前查询过的语句会以key-value的形式,直接缓存在内存中。key为查询的语句,value是查询的结果。

若语句不在缓存中,则执行后续操作,执行完成后,执行结果又将会被存入缓存中。但是,在生产环境中,并不推荐使用缓存。

因为查询缓存的失效十分频繁,只要有对于一个表的更新,这个表上的所有查询缓存都会被清空。因此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库来说,查询缓存的命中率会非常低。

除非你的业务就是有一张静态表,很长时间才会更新一次。比如,一个系统配置表,那这张表上的查询才适合使用查询缓存。

MySQL也提供了“按需使用的方式”,可以将参数 query_cache_type 设置成 DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,可以用 SQL_CACHE 显式指定。

如下所示:

select SQL_CACHE * from T where ID=10;

但要注意MySQL8.0起直接将查询缓存的整块功能都删掉了。

分析器

如果没有命中查询缓存,就要开始真正执行语句了。

MySQL首先会对SQL语句做“词法分析”,MySQL将会把由多个字符串和空格组合成的SQL语句分析识别得到每个字符串是什么,代表什么。

当做完了词法分析时,分析器又要做“语法分析”。根据上步分析得到的结果,分析SQL语句是否为MySQL的语法,若语法不对,则会提示“You have an error in your SQL syntax”。

一般语法错误会提示第一个出现错误的位置,所以我们要关注的是紧接“use near”的内容。

优化器

经过了分析器的分析,MySQL就知道我们要做什么了,但是在开始执行之前,还要经过优化器的处理,优化器是当表中有多个索引时,由优化器来分析决定使用哪个索引;或者一个语句有多表关联(join)时,决定各个表的连接顺序。

如下语句:

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 有没有执行查询的权限,如果没有,就会返回没有权限的错误,如下所示 (在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。查询也会在优化器之前调用 precheck 验证权限)。

mysql> select * from T where ID=10;
 
ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T'

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

如上述例子,若ID字段没有用索引,那么执行流程如下:

调用InnoDB引擎的接口读取每一行,判断是否满足选择条件,并将所有满足条件的行记录作为结果集返回给客户端。

在数据库的查询日志中有一个名为rows_examined的字段,表示语句执行过程中扫描了多少行,这个值便是执行器在每次调用引擎来获取数据行时累加得到的。但是在有些常见下,执行器调用一次,在引擎内部扫描了多行,所以引擎扫描行数跟 rows_examined 并不是完全相同的

上一篇:JavaScript之函数基础


下一篇:JavaScript之Window对象