绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
MySQL 查询语句的准备阶段是怎样的?
2022-05-19 16:54:48

以下文章来源于公众号--一树一溪 ,作者一树一溪  

这一篇主要讲的内容是一条简单查询语句,在查询准备阶段会干哪些事情?分 3 个部分:

  • 打开表
  • select * 替换为表字段
  • 填充 where 条件

示例表及 SQL 如下:

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

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

接下来,我们进入正题,展开讲讲这 3 部分内容。

1. 打开表

从存储引擎读取数据之前,MySQL 需要把 SQL 中涉及的所有表的信息读取出来。研究源码之前,我想象中的打开表就是读取 frm 文件中的信息,构造出来一个对象啥的,然后就没有然后了,不知道正在看文章的你想象中的打开表的过程是什么样的呢?

MySQL 打开表的过程比较复杂,读取 frm 文件并进行处理的一个方法就有 1700+ 行代码,还不包括调用的其它方法。

正是因为打开表的过程复杂,而代码复杂意味着执行效率下降,这对于 MySQL 来说是不能接受的,所以必定要有优化手段。

每次执行 SQL 的时候,不管是增、删、改、查,还是修改表结构,都要打开表,可见打开表是个非常频繁的操作,对于这种复杂而又频繁的操作,能用什么优化手段呢?聪明如你,一定能够想到,当然是用缓存了。

没错,MySQL 中就是用缓存的思想实现的,而且是本机内存缓存,效率极高。天下武功,唯快不破,为了快得,MySQL 还不只用了一级缓存,而是用了两级缓存。

一级缓存

一级缓存是TABLE 类实例缓存,顾名思义,该缓存中保存的就是已经创建好的 TABLE 类实例,是之前的连接中使用过的 TABLE 类实例,用完之后又放回到缓存中了,所以从这个缓存里拿到的 TABLE 类实例,可以直接使用。打开表时,步就是从这个缓存中去拿 TABLE 类实例

TABLE 类实例中保存的是表结构信息。

从缓存中查找 TABLE 类实例,需要一个 key,这个 key 是由数据库名和表名组成的,key 的形式是 dbname_tablename

TABLE 类实例缓存实际上并不是只有 1 个,而是有多个,数量由系统变量  table_open_cache_instances 控制,默认为 16。总共可以缓存多少个实例由系统变量 table_open_cache 控制,默认为 2000,所以默认情况下,每个 TABLE 类实例缓存可以缓存 2000 / 16 = 125 个 TABLE 类实例。

同一个表的 TABLE 类实例,在缓存中可以存在很多个,理论上限是 table_open_cache,就是缓存的全都是同一个表的 TABLE 类实例。

那么,怎么确定要从哪个 TABLE 类实例缓存读取 TABLE 类实例呢?是这样的:每个连接都会有一个线程 ID,用线程 ID % table_open_cache_instances 得到一个序号,通过序号找到对应的 TABLE 类实例缓存

既然是缓存,就会涉及到缓存满了怎么办的问题,对于 TABLE 类实例缓存,当往缓存中放入 TABLE 类实例时,会判断缓存是否已满,如果满了,则按照近少使用原则,把多出来的 TABLE 类实例释放掉。还有另一种方式就是手动了,执行 FLUSH TABLES 命令可以清空缓存。

如果从一级缓存中没有读取到 TABLE 类实例,就要进入二级缓存的处理流程了,二级缓存逻辑比一级缓存复杂,所以执行效率要低一些。

二级缓存

二级缓存是 TABLE_SHARE 类实例缓存,可以缓存的 TABLE_SHARE 类实例数量由系统变量 table_definition_cache 控制,默认为 1400,每个表只对应一个 TABLE_SHARE 实例,从这个缓存中读取到 TABLE_SHARE 类实例以后,用该实例中的各个属性去创建并初始化一个 TABLE 类实例,然后就可以使用 TABLE 类实例进行后续的操作了。

TABLE_SHARE 类实例中保存的也是表结构信息,TABLE 类实例中的数据就是从 TABLE_SHARE 类实例中复制过来的。

如果从二级缓存中没有读取到可以用于初始化 TABLE 类实例的表结构信息,就只能从表 frm 文件中读取了。

读取 frm 文件

到这一步,要从 frm 文件中读取表名、表注释、字段名、字段类型、字段注释、索引等所有信息,并且进行一大堆各种检查,然后创建 TABLE_SHARE 类实例,再用 TABLE_SHARE 类实例创建 TABLE 类实例。

