1、如何执行一条 SQL 查询语句
做一件事情我们最好先鸟瞰全貌,从高纬度理解问题。对于 MySQL 的学习我们也是这样。
平时我们使用数据库,看到的一般都是一个整体。
比如:一个最简单的表 test,表中只有一个字段 ID,我们执行下面的查询语句
SELECT * FROM test WHERE ID = 1;
我们看到的仅仅只有输入一条语句,返回一个结果,却不了解这条语句在 MySQL 内的执行过程
所以今天让我们一起把 MySQL 拆开,看看里面都有哪些零件,通过这个拆解让我们对 MySQL 有更深入的理解,让我们在碰到 MySQL 的一些异常或者问题的时候,能够快速定位并解决问题
1.1 MySQL基础架构
下面是 MySQL 的基本架构示意图,可以清楚地看到 SQL 语句在 MySQL 的各个功能模块中的执行顺序
总体来说,MySQL 可以分为 Server 层和存储引擎两部分
- Server 层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖了 MySQL 的大多数核心功能服务,以及所有的内置函数(日期、时间、数学、加密等),所有的跨库存引擎都在该层实现,比如存储过程、触发器、视图等
- 存储引擎负责数据的存储和提取,架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在常用的是 InnoDB,MySQL 5.5.5 版本就开始成为了默认存储引擎
1.1.1 连接器
首先,我们要先连接到这个数据库,连接器就是负责与客户端建立连接、获取权限、维持和管理连接连接命令如下:
mysql -hport -u$user -p
输入完命令后,我们在交互中输入密码,也可以直接将密码跟在 -p 后面写在命令中(不推荐)
连接命令中的 MySQL 是客户端工具,用来与服务端建立连接。在完成 TCP 握手后,连接器就会对输入的用户名和密码进行身份认证
如果输入的用户名或者密码不对,就会收到一个 Access denied for user
的报错,然后客户端程序结束执行。
认证通过后,连接器会到权限表里面查出来该用户拥有的权限,之后连接内的权限都会依赖于这个时候读取到的权限,也就意味着如果一个用户成功连接 MySQL,即使对用户权限进行了修改,也不会影响已经成功连接的权限,重新连接后才会使用新的权限
连接完成后如果没有后续操作的话,这个连接就处于空闲状态,我们可以使用以下命令来看到它
show processlist;
如下图所示:
如果 Command 列为 sleep 的就代表着空闲的连接
客户端如果长时间没有操作,连接器会在超时后自动断开连接,超时时间由参数 wait_timeout
控制,默认 8 小时。
查看等待超时时间命令:
show variables like ‘wait_timeout’
连接断开后客户端再发送请求的话,会受到报错:Lost connection to MySQL server during query
,需要重连后再执行请求
在数据库中,长链接是指连接成功后,如果客户端持有请求,则一直使用同一个连接。短连接则是每次执行完较少的几次查询就断开连接,下次查询再重新建立一个新的连接
建立连接的过程通常是较为复杂的,我们最好使用长链接,尽量减少建立连接的动作
如果全部使用长链接的话,MySQL 可能占用内存会涨的比较快,这是因为 MySQL 在执行过程中临时使用的内存是管理在连接对象中的。这些资源会在连接断开的时候释放,如果长连接累积可能导致内存占用太大 OOM,被系统强行杀掉,这样的话表现出来就是 MySQL 异常重启了
解决这个问题的办法可以考虑以下方案:
- 定期断开长连接,使用一段时间后,或者判断执行了一个占用内存大的查询后,断开连接,之后要查询再重连
- 如果是 MySQL 5.7 或者更新版本,可以在执行一个比较大的操作后,通过执行
mysql_resert_connection
来重新初始化连接资源。这个过程不需要重连和重新进行权限认证,但是会将连接恢复到刚创建完的状态
1.1.2 查询缓存
建立完连接后,我们就可以执行 select 语句了。执行逻辑就会来到第二步:查询缓存
MySQL 拿到一个查询的请求后,会先到查询缓存中查看,之前是否执行过这条语句。之前执行过的语句及其结果可能会以 key-value
的形式,被直接缓存在内存中。key 是查询语句,value 是查询返回的结果。如果查询能够直接在缓存中找到这个 key,这个 value 就会直接返回给客户端
如果语句不在缓存中查询的话,就会继续后面的执行阶段。执行完操作后,执行结果会存入查询缓存中。如果查询命中缓存的话,MySQL 的执行效率很高。
但是多数情况下,我们不建议使用缓存,因为缓存往往弊大于利
查询缓存的失效很频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。对于更新较频繁的表来说,查询缓存的命中率很低,除非业务表是一张静态表,很久才更新一次,比如系统配置表,这种的查询才比较适合使用缓存
MySQL 提供了可选择是否使用缓存的方式,我们将参数 query_cache_type
设置为 DEMAND,这样对于默认的 SQL 语句都不会使用缓存,想要使用的话用 SQL_CACHE
显示的指定,例如下面的 SQL
SELECT SQL_CACHE * FROM test WHERE ID = 1;
注意!!!MySQL 8 版本以后都没有这个功能了!!!
1.1.3 分析器
如果没有命中查询缓存,就要真正的执行 SQL 语句了,MySQL 会先对 SQL 语句进行解析
分析器会先对我们的 SQL 进行词法分析,我们输入的是由多个字符串和空格组成的一条 SQL,MySQL 需要识别 SQL 中的字符串分别是什么,又代表什么
MySQL 从我们输入的 SELECT
这个关键字识别出来这是一个查询语句,把字符串 test
识别成表名 test
,把字符串 ID
识别为 列 ID
词法分析完成后,就要做语法分析,根据词法分析的结果,语法分析器根据语法规则,判断输入的 SQL 语句是否满足 MySQL 语法
如果 SQL 语法不对,会收到 You have an error in your SQL syntax
的报错提示,提示一般是第一个出现错误的位置,下面是个错误示例:
查询的 SELECT 少一个字母 E
SLECT * FROM test WHERE ID = 1;
1.1.4 优化器
经过了分析器,MySQL 在执行前还要经过优化器的处理
优化器是在表里有多个索引的时候,决定使用哪个索引。或者在一个表有多表关联查询的时候,决定表的连接顺序
例如下面的 SQL,执行两个表的 join:
SELECT * FROM test1 join test2 using(ID) WHERE test1.a = 2 AND test2.b = 3;
这条 SQL 既可以先从 test1 表里面取出 a = 2 的记录的 ID,在根据 ID 值关联到 test2 表,再判断 test2 里面的 b 的值是否等于 3。
也可以从表 test2 里面取出来 b = 3 的记录的 ID,然后再用 ID 值关联到 test1,再判断 test1 里面的 a 的值是否等于 2。
两种方法执行的结果是一样的,但执行的效率可能不同,优化器的作用就是决定选择哪一种方案执行
优化器阶段完成后,SQL 语句的执行方案就确定下来了,然后进入执行器阶段
1.1.5 执行器
通过分析器知道要做什么,通过优化器知道该怎么做,进入执行阶段开始执行语句
执行开始会先判断对表 test 是否有查询的权限,如果没有就会返回没有权限的报错,如下:
SELECT * FROM test WHERE ID = 1;
ERROR 1142(42000): SELECT command denied to user ‘b’@‘localhost’ for table ’test’
有权限就继续执行,打开表的时候,执行器根据表的引擎定义,去使用这个引擎提供的接口。
比如我们这个例子中的表 test,ID 字段没有索引的话,执行器的执行流程是这个样的:
- 调用 InnoDB 引擎接口获取表的第一行,判断 ID 是不是 1,如果不是则跳过,如果是的话将这行存到结果集中
- 调用引擎接口获取下一行,重复判断逻辑,直到表的最后一行
- 执行器将上述遍历中所有满足条件的行组成的记录作为结果集返回给客户端
一个 SQL 语句就这么执行完成了
如果有索引的话,执行逻辑也类似,第一次调用的是 取满足条件的第一行
这个接口,之后循环这个接口,这些接口都是引擎中已经定义好的
我们可以在数据库的慢查询日志中看到一个 rows_examined
的字段,这个标识语句在执行过程中扫描了多少行,这个值是在执行器每次调用引擎获取数据行的时候累加的。要注意引擎扫描行数跟 rows_examined
并不是完全相同的
小结
以上介绍了一下 MySQL 的逻辑架构,让我们对一个 SQL 语句完整的执行流程的各个阶段有了初步的认知,后续的章节中再继续介绍 MySQL 的其它知识
问题
提出一个问题,如果表 test 中没有字段 c,当你执行了 SQL 语句 SELECT * FROM test WHERE c = 1;
,那肯定会报错 Unknown column ‘c’ in where clause
,这个错误是从上面提到的哪个阶段报出来的呢?