一篇文章搞懂一条查询SQL是如何执行?

0 阅读7分钟

咱们平时看事情得先整体后局部,学MySQL也是一样的道理。直接说细节你可能会懵,不如先看看全貌。

比如最简单的表,只有一个ID字段,执行一句:

select * from T where ID=10

我们输入SQL,拿到结果,但MySQL内部是怎么工作的?今天我们就把MySQL拆开看看,了解里面的“零件”。

这样以后遇到问题,你就能更快地看透本质、找到解决方法了。

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

总的来说,MySQL 主要分成两大层:Server 层存储引擎层

Server 层主要负责处理客户端的连接、SQL 解析与执行等核心服务,包括连接器、查询缓存、分析器、优化器、执行器等模块。我们常用的内置函数(如日期、数学计算等),以及存储过程、视图、触发器这些跨引擎的功能,都在这一层实现。

存储引擎层则负责数据的实际存储和读写。MySQL 的存储引擎设计是插件式的,常见的包括 InnoDB、MyISAM、Memory 等。

目前最常用的是 InnoDB,并且从 MySQL 5.5.5 开始,它已经成为默认引擎。也就是说,建表时不指定引擎的话,就会自动使用 InnoDB。当然你也可以手动选择其他引擎,比如用 engine=memory来创建内存表。

不同的存储引擎存取数据的方式不同,功能也有差异,后续我们还会讨论如何选择合适的引擎。

值得注意的是,所有存储引擎都共用同一个 Server 层,也就是从连接器到执行器这一整套流程。

你可以先对这些组件名称有个印象,接下来我会用最开始那条 SQL 语句,带你走一遍完整执行过程,看看每个组件究竟做了什么。

第一步:连接到数据库时,负责接待你的就是连接器。

它的工作就是跟你的客户端建立联系、验证身份、管理连接状态。

一般我们通过下面的命令连接数据库:

mysql -h主机地址 -P端口 -u用户名 -p

输入命令后,会提示你输入密码。

⚠️ 虽然密码可以直接跟在 -p后面,但这样容易被别人看到,如果是线上数据库,千万不要这么做

这里的 mysql是客户端程序,用来跟 MySQL 服务端建立连接。一旦 TCP 握手完成,连接器就会验证你的用户名和密码。

  • 如果验证失败,会提示“Access denied”,连接直接终止。
  • 如果验证成功,连接器会查出你的权限,并且这个权限在本次连接中会一直生效
  • 也就是说,即使管理员中途修改了你的权限,已经连着的会话不会受影响,只有新建的连接才会用新权限。

连接成功后,如果没有操作,这个连接就处于空闲状态。

你可以执行 show processlist查看,如果看到一行 CommandSleep,就表示有一个空闲连接。

如果客户端长时间没动作,连接器会自动断开连接(由 wait_timeout控制,默认 8 小时)。

断开后客户端再发请求,就会收到“Lost connection to MySQL server”的错误,这时候就需要重新连接才能继续操作。

说到连接,一般有两种模式:

  • 长连接:连接成功后一直用同一个连接,只要持续有请求。
  • 短连接:执行几次查询就断开,下次查询重新建立。

因为建立连接的过程比较耗资源,所以建议尽量用长连接

不过一直用长连接的话,你可能会发现 MySQL 内存上涨比较快,这是因为 MySQL 使用的临时内存是跟着连接走的,只在连接断开时才释放。如果长连接很多,累积起来可能导致内存占用过高。

第二步:连接建立后,就可以执行查询了。下一步是查询缓存。

MySQL 收到查询请求时,会先到查询缓存里看看是否执行过相同的语句。

缓存以 key-value 形式存储,key 是查询语句,value 是查询结果

如果命中缓存,结果会直接返回,无需继续执行,效率很高。

但通常不建议使用查询缓存,因为它弊大于利。

原因在于,只要对表有更新,这个表的所有查询缓存都会被清空

对于频繁更新的数据库,缓存命中率极低,很可能刚存的结果就被清掉了。

除非你的业务表很少更新(比如系统配置表),才适合开启查询缓存。

MySQL 支持“按需使用”缓存:

将参数 query_cache_type设为 DEMAND,默认查询就不走缓存。

需要缓存时,用 SQL_CACHE显式指定,例如:

select SQL_CACHE * from T where ID=10;

注意:MySQL 8.0 版本已彻底移除查询缓存功能,以上内容主要针对 8.0 之前的版本。

第三步:如果查询没走缓存,接下来就进入分析器处理。

分析器会先理解你的 SQL 语句是做什么的,主要分两步:

  1. 词法分析
  2. MySQL 会识别你输入字符串中的每个部分。
  3. 比如,从 select知道是查询语句,把 T识别为表名,ID识别为列名。
  4. 语法分析
  5. 根据词法分析结果,检查 SQL 语句是否符合 MySQL 的语法规则。
  6. 如果写错了,会报错:You have an error in your SQL syntax
  7. 比如少写个字母:
elect * from t where ID=1;
--           ↑-- 错误提示会定位到 near 'elect...'
  1. 通常错误信息会给出第一个出错位置,可以注意“use near”后面的内容。

第四步:经过分析器,MySQL 知道你要做什么了,但执行前还要由优化器来制定执行计划。

优化器主要做两件事:

  • 有多个索引时,决定用哪个
  • 多表关联时,决定连接顺序

比如这个查询:

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

可以有两种执行方式:

  1. 从 t1 中找 c=10 的记录,关联 t2,再判断 t2.d=20。
  2. 从 t2 中找 d=20 的记录,关联 t1,再判断 t1.c=10。

两种方式结果一样,但效率可能不同,优化器 就负责选择更优的方案

确定方案后,就进入执行器阶段。

关于优化器如何选择索引、会不会选错等问题,我们后续会展开讲解。

第五步:经过分析器和优化器后,进入执行器阶段,开始执行语句。

执行前,先检查权限

如果你对表 T 没有查询权限,会报错:

ERROR 1142: SELECT command denied to user ...

注:实际工程中,权限验证可能在查询缓存返回时或优化器之前就做了。

如果有权限,执行器就打开表,并根据表的存储引擎调用对应接口。

以没有索引的表 T 为例,执行流程如下:

  1. 调用引擎接口取第一行,判断 ID 是否为 10,是则放入结果集,否则跳过。
  2. 继续调用引擎接口取下一行,重复判断,直到最后一行。
  3. 将所有满足条件的行作为结果集返回给客户端。

有索引的表执行逻辑类似,只是第一次调“取满足条件第一行”接口,之后循环调“取下一行”接口。

执行中扫描的行数,会在慢查询日志中以 rows_examined字段记录。

注意:执行器调用一次,引擎内部可能扫描多行,所以 rows_examined与引擎实际扫描行数不一定完全相同。

最后留个思考题给你:

如果表 T 中并没有字段 k,而你执行了语句:

select * from T where k=1;

显然会报错:“Unknown column ‘k’ in ‘where clause’”。

你觉得这个错误是在我们提到的哪个阶段报出来的?