以下文章来源于公众号--一树一溪 ,作者一树一溪
这一篇主要讲的内容是一条简单查询语句,在查询准备阶段
会干哪些事情?分 3 个部分:
打开表 select * 替换为表字段 填充 where 条件
示例表及 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
接下来,我们进入正题,展开讲讲这 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 字段的类型就确定了,也就知道这两个值怎么比较了。