当前许多OLTP应用都有数据读多写少的特点,所以平日里开发人员与数据库打交道最多的地方恐怕非写查询SQL莫属了。当执行
select * from Table where id = 'xxx'
类似这样的查询语句时,语句在数据库内部是怎么被执行的呢?下面就以MySQL数据库为例对该问题展开说明。
1. MySQL 基础架构
首先,放一张MySQL 的基础架构图。可以看到,MySQL架构主要由 Server 层 和 Storage Engines 层两部分构成。
其中,Server 层主要包括有:
- Connection Pool: 连接池,这里主要负责连接管理,授权认证等等功能;
- Parser: 解析器,主要负责查询语句的词法分析和语法分析;
- Optimizer: 优化器,主要负责执行计划的生成,通过查看执行计划,你也可以看到由执行计划生成的一些统计信息;
- Cache & Buffers:查询缓存,如果可以命中,则会直接返回查询结果;
- SQL Interface: SQL接口,执行DML,DDL 语句,存储存储过程、视图、触发器等等内容
而 Storage Engines 存储引擎层则主要有各种存储引擎的实现,包括比较常用的InnoDB
,MyISAM
等,值得一提的是, MySQL 设计将存储引擎层设计成了可插拔模式。另外,现在最常用的存储引擎是 InnoDB
,它从 MySQL 5.5.5 版本开始就成为了默认存储引擎。
2. 查询语句的执行过程
- 首先,得有客户端 Client. 客户端可以是 MySQL 自带的 shell 工具,也可以是像 Navicat 这样专门连接数据库的客户端软件,又或者就是你本地的编译好的程序代码。如果你是使用 Java 语言来编写连接MySQL 的程序代码的话,一般会用到 JDBC 驱动 jar 包。通过客户端,可以跟 Server 端建立起 TCP 连接,连接建立完成后,MySQL 服务端会对你的用户身份作校验,即,要求你输入账号和密码。类似于在shell 上敲下如下命令时:
1 | > mysql -u root -h localhost -P 3306 -p |
MySQL 会弹出一个 enter password
这么一个提示语。 当你输完密码后,如果密码跟用户名匹配不上,连接器会报错:Access denied for user
. 然后客户端便终止运行。
只有在正确输入完密码后,MySQL 连接器校验通过了,才可真正登录到Server端去执行SQL语句。
用户正确建立连接后如果没有后续动作,当前连接就处于一个 Sleep 状态。如果太长时间没有操作,那么 MySQL 会自动断开这个连接,这个闲置断开时长由参数
wait_timeout
控制,默认值是 8 小时。
连接建立完成后,就可以执行 select 语句了。执行逻辑就会来到第二步:查询缓存。MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,也就是缓存命中了的话,那么这个 value 就会被直接返回给客户端。
如果缓存没命中,那么就来到下一步,真正的要去执行这个查询语句了。因此执行逻辑的第三步,首先是去解析这个查询语句。
- 这一步的工作是交给 Parser 也就是分析器来完成的。
首先,是做词法分析,解析出这条查询语句的 SQL 字符串都有哪些词语组成,又分别代表什么意思。比如,当 Parser 读入 select
这个完整的单词时,便能初步识别出这是一个查询语句。
如果词法分析没出什么大问题的话,接下来就要做语法分析了。在这一步,MySQL 将会检查输入的查询语句是否满足MySQL 查询语句的语法要求。通常我们能见到的常见 SQL 报错如:
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” 的内容。
如果能正常通过语法分析并正确生成AST的话,意味着MySQL 此时已经明白你要做什么了。所以接下来,MySQL会去分析该怎么去做。
- 在查询语句真正被执行之前,要先经由MySQL优化器生成执行计划 ,执行流程流转到这一步,其工作交由 Optimizer 优化器来完成。
优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(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 有没有执行查询的权限,如果没有,就会返回没有权限的错误(在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。查询也会在优化器之前调用 precheck 验证权限)。如果有权限,那么就继续往下打开表执行。打开表时,会根据这个表定义时选用的存储引擎去使用对应的引擎接口。比如在查询语句
select * from t where id = 10
中,如果id字段上没有索引,那么执行流程是这样的:
- 调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是 10,如果不是则跳过,如果是则将这行存在结果集中;
- 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
- 执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
如果执行的查询语句查询条件id上带有索引,第一次调用的是“取满足条件的第一行”这个接口,之后循环取“满足条件的下一行”这个接口,这些接口都是引擎中已经定义好的。
至此,这个查询语句就执行完成了。
一个查询语句,在MySQL 内部被执行时,大抵都要经过如上那么些步骤,最终由执行器访问具体的存储引擎接口,取得所有符合查询条件的结果作为结果集返回给客户端。
参考内容:
1.《极客时间MySQL实战45讲》01