从 frm 文件中读取信息构建 TABLE_SHARE 类实例这个过程,逻辑太复杂,执行效率就更低了。

然而不管怎样,只要表存在,并且服务器没问题,多执行完上面 3 个步骤,就能拿到 TABLE 类实例了,然后就可以赋值给昨天说的 TABLE_LIST 的 table 属性了,从此,TABLE_LIST 就完整了。

2. select * 替换为表字段

我们在写 select 语句的过程中,经常会用到星号(*),表示查询表中所有字段,但是表中并没有一个星号字段用来表示所有字段,所以在查询准备阶段,会把星号替换为表中的所有字段。

这个替换过程比较简单,直接遍历表中的所有字段,为每个字段创建一个 Item_field  类实例,并且由于是直接遍历表中的 Field 子类实例列表,在创建 Item_field 类实例的时候就关联上了 Field 子类实例,不需要进行小蝌蚪找妈妈的过程了。

遍历完表中所有字段之后,形成一个 Item_field 列表,替换掉星号(*)对应的 Item_field 列表就行了,至此,就完成了 select 语句中星号替换为表字段的过程了。

3. 填充 where 条件

示例 SQL 的 where 中只有一个条件(i1 > 49276),条件中的 i1 字段也是一个 Item_field 类实例,需要找到对应表中的字段,并且关联上该字段的 Field 子类实例。

where 条件中的字段找到对应 Field 子类实例的过程,是这样的:遍历 SQL 中使用到的表,在遍历每个表的过程中,根据字段名查找表中有没有这个字段,如果没有,继续去下一个表找。如果找到了呢?那也不是就万事大吉了,像 i1 > 49276 中的 i1 字段,前面没有限定数据库名和表名,也还要继续遍历下一个表查找字段。

只有像 where 数据库名.表名.字段名 > 49276 这样,字段前面带有限定的数据库名和表名时,找到一个字段之后,才能立马结束查找过程,而不用遍历整个查询语句中使用到的所有表。

为什么在某个表中找到了字段之后不停止查找,还要继续遍历下一个表呢?

这是为了判断字段名是不是存在冲突,如果同一个字段名可以在大于 1 个表中找到对应的字段,说明字段名冲突了,就会报错:1052 - Column 'i1' in field list is ambiguous

在这个过程中,为了提升根据字段名查找对应 Field 子类实例的性能,也使用了两级缓存。

一级缓存

一级缓存在 Item_field 类实例中保存字段在表中的序号,通过这个序号可以直接找到 Field 子类实例,就能一步到位了。

不过可惜的是,一级缓存是给 PREPARE Statement 使用的,本文中的示例 SQL 用不上。

二级缓存

二级缓存是一个 hash,key 是字段名,value 是字段 Field 子类实例。

前面说过查找字段的过程是遍历表,然后在遍历的当前表中查找字段,二级缓存中的 hash 是挂靠在表(TABLE_SHARE 类实例)上的,所以可以只用字段名作为 key。

又要可惜了,本文示例 SQL 中的 i1 字段是用不上 hash 查找了,因为只有当表中的字段数量大于等于 32时,才会为该表创建 hash,用于字段查找。

既然字段名 hash 是挂靠在 TABLE_SHARE 类实例上的,那么就是共享的,可以一次创建,无限次使用,边际成本为 0,为什么不是每个表都使用 hash 来进行字段查找?这点我也没想明白。

如果上面说的两级缓存都用不上,那就剩一条路了,就是:遍历。遍历表中的每一个字段,然后比较该字段名和要查找的字段名是不是一样,如果一样那就是找到了,如果不一样,再接着遍历,直到遍历完表中的所有字段。

到这里,就把我们上一篇留下的小蝌蚪怎么找妈妈的故事讲完了。

然而,还有一点要补充的,就是 i1 字段和常数 49276 比较时执行的比较函数也是在填充 where 条件这一步中确定下来的,因为 Item_field 类实例找到对应的 Field 子类实例之后,i1 字段的类型就确定了,也就知道这两个值怎么比较了。


分享好友

分享这个小栈给你的朋友们,一起进步吧。

MySQL大本营
创建时间:2019-04-18 16:52:37
MySQL大本营是MySQL爱好者交流的社区。关注:MySQL实战,MySQL高性能,MySQL架构实战,MySQL DBA职业发展。MySQL大本营旨在创造一个MySQL社区交流环境。
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

栈主、嘉宾

查看更多
  • coolriver
    栈主

小栈成员

查看更多
  • 小雨滴
  • hwayw
  • 栈栈
  • 老七
戳我,来吐槽~