在日常工作总结中,数据库我们使用的比较多。
例如,有一个最简单的表,只有一个 id 字段,执行以下查询语句时:
csharp
select * from T where id = 1;
我们看到的只是输入一条语句并返回一个结果,但我们不知道该语句在MySQL内部是如何执行的。
这篇文章将从底层分析这条SQL语句的执行经历了哪些过程。
结构
- 连接:管理连接、权限验证。
- 解析器:语法分析。
- Query Cache:查询缓存,如果存在则直接返回。
- 优化器:索引选择、执行计划生成。
- 执行器:执行计划、结果检索。
- 存储引擎:读写接口、数据存储。
总的来说,MySQL可以分为两个主要部分:服务器层和存储引擎层。
服务器层包括连接器、查询缓存、解析器、优化器、执行器等组件。
它包含 MySQL 的大部分核心服务和功能,包括所有内置函数(例如日期、时间、数学和加密函数)。
所有独立于存储引擎的功能,如存储过程、触发器、视图等,都在这一层实现。
存储引擎层负责MySQL中的数据存储和检索。它支持多种存储引擎,包括InnoDB、MyISAM、Memory等。
目前最常用的存储引擎是InnoDB,从MySQL 5.5版本开始它成为默认的存储引擎。MySQL中执行CREATE TABLE建表时,如果不指定引擎类型,则默认使用InnoDB存储引擎。
当然,你也可以在创建表时通过添加参数ENGINE=MEMORY来指定使用Memory引擎。不同的存储引擎有不同的存储和访问表数据的方式,以及不同级别的支持功能。
从图中可以明显看出,不同的存储引擎共享一个公共的服务器层,其中包括连接器、查询缓存、解析器、优化器和执行器等组件。
各个组件的作用
在我们使用您提到的 SQL 语句深入了解执行过程之前,我们先简单了解一下每个组件的作用。
connector连接器
第一步,您将建立与数据库的连接,此时连接器就开始发挥作用。 连接器负责与客户端建立连接、处理身份验证并在整个生命周期中管理连接。
css
mysql -uroot -p
输入命令后,需要在交互会话中输入密码。 虽然可以使用 -p 将密码直接附加到命令中,但这样做可能会导致密码泄露。 如果您要连接到生产服务器,强烈建议不要这样做。
连接命令中的mysql是一个客户端工具,用于与服务器建立连接。
完成经典的 TCP 握手后,连接器开始使用您提供的用户名和密码来验证您的身份。
如果用户名或密码不正确,您将收到"用户错误导致访问被拒绝"的消息,并且客户端程序将终止其执行。
用户名和密码身份验证成功后,连接器将在权限表中查找经过身份验证的用户的权限。从此时起,连接内的授权逻辑将依赖于在此步骤中获得的权限。这意味着一旦用户成功建立连接,管理员对用户权限所做的任何后续更改都不会影响现有连接的权限。修改将仅应用于新连接,而现有连接将继续使用原始权限设置。
连接建立后,如果不执行任何进一步操作,连接将保持空闲状态。
您可以通过执行 show processlist 命令来查看此空闲连接。在文中提供的图中,Command 列显示 Sleep 的行表示系统中当前存在空闲连接。
如果客户端长时间保持不活动状态,连接器将自动断开连接。此空闲超时的持续时间由 wait_timeout 参数确定,默认值为 8 小时。如果连接断开,客户端再次发送请求,则会收到错误消息:在查询期间丢失与 MySQL 服务器的连接。要继续,将需要重新连接到服务器
在数据库上下文中,长连接是指如果客户端在成功连接后继续发送请求,则使用相同的连接。另一方面,短连接意味着执行几个查询后关闭连接,并为下一个查询建立新连接。
建立连接的过程通常很复杂,建议尽量减少连接建立操作的数量并尽可能使用长连接。但是,如果专门使用长连接,可能会遇到MySQL内存使用快速增加的情况。
这是因为MySQL在执行过程中使用的临时内存是在连接对象内管理的,这些资源只有在连接终止时才会被释放。因此,如果长连接日积月累,就会导致内存使用过多,系统可能会强制终止MySQL(OOM),从而导致异常重启。
针对这个问题,您可以考虑以下两种解决方案:
- 定期断开长连接:您可以在一段时间后或当您的程序确定已执行内存密集型查询时断开长连接,而不是无限期地保持长连接打开。当需要执行新的查询时,可以重新连接到数据库。这种方法有助于释放累积的内存资源。
- 利用"mysql_reset_connection" :如果您使用MySQL 5.7或更高版本,则可以在执行重要操作后使用"mysql_reset_connection"命令重新初始化连接资源。此过程不需要重新连接或重新验证权限,但会将连接重置为其首次建立时的初始状态。这有助于释放与先前操作相关的临时内存。
通过实施这些解决方案之一,您可以有效地管理内存使用情况并减轻长连接对 MySQL 性能的潜在影响。
Query Cache查询缓存
连接建立后,执行 SELECT 语句。这将我们带到第二步:查询缓存。
当MySQL收到查询请求时,首先检查查询缓存以确定该语句是否已经被执行过。先前执行的语句及其结果可以作为键值对直接缓存在内存中。
key代表查询语句,value代表查询结果。如果根据key可以在缓存中找到该查询,则直接返回对应的值给客户端。如果在查询缓存中没有找到该语句,则执行过程继续到后续阶段。执行完成后,结果将存储在查询缓存中。
如果查询到缓存,MySQL可以直接返回结果,而无需执行复杂的操作,从而提高效率。
但是,在大多数情况下,建议不要使用查询缓存。为什么?查询缓存通常弊大于利。
查询缓存失效率较高。每当对表进行更新时,该表的所有相应查询缓存条目都会失效并被清除。因此,存储结果所付出的努力可能是徒劳的,因为它们可以通过一次更新来清除。
对于更新压力较高的数据库,查询缓存命中率可能会非常低。它仅在您拥有很少更新的静态表的情况下才有用。
MySQL 提供了选择性使用查询缓存的选项。通过将 query_cache_type 参数设置为 DEMAND,将为默认 SQL 语句禁用查询缓存。对于要使用查询缓存的特定语句,可以显式指定 SQL_CACHE,如以下语句所示:
csharp
select SQL_CACHE * from T where id = 1
需要注意的是,查询缓存的全部功能在 MySQL 8.0 中被删除。从8.0版本开始,该功能被彻底消除。
Parser解析器
如果查询缓存没有命中,MySQL 将继续往下执行。首先,MySQL需要了解你想要做什么,因此它执行SQL语句解析。
解析器从词法分析开始。输入是由多个字符串和空格组成的 SQL 语句。 MySQL 需要识别每个字符串代表什么
根据您的输入,MySQL 根据关键字 select 识别为查询语句。它还将字符串 T 标识为表名 T,将字符串 id 标识为列 id。
在此识别过程之后,执行语法分析。语法分析器根据词法分析的结果判断SQL语句是否满足MySQL的语法规则。如果您的语句中存在语法错误,您将收到一条错误消息,指出您的 SQL 语法有错误。例如,下面的语句缺少 select 中的开头字母 s
sql
elect * from t where id = 1;
----------------------------------
Query 1 ERROR: 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
通常我们应该注意消息中"near"后面的内容。
Optimizer优化器
解析阶段之后,MySQL 就明白你想要做什么。在继续执行之前,它会经过优化器的处理。
当表中有多个索引时,优化器决定使用哪个索引。它还决定涉及多个表连接的语句中表的连接顺序。例如,考虑以下在两个表之间执行联接的语句:
csharp
select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20;
在这条SQL语句中,MySQL可以先从表t1中取出c=10的记录的ID值,然后根据ID值将它们与表t2连接,最后检查t2中d的值是否等于20。或者,也可以先从表t2中取出d=20的记录的ID值,然后根据ID值将它们与表t1连接,最后检查t1中c的值是否等于10。
两种执行方法产生相同的逻辑结果,但它们的效率可能不同。优化器的作用是决定选择哪个执行计划。
优化阶段完成后,该语句的执行计划就确定了,进入执行阶段。
Executor执行器
MySQL 通过解析器知道你想做什么,并通过优化器知道如何做。现在进入执行阶段并开始执行该语句。
执行过程中,首先会检查是否有查询表T所需的权限,如果没有,会返回权限不足的错误。
如果有所需的权限,执行器将继续打开表并开始执行语句。打开表时,执行器通过底层存储引擎提供的接口与底层存储引擎进行交互。
在我们的例子中,表T在ID列上没有索引,执行器中的执行流程如下:
- 执行器调用InnoDB引擎提供的接口获取表的第一行,检查ID值是否为10,如果不是,则跳到下一行。如果匹配,则该行存储在结果集中。
- 执行器继续调用引擎接口来获取"下一行",并重复相同的逻辑来检查ID值是否为10。这个过程一直持续到表的最后一行。
- 最后,执行器将所有满足条件的行组成的结果集返回给客户端。
在数据库的慢查询日志中,您可能会发现一个名为 rows_examined 的字段,该字段表示语句执行过程中扫描了多少行。每次执行器调用引擎检索行时,该值都会累积。
在某些情况下,从执行器到引擎的单个调用可能会导致在引擎内部扫描多行。因此,引擎扫描的行数可能并不匹配 rows_examined 的值。
今天给大家介绍一下MySQL的逻辑架构,希望大家对一条SQL语句的完整执行过程的各个阶段有一个初步的印象。
如果喜欢这篇文章,点赞支持一下,关注我第一时间查看更多内容!