目录
[四 . 基础架构:一条SQl查询语句是如何执行的?](#四 . 基础架构:一条SQl查询语句是如何执行的?)
[4.1 MySQL逻辑架构图:](#4.1 MySQL逻辑架构图:)
[4.2 MySQL的Server层和存储引擎层](#4.2 MySQL的Server层和存储引擎层)
[4.2.1 连接器](#4.2.1 连接器)
[4.2.1.1 解释](#4.2.1.1 解释)
[4.2.1.2 MySQL 异常重启 解决方案:](#4.2.1.2 MySQL 异常重启 解决方案:)
[4.2.1.2.1. 定期断开长连接:](#4.2.1.2.1. 定期断开长连接:)
[4.2.1.2.2. 初始化资源:](#4.2.1.2.2. 初始化资源:)
[4.2.2 查询缓存](#4.2.2 查询缓存)
[4.2.2.1 解释](#4.2.2.1 解释)
[4.2.2.2 解释大部分情况下建议不要使用查询缓存](#4.2.2.2 解释大部分情况下建议不要使用查询缓存)
[4.2.3 分析器](#4.2.3 分析器)
[4.2.4 优化器](#4.2.4 优化器)
[4.2.4.1 例子:](#4.2.4.1 例子:)
[4.2.4.2表的连接顺序优化 详细解释:](#4.2.4.2表的连接顺序优化 详细解释:)
[4.2.4.2.1 优化效果:](#4.2.4.2.1 优化效果:)
[4.2.4.2.1 .1内存占用:](#4.2.4.2.1 .1内存占用:)
[4.2.5 执行器](#4.2.5 执行器)
[4.2.5.1 解释](#4.2.5.1 解释)
[4.2.5.2 T表中ID没有索引](#4.2.5.2 T表中ID没有索引)
[4.2.5.4 有无索引的查询详细解释:](#4.2.5.4 有无索引的查询详细解释:)
[4.2.5.5 rows_examined 详细解释:](#4.2.5.5 rows_examined 详细解释:)
四 . 基础架构:一条SQl查询语句是如何执行的?
我们进行整体的分析,从MySQL的基础架构进行分析。
如我们看到这个查询语句:
java
mysql> select * from T where ID=10;
我们只能看到输入的一条语句,返回的结果,但是却不知道这条语句在MySQL内部的执行过程。
4.1 MySQL逻辑架构图:
客服端
MySQL可以Server层和存储引擎两部分。
4.2 MySQL的Server层和存储引擎层
Server层 :
包括连接器,查询缓存,分析器,优化器,执行器等,Server层还提供了许多内置函数,例如日期、时间、数学和加密函数等,用于方便地进行数据处理和计算,Server层实现的功能不依赖于具体的存储引擎,因此可以在不同的存储引擎之间共享和使用,这些功能包括存储过程、触发器、视图等,它们可以跨存储引擎使用,为数据库提供了更强大和灵活的功能。
详细解释:
存储过程、触发器和视图是数据库中的重要功能,它们可以在数据操作和管理中提供更高级的逻辑和灵活性。
存储过程是一段预定义的代码块,可以被调用执行。它允许开发人员将一系列的SQL语句和逻辑组织起来,形成一个可重复使用的模块。通过存储过程,可以降低网络通信的开销、提高性能,并且可以实现复杂的业务逻辑。
触发器是与表相关联的特殊类型的存储过程。它可以在指定的事件(例如插入、更新或删除数据)发生时自动触发执行一段代码。触发器允许我们在数据的变动发生时,对其进行额外的处理,实现数据完整性约束、审计跟踪等功能。
视图是虚拟的表,是根据查询语句的结果集创建的。它为用户提供了一种简化和抽象的方式来访问和操作数据,隐藏了底层表的复杂性。通过视图,我们可以实现数据的安全性控制(只显示部分字段)、数据的逻辑组织(合并多个表的数据)以及简化复杂的查询操作。
因为存储过程、触发器和视图等功能在数据库中扮演着重要的角色,并能提供更高级的数据处理和管理能力,所以被包括在MySQL的Server层中,以便跨存储引擎统一实现和使用。
存储引擎层:
负责数据的存储和提取。其架构模式是插件式的,支持InnoDB,MyISAM,Memory等多个存储引擎。目前最常用的引擎是InnoDB,从MySQL 5.5.5版本开始成为了默认存储引擎。
例子:
当我们执行create table 建表的时候,我们不进行指定引擎类型,默认使用的就是InnoDB。
不同的存储引擎共用一个Server层,也就是从连接器到执行器的部分,下面将会进行一遍执行流程的讲解,依次看每个组件的作用。
4.2.1 连接器
4.2.1.1 解释
我们一开始将会先进行连接到这个数据库上,这时候,第一个接待你的就是连接器,连接器负责与客服端建立连接,获取权限,维持和管理连接。
连接命令:
java
mysql -h$ip -P$port -u$user -p
输入命令后我们将需要在交互对话中输入密码。
也可以直接在-p后写,直接放到命令行,但是这样可能会导致密码泄露,尤其是连接的生产服务器。
连接命令中的mysql是客户端的工具,用来跟服务器建立连接。在进行经典的TCP握手后,连接器将进行认证身份,使用的是用户名和密码。
不正确的话,会收到Access denied for user的错误,然后客户端程序结束,如果正确的话,连接器将会进入到权限表中查询你拥有的权限,之后的连接里面的权限判断逻辑,都依赖此时读到的权限。
即使用管理员账号对这个用户进行了权限修改,也不会影响已经存在连接的权限。
修改完成后,只有再新建的连接才会使用新的权限设置。
连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,你可以在 show processlist
命令中看到它。文本中这个图是 show processlist 的结果,其中的 Command 列显示为
"Sleep"的这一行,就表示现在系统里面有一个空闲连接
客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控
制的,默认值是 8 小时。
如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost
connection to MySQL server during query。这时候如果你要继续,就需要重连,然后再执
行请求了。
数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。短连
接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。
建立连接的过程通常是比较复杂的,建议在使用中要尽量减少建立连接的动作,也就
是尽量使用长连接。
但是全部使用长连接后,你可能会发现,有些时候 MySQL 占用内存涨得特别快,这是因为
MySQL 在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时
候才释放。所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),
从现象看就是 MySQL 异常重启了。
4.2.1.2 MySQL 异常重启 解决方案:
4.2.1.2.1. 定期断开长连接:
使用一段时间后,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后再查询重连。
4.2.1.2.2. 初始化资源:
如果是MySQL5.7以上版本,我们可以再执行一个比较大的操作后,通过执行mysql_reset_connection来进行初始化连接资源。过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。
4.2.2 查询缓存
4.2.2.1 解释
连接建立完毕后,我们可以进行执行select语句,执行逻辑就会来到第二步,查询缓存
MySQL 拿到一条语句的时候,会先到查询缓存里面看看,看之前是不是执行过这条语句。
之前执行过的语句及其结果可能会以key-value对的形式,被直接缓存在内存中。
key是查询语句,value是查询结果,如果查询能直接在这个缓存里面找到Key,那么这个value就会被直接返回到客户端。
如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中,如果查询命中缓存,MySQL不需要执行后面的复杂操作,可以直接返回结果,效率很高。
4.2.2.2 解释大部分情况下建议不要使用查询缓存
查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因
此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。
对于更新压力大的数据库来说,查询缓存的命中率会非常低。除非你的业务就是有一张静态表,很长时间才会更新一次。比如,一个系统配置表,那这张表上的查询才适合使用查询缓存。
详细解释:
当对一个表进行更新操作时,包括插入、删除、修改等,该表上所有的查询缓存都会被清空。这意味着即使之前有查询结果被缓存起来了,但只要有更新操作,所有的缓存都会被清空。
这样的缓存失效机制导致了查询缓存命中率的降低,尤其是在更新压力大的数据库环境下。如果数据库经常有更新操作,那么查询缓存很可能无法发挥作用,因为缓存的结果很快就会被清空,需要重新执行查询。
因此,查询缓存更适合应用于静态表或者更新频率较低的表。比如系统配置表这种很长时间才会更新一次的表,查询缓存可以有效地提高查询性能,因为缓存的结果可以长时间保持有效,避免频繁的查询操作。
我们可以在MySQL中将参数query_cache_type设置成为demand,这样对于默认的SQL语句都不在使用查询缓存。
对于你想要进行查询缓存的语句,可以使用SQL_cache 显式指定
例子:
java
mysql> select SQL_CACHE * from T where ID=10;
注意:
MySQL 8.0版本直接将查询缓存整块功能进行删掉,8.0开始就没有这个功能了。
4.2.3 分析器
如果没有命中查询缓存,我们就会进行真正的执行语句。
首先,MySQL需要知道你要做什么,所以会对SQL语句进行解析。
词法分析:
输入的是多个字符串和空格组成的一条SQL语句,MySQL需要识别里面的字符串分别是什么,代表什么,MySQL从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串"T"
识别成"表名 T",把字符串"ID"识别成"列 ID",然后下一步将是语法分析。
语法分析:
根据词法分析的结果,语法分析器会根据语法规
则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。
如果你的语句不对,就会收到"You have an error in your SQL syntax"的错误提醒,比如
下面这个语句 select 少打了开头的字母"s"
java
mysql> elect * from t where ID=1;
ERROR 1064 (42000): You have an error in your SQL syntax;
一般语法错误提示将会进行提示第一个错误的位置。
4.2.4 优化器
经过分析器后,MySQL就知道你要做什么了,在开始执行之前,我们还要进行优化器的处理,优化器是在表里面有多个索引的时候,决定采用哪一个索引,或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。
4.2.4.1 例子:
sql
SELECT *
FROM A INNER JOIN B ON A.id = B.user_id
WHERE A.name = 'John' AND B.address = '123 Street';
优化器会根据查询语句和表的索引来确定使用哪个索引和连接顺序。
首先,优化器会考虑过滤条件。在这个查询中,我们有两个过滤条件:A.name = 'John' 和 B.address = '123 Street'。其中,A.name = 'John' 使用了表A的列name,并且表A上有一个名为idx_name的索引,因此优化器可能会选择使用idx_name索引来加速查询。
接下来,优化器会考虑连接顺序。在这个查询中,我们有一个INNER JOIN操作,连接条件是A.id = B.user_id。优化器会分析表A和表B之间的关系以及它们的大小、索引等信息,然后决定连接顺序。
假设表A比表B小,那么优化器可能会选择先读取表A,然后根据连接条件A.id = B.user_id来获取匹配的行。由于A.id和B.user_id都没有索引,优化器可能会执行全表扫描来进行连接操作。
但如果表A非常大,而表B非常小,并且idx_user_id索引在表B上能够更好地支持连接操作,那么优化器可能会选择先读取表B,并使用idx_user_id索引来快速定位匹配的行。然后再根据连接条件A.id = B.user_id来获取表A中的相应行。
在多表关联的查询中,优化器会通过分析查询语句、表的大小和索引等信息,来决定使用哪个索引以及表的连接顺序。这样可以最大程度地提高查询性能和效率。
4.2.4.2表的连接顺序优化 详细解释:
多表关联查询中,选择先读取较小的表并根据连接条件获取匹配的行。
4.2.4.2.1 优化效果:
4.2.4.2.1 .1内存占用:
通过先读取较小的表,可以使得该表的数据能够完全加载到内存中。这样,在进行连接操作时,只需要保留相对较小的表的数据和相关索引,可以减少内存占用。而如果先读取较大的表,则可能需要将较大表的数据逐步加载到内存中,会占用更多的内存资源。
4.2.4.2.1.2磁盘IO开销:
通过先读取较小的表,可以减少磁盘IO次数。当进行连接操作时,需要根据连接条件在两个表之间进行数据匹配。如果先读取较小的表,并使用连接条件过滤出匹配的行,可以减少后续连接操作所需处理的数据量。而如果先读取较大的表,则可能需要处理更多的数据行,导致磁盘IO开销增加。
4.2.5 执行器
4.2.5.1 解释
MySQL通过分析器知道SQl语句要做什么,通过优化器知道了应该怎么做最优,然后进行执行器,开始执行语句。
开始执行的时候将会先进行判断你对这个表T有没有执行查询权限的权限,没有的话,就会返回没有权限的错误,如果进行了命中缓存,我们将在查询缓存返回结果的时候,进行权限验证。
查询也会在优化器之前调用 precheck 验证权限)。
java
mysql> select * from T where ID=10;
ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T'
如果具有权限,我们就打开表继续执行,打开表的时候,执行器将会根据表的引擎定义,使用引擎提供的接口。
4.2.5.2 T表中ID没有索引
我们执行器的执行流程就是:
1.调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是 10,如果不是则跳过,如果
是则将这行存在结果集中;
-
调用引擎接口取"下一行",重复相同的判断逻辑,直到取到这个表的最后一行。
-
执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
至此语句就结束了。
4.2.5.3 有索引的表
则是:
第一次调用的是"取满足条件的第一行"这个接口,
之后循环取"满足条件的下一行"这个接口,这些接口都是引擎中已经定义好的。
4.2.5.4 有无索引的查询详细解释:
有无索引对执行器的执行流程和性能有着重要影响。索引是一种数据结构,用于提高数据库查询操作的速度和效率。在有无索引的情况下,执行器的执行流程会有所区别。
-
无索引的表:
- 执行器会依次读取表中的每一行数据,并判断ID值是否为10。
- 如果ID不是10,则跳过该行继续读取下一行。
- 如果ID是10,则将该行数据添加到结果集中。
- 执行器会持续循环这个过程,直到遍历完整个表。
由于没有索引的支持,无索引的表需要逐行扫描并判断条件,这称为全表扫描。这种方式在数据量大的情况下,性能较差,因为需要消耗更多的IO和处理资源。
-
有索引的表:
- 执行器会使用索引来加速查询。根据索引的定义和查询条件,执行器可以快速定位满足条件的第一行数据。
- 之后,执行器会通过引擎接口获取满足条件的下一行数据,直到达到查询结果的要求或遍历完索引。
有索引的表可以利用索引进行快速定位和筛选数据,避免全表扫描。这样可以大幅提高查询性能,减少IO开销和资源消耗。索引可以根据查询条件的列或列组合来创建,以支持特定的查询操作。
有无索引对执行器的执行流程和性能有显著影响。无索引的表需要逐行扫描并判断条件,而有索引的表可以利用索引快速定位和筛选数据。通过合适地创建和使用索引,可以大幅提高数据库查询的效率。
最后我们可以在数据库慢查询日志中看到一个rows_examined的字段,表示语句执行过程中扫描了多少行,这个值就是执行器每次调用引擎获取数据行的时候累加的。
在有些场景下,执行器调用一次,在引擎内部则扫描了多行,因此引擎扫描行数跟
rows_examined 并不是完全相同的。
4.2.5.5 rows_examined 详细解释:
在某些查询场景下,执行器可能需要扫描多行数据才能完成查询操作。这是因为查询可能需要访问多个表或者涉及复杂的查询条件,执行器需要逐行扫描这些数据以便找到满足查询条件的结果集。可能会出现以下情况:
-
聚合查询:当执行聚合查询(例如 SUM、COUNT、AVG)时,执行器需要扫描多行数据并对其进行计算,以得出最终的聚合结果。
-
连接查询:当执行连接查询(例如使用 JOIN 操作符连接多个表)时,执行器需要扫描多行来匹配连接条件,并返回满足条件的结果。
-
子查询:当执行包含子查询的语句时,在子查询执行过程中可能需要扫描多行数据并将结果传递给外部查询。
这意味着在某些查询场景下,执行器可能需要扫描多行数据以便完成所需的操作,而不仅仅是返回给用户的结果集所包含的行数。
在执行器返回结果集时,引擎会将实际扫描的行数汇总为 rows_examined 并返回给用户。因此,rows_examined 是评估查询性能和优化查询计划的指标之一。如果 rows_examined 远大于实际所需的行数,那么可能需要进一步考虑优化查询条件、添加索引或调整表结构等方法来提高查询性能。