MySQL 简单查询语句执行过程分析(三)从存储引擎读数据

本文详细解析了MySQL InnoDB存储引擎在执行查询语句时,如何创建InnoDB实例、建立索引映射、创建查询模板,并重点阐述了如何根据查询条件选择索引、是否需要回表以及从BufferPool读取记录的预读缓存策略。内容涵盖查询优化阶段确定的索引使用、回表逻辑以及预读缓存的工作机制,帮助读者理解InnoDB内部的数据读取流程。

本文是 MySQL 简单查询语句执行过程分析 6 篇中的第 3 篇,第 1 ~ 2 篇请看这里:

MySQL 简单查询语句执行过程分析(一)词法分析 & 语法分析

MySQL 简单查询语句执行过程分析(二)查询准备阶段

示例表及 SQL 如下:

-- 表结构
CREATE TABLE `t_recbuf` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `i1` int(10) unsigned DEFAULT '0',
  `str1` varchar(32) DEFAULT '',
  `str2` varchar(255) DEFAULT '',
  `c1` char(11) DEFAULT '',
  `e1` enum('北京','上海','广州','深圳','天津','杭州','成都','重庆','苏州','南京','洽尔滨','沈阳','长春','厦门','福州','南昌','泉州','德清','长沙','武汉') DEFAULT '北京',
  `s1` set('吃','喝','玩','乐','衣','食','住','行','前后','左右','上下','里外','远近','长短','黑白','水星','金星','地球','火星','木星','土星','天王星','海王星','冥王星') DEFAULT '',
  `bit1` bit(8) DEFAULT b'0',
  `bit2` bit(17) DEFAULT b'0',
  `blob1` blob,
  `d1` decimal(10,2) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2001 DEFAULT CHARSET=utf8;

-- 查询语句
select * from t_recbuf where i1 > 49276

本文讲述从 InnoDB 存储引擎中读取数据,所以文中涉及到存储引擎的地方,都直接使用 InnoDB 代替了。

内容目录如下:

  • 创建 InnoDB 实例
    • 建立 MySQL 和 InnoDB 索引映射
    • 创建 InnoDB 查询模板
  • 填充查询模板
    • 使用什么索引
    • 是否需要回表
    • server 层需要 InnoDB 返回哪些字段
  • 从 InnoDB 读取记录
    • 预读缓存
    • 从 Buffer Pool 读记录
    • 一致性视图

1. 创建 InnoDB 实例

MySQL 每打开一个表,就会创建一个对应的 InnoDB 实例。创建 InnoDB 实例时,主要会干 3 件事情:

  • 打开 InnoDB 表(这个过程比较复杂,需要单独写文章讲述)
  • 建立 MySQL 和 InnoDB 索引映射
  • 创建 InnoDB 查询结构

创建 InnoDB 实例是在上一篇(查询准备阶段)中讲到的打开表的过程中进行的。

1.1 建立 MySQL 和 InnoDB 索引映射

MySQL 从 InnoDB 读取数据之前,词法分析、语法分析、查询准备、查询优化这些阶段都是 server 层的范围,在 server 层中需要使用索引信息时,使用的都是 MySQL 的索引信息,InnoDB 读取数据时会使用自己的索引信息,需要通过 MySQL 索引找到 InnoDB 索引,而这个找 InnoDB 索引的过程,是每执行一条使用索引进行查询的 SQL 都会用到的,又是一个频繁而重复的操作。为了更快的找到 InnoDB 索引,在创建 InnoDB 实例阶段就会建立 MySQL 索引和 InnoDB 索引之间的映射关系,这本质上也是个缓存(MySQL 中缓存思想无处不在)。

建立映射的过程是遍历 MySQL 表中的每一个索引,用索引的名字去对应的 InnoDB 表中找 InnoDB 索引,找到后把这个对应关系保存到数组中,数组 key 为 MySQL 表中的索引 ID,value 为 InnoDB 索引。

dict_index_t**	index_mapping;

// id, idx_i1 指的是 InnoDB 中相应的索引,都是 dict_index_t 指针
index_mapping[0] = id
index_mapping[1] = idx_i1

建立映射之后,在以后的操作中,通过 MySQL 索引 ID 就可以很方便的找到 InnoDB 索引了。

1.2 创建 InnoDB 查询模板

InnoDB 会根据查询模板中的信息执行查询,查询模板中包含这些信息:

  • 使用什么索引
  • 索引筛选条件
  • 索引条件下推
  • 是否需要回表
  • server 层需要 InnoDB 返回哪些字段

还有很多其它信息,就不一一列举了。

创建 InnoDB 实例过程中,只是创建了查询模板实例并给其中的属性设置了初始值。在填充查询模板阶段会为这些属性设置实际的值。

2. 填充查询模板

查询模板中的属性很多,我们只介绍示例 SQL 执行过程中使用到的比较重要的属性:

  • 使用什么索引
  • 是否需要回表
  • server 层需要 InnoDB 返回哪些字段

2.1 使用什么索引

InnoDB 中有两种索引:主键索引(也叫聚簇索引)、二级索引,在执行过程中使用哪种类型的索引,使用哪个索引,是在查询优化阶段决定的。

根据查询优化阶段确定的要使用的索引 IDMySQL 和 InnoDB 索引映射中找到 InnoDB 索引,后面执行读取数据操作时,就直接使用这个索引。

2.2 是否需要回表

先来说说什么是回表

使用二级索引读取数据时,如果 server 层要求 InnoDB 返回的字段,在二级索引中并不存在,那么就需要再去读主键索引,以获取二级索引中不存在的那些字段,这个过程就叫回表。

回表是 InnoDB 的自主操作,当使用二级索引读取数据并需要回表时,InnoDB 就直接使用二级索引中读取到的主键字段值去主键索引中读取完整的记录,并返回给 server 层,server 层对于回表无感知。

知道了回表的原理,我们可以想到回表并不复杂,只需要进行两个逻辑判断就可以了:

  • 如果使用主键索引,不需要回表,因为主键索引中本身就包含完整记录。
  • 如果使用二级索引,需要再进一步判断: 如果 server 层要求 InnoDB 返回的字段,都在二级索引中(就是覆盖索引了),不需要回表,否则就需要回表。

举例说明:

假设 test 表的字段 a 上有个二级索引 idx_a,当执行以下 SQL 使用 idx_a 索引进行查询时,就需要回表

select * from test where a > 1024

2.3 server 层需要 InnoDB 返回哪些字段

InnoDB 每次从 Buffer Pool 中读取一条记录时,都会读取完整的记录(字段内容有溢出时例外),读取记录之后,会拷贝字段内容到 server 层和引擎层交换内容的记录缓冲区中,而这个拷贝字段内容的过程也是需要时间的,如果 server 层只需要 InnoDB 返回记录中的部分字段,那么拷贝所有字段内容就存在浪费了。

如果字段内容有溢出,溢出内容会存储到单独的溢出页,只有当 server 层需要该字段时,InnoDB 才会去溢出页读取其中的内容,和该字段在记录中的部分内容一起组成字段的完整内容。

关于 server 层和 InnoDB 之间的数据交换,可以参考这篇文章:MySQL server 层和存储引擎层是怎么交互数据的?

举例说明:

假设 test 表有 6 个字段:d、e、f、x、y、z,当执行以下 SQL 时(表上没有索引),server 层会要求 InnoDB 返回哪些字段?

select x, y, z from test
where e > 1024 order by d

select 子句中有 x, y, z 3 个字段,server 层只会要求 InnoDB 返回这 3 个字段吗?

不不不,没这么简单,server 层还想要更多,它还需要字段 e 来做 where 条件筛选,需要字段 d 来做排序,所以它会要求 InnoDB 返回 d, e, x, y, z 这 5 个字段。

经过上面的讲述,相信大家对 server 层需要 InnoDB 返回哪些字段的逻辑已经明白了,咱们来总结一下:

只有 server 层需要用到的字段,才会要求 InnoDB 返回。那是为什么呢?两个字:效率。

接下来就要开始从 InnoDB 读取记录了,由于 InnoDB 记录存储本身也是个很大的话题,要说清楚怎么定位到要查找的记录,怎么读取下一条记录这些细节,需要很多关于 InnoDB 的文件存储以及 Buffer Pool 相关的内容作铺垫,要用一个系列的文章才能讲清楚,所以本文不会涉及 InnoDB 文件存储及 Buffer Pool 相关的细节。

3. 从 InnoDB 读取记录

为了提升执行效率,InnoDB 在读取时也是煞费苦心的,用了 2 种优化手段:

  • 预读缓存
  • 自适应哈希索引

自适应哈希索引的使用有很多条件限制,本文示例 SQL 不能使用自适应哈希索引来加快读取记录的速度,所以暂时先不介绍。接下来我们来看看预读缓存是什么以及它是怎么工作的?

3.1 预读缓存

InnoDB 读取数据的第一步,就是先看看预读缓存里有没有记录,如果有就直接取出 1 条记录返回给 server 层,如果没有,才会去 Buffer Pool 中读取记录。

预读缓存又是个新概念,我们来看看它是什么?

预读缓存是 InnoDB 丰富的内心戏的产物,当 InnoDB 心里想象着 server 层会要它读取很多记录时,它就会在读取某一条记录的时候,偷偷的多读几条缓存起来,等下次 server 层再找它要数据的时候,它就可以愉快的把已经缓存起来的记录快速的 取 1 条出来返回给 server 层,直到预读缓存中的全部记录都取出来返回给 server 层之后,再读下一条记录时,又接着偷偷的多读几条缓存起来,循环往复,直到读完所有符合条件的记录,这个缓存就是预读缓存

如果 server 层和 InnoDB 是两个人的话,InnoDB 从预读缓存里读记录这么快,server 层还不得给加鸡腿?

InnoDB 想象的依据是什么呢?

虽说是靠想象,但是也不能凭空瞎想吧,所以还得有个规则,规则有 2 条:

  • where 条件使用了索引,并且是 i1 = 1024 这种形式,也就是用等号比较。
  • 连续读取了大于等于 4 条记录。

上面说的 2 条规则是主要的,但是要想使用预读缓存,还会有一些其它隐密条件的限制,导致不能使用预读缓存,不再一一列举了。

上面的 2 条规则,只需要满足其中 1 条,外加满足其它隐密条件的限制,就会使用预读缓存。

预读缓存可以存几条记录?

在其它隐密的限制条件都满足的前提下:
如果当前查询的 where 条件使用了索引,并且是 i1 = 1024 这种形式,InnoDB 从 Buffer Pool 中读取 1 条记录之后,会再去读取 8 条记录,这 8 条记录存到预读缓存里,然后把开始单独读的 1 条记录返回给 server 层(此时预读缓存中有 8 条记录)。

如果当前查询的 where 条件没有使用索引,或者是使用了索引,但是索引字段使用的是 IN、>、>=、<、<=、!= 等等这些非等号比较操作符,那就会判断是不是已经连续读取了大于等于 4 条记录,如果是,InnoDB 从 Buffer Pool 中读取 1 条记录之后,会再去读取 8 条记录,这 8 条记录存到预读缓存里,然后把开始单独读的 1 条记录返回给 server 层(此时预读缓存中有 8 条记录)。

读取 8 条记录存到预读缓存里,这个 8 是在代码里写死的,如果想修改,只能改代码。

如果是从预读缓存读取记录,不会再从 Buffer Pool 多读 8 条记录存到预读缓存里,只有从 Buffer Pool 中读取记录并满足条件时,才会多读 8 条记录存到预读缓存里。

3.2 从 Buffer Pool 读记录

InnoDB 读取记录时,都是从 Buffer Pool 中读的,如果数据页不在 Buffer Pool 中,会先把数据页从磁盘上加载到 Buffer Pool 中,然后再从 Buffer Pool 中读取记录(这是不是跟我们日常写代码时使用缓存的思路一样?)。

InnoDB 的增、删、改、查语句,都必须在事务中执行,本文示例 SQL 执行时,设置的事务隔离级别为 REPEATABLE-READ,可以通过 show variables like 'transaction_isolation' 查看事务隔离级别。

如果我们没有显式开启事务,InnoDB 会为每条语句默认开启一个事务,语句执行完,事务就结束了。

可重复读隔离级别下,在一个事务中多次执行同一条查询语句时,得到的结果应该是完全一样的(不考虑幻读的话)。那么可重复读是通过什么东西实现的呢?答案就是:一致性视图

3.3 一致性视图

创建一致性视图(ReadView)时,会记录 InnoDB 中此刻的活跃事务信息:

  • 所有活跃事务的 IDs
  • 活跃事务的最小 ID
  • 即将分配给下一个事务的 ID
  • 当前事务的 ID

上面只列出了示例 SQL 执行过程,进行可重复读时需要使用到的信息。

可重复读隔离级别下,一个事务中只会创建一个 ReadView,创建的时机为:事务中第 1 条 SQL 执行的时候。

InnoDB 的每条记录中都有一个隐藏的字段用来表示最后修改记录的事务 ID(DB_TRX_ID),还有一个隐藏字段(DB_ROLL_PTR),是个指针,指向记录的历史版本

当 InnoDB 从 Buffer Pool 中读到一条记录时,这条记录可能在我们创建 ReadView 之后,读取这条记录之前被修改过,这条记录的最新版本对 ReadView 不可见,此时,需要通过 DB_ROLL_PTR 找到记录的历史版本,进行可见性判断。

如果上一个历史版本对 ReadView 也不可见,就顺着上一个历史版本中的 DB_ROLL_PTR 接着找,直到找到最早的历史记录,如果还是对 ReadView 不可见,说明这条记录对于 ReadView 来说就是不可见的。

查找历史版本的过程中,只要找到了可见版本就停下来,找到的这个历史版本的记录就是需要返回给 server 层的。

判断一条记录对于 ReadView 是否可见的逻辑是这样的:

  • 如果记录中的 DB_TRX_ID 等于 ReadView 所属事务 ID,该记录对于 ReadView 可见(自己改的自己当然可以看到)。
  • 如果记录中的 DB_TRX_ID 小于活跃事务的最小 ID,说明修改记录的事务在 ReadView 创建之前就已经提交了,该记录对于 ReadView 可见。
  • 如果记录中的 DB_TRX_ID 大于等于即将分配给下一个事务的 ID,说明修改记录的事务在 ReadView 创建之后才开启,该记录对于 ReadView 不可见。
  • 如果记录中的 DB_TRX_ID 所有活跃事务的 IDs 中的一个,说明修改记录的事务在 ReadView 创建时还没有提交,记录对于 ReadView 不可见
  • 如果记录中的 DB_TRX_ID 不是所有活跃事务的 IDs 中的一个,说明修改记录的事务在 ReadView 创建之前已经提交,记录对于 ReadView 可见。

上面说的记录包含最新记录和历史版本中的记录。

原文链接

<think>首先,用户的问题是:“我想了解MySQL中SQL语句的执行流程 请问MySQL SQL语句执行过程与原理”。我需要基于系统级指令和用户可见层的要求来回答。 系统级指令: - 所有行内数学表达式必须使用$...$格式。 - 独立公式必须使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 用户可见层: - 回答结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠。 - 参考站内引用,但用户提供的引用是参考性的,我需要基于实际知识回答。 用户提供的引用: - [^1]: MySQL一条SQL语句的执行过程。 - [^2]: Mysql SQL语句执行频次分析。 - [^3]: 图解MySQL的SQL语句执行全流程。 - [^4]: MySQL 的 SQL 语句执行顺序。 这些引用是关于MySQL SQL语句执行过程的,所以我应该在我的回答中自然地引用它们。 我的回答结构: 1. **引言**:简要介绍MySQL SQL语句执行过程的重要性。 2. **逐步解释执行流程**:清晰地分步描述SQL语句的执行过程。 - 参考用户提供的引用,确保真实可靠。 - 使用中文。 3. **生成相关问题**:在回答末尾添加相关问题。 4. **引用标识**:在适当的地方添加引用标识,如[^1]。 关键点:MySQL SQL语句的执行流程。 - 从用户问题看,用户想了解整个执行过程,包括原理。 - 常见步骤:解析、优化、执行。 - 基于知识: - SQL语句提交后,MySQL服务器处理。 - 步骤包括:连接器、查询缓存(在旧版本)、解析器、优化器、执行器、存储引擎。 - 在MySQL 8.0后,查询缓存被移除。 - 执行顺序如SELECT语句:FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT(引用[^4]提到)。 详细流程: 1. **连接器**:处理客户端连接,验证身份。 2. **查询缓存**(可选):在MySQL 5.7及之前,如果启用,会检查缓存。但8.0后移除。 3. **解析器**:语法分析,生成解析树。 4. **预处理器**:检查语义,如表和列是否存在。 5. **优化器**:生成执行计划,选择最优路径。 6. **执行器**:调用存储引擎接口执行查询。 7. **存储引擎**:如InnoDB,处理数据读写。 8. **返回结果**:将结果返回给客户端。 确保回答真实可靠:基于标准MySQL知识。 数学表达式:用户问题中没有涉及数学,但如果有数学部分,比如解释索引时,使用$...$格式。但这里可能不需要,除非讨论性能公式。 相关问题生成:在末尾添加3-5个相关问题,如: 1. 如何优化SQL查询性能? 2. 什么是查询优化器的工作原理? 3. MySQL中不同存储引擎的影响? 4. 如何分析SQL语句的执行频次?(参考引用[^2]) 5. SQL语句的执行顺序具体是什么?(参考引用[^4]) 回答结构: - 开头:介绍主题。 - 主体:分步解释流程。 - 结尾:总结并添加相关问题。</think>### MySQL SQL语句执行过程与原理详解 作为一名编程博客专家,我深知理解MySQL SQL语句的执行流程对于数据库性能优化、SQL调优和故障排查至关重要。MySQL执行过程是一个多阶段工作流,涉及多个组件协同工作。下面我将基于真实可靠的MySQL内部机制(以InnoDB存储引擎为例),逐步解释SQL语句的执行流程与原理。整个过程遵循**解析→优化→执行**的核心逻辑,确保高效处理查询[^1]。 #### 1. **连接建立与请求接收** - **过程**:当客户端(如应用程序)发起SQL请求时,MySQL的**连接器**首先处理TCP连接,验证用户名、密码和权限。如果验证通过,连接器会分配一个线程处理该请求,并维护会话状态。 - **原理**:连接器确保请求的隔离性和安全性。例如,如果执行`SELECT * FROM users;`,连接器会检查用户是否有`users`表的读取权限。这一步避免了未授权访问,但可能成为性能瓶颈(如连接数过多)[^1][^3]。 #### 2. **查询解析与语法分析** - **过程**:SQL语句(如`SELECT name FROM users WHERE age > 18;`)被传递给**解析器**。解析器进行词法分析(拆分关键词如`SELECT`、`FROM`)和语法分析,生成**解析树**(Abstract Syntax Tree, AST)。 - **原理**:解析器验证SQL语法是否正确。例如,它检查括号匹配、关键词顺序等。如果语法错误(如缺少分号),会立即返回错误。解析树是后续优化的基础,确保查询结构清晰[^1][^3]。 #### 3. **预处理与语义检查** - **过程**:**预处理器**接收解析树,进行语义验证。它检查表名、列名是否存在,数据类型是否兼容,以及权限是否足够。例如,对于`WHERE age > 18`,它会确认`age`列是否在`users`表中且为数值类型。 - **原理**:这一步防止运行时错误(如访问不存在的表)。如果验证失败,查询中止;否则,解析树被转换为内部表示形式,供优化器使用[^1][^3]。 #### 4. **查询优化** - **过程**:**优化器**是核心组件,它分析多种执行路径(如索引选择、JOIN顺序)。优化器基于成本模型(例如,I/O和CPU开销)生成最优**执行计划**。例如,对于`SELECT * FROM orders WHERE user_id = 100`,优化器可能选择使用`user_id`索引而非全表扫描。 - **原理**:优化器使用统计信息(如索引基数)估算成本。数学上,成本可表示为: $$ \text{Cost} = \text{disk\_reads} \times w_d + \text{cpu\_cycles} \times w_c $$ 其中 $w_d$ 和 $w_c$ 是权重因子。优化后,执行计划被编译为可执行代码[^1][^3][^4]。 #### 5. **查询执行** - **过程**:**执行器**调用存储引擎接口执行计划。执行器分步处理数据: - **数据读取**:根据WHERE条件过滤行(如`age > 18`)。 - **聚合与分组**:如果涉及GROUP BY,执行分组计算。 - **排序与限制**:最后应用ORDER BY和LIMIT。 - 引用[^4]强调了执行顺序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT。 - **原理**:执行器通过**存储引擎**(如InnoDB)访问数据。存储引擎处理磁盘I/O、锁机制(如行级锁)和事务(ACID特性)。结果集被缓存在内存中,逐步返回给客户端[^1][^3][^4]。 #### 6. **结果返回与清理** - **过程**:执行器将结果集通过连接器返回客户端。如果是事务操作(如INSERT),存储引擎确保日志写入(Write-Ahead Logging, WAL)。最后,资源(如临时表)被释放。 - **原理**:返回格式基于客户端协议(如二进制或文本)。性能关键点包括网络延迟和结果集大小。例如,LIMIT子句能减少数据传输量[^3][^4]。 #### 关键原理总结 - **整体流程**:连接→解析→优化→执行→返回,全程在MySQL服务器层与存储引擎层交互。 - **性能影响**:优化器决策占主导(70%以上性能优化机会),索引使用能降低复杂度(如从 $O(n)$ 到 $O(\log n)$)[^1][^3]。 - **版本差异**:MySQL 8.0移除了查询缓存(因命中率低),聚焦于优化器和执行器改进[^2][^3]。 理解此流程能帮助您诊断慢查询(如优化器选择不当索引)或设计高效SQL[^1][^2]。实际应用中,可通过`EXPLAIN`命令查看执行计划。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值