MySQL查询执行流程
MySQL执行一条SELECT查询SQL起自于客户端发起的一条请求。MySQL服务收到请求后首先根据SQL查询缓存,若果该查询SQL已经事先执行过,则直接返回缓存中的数据给客户端。否则进入SQL解析优化阶段。MySQL解析器对SQL进行语法解析,生成解析树,然后经过预处理阶段生成一棵新的语法解析树。将解析树送入查询优化器中生成执行计划。查询执行引擎根据执行计划调用存储引擎的API查询数据。
由于InnoDB是MySQL默认的存储引擎,也是我们最常用到的存储引擎,我们也没有那么多时间去把各个存储引擎的内部实现都看一遍,所以本集要唠叨的是使用InnoDB作为存储引擎的数据存储结构,了解了一个存储引擎的数据存储结构之后,其他的存储引擎都是依葫芦画瓢。
InnoDB页的概念
为了提高读写效率,MySQL将磁盘数据划分为页。页是MySQL和磁盘读写数据交互的基本单位,InnoDB的页的大小一般为16KB,也就是一次从磁盘读取数据的数据量最小为16KB,写入时,一次最少写入16KB的数据。
InnoDB行的格式
设计InnoDB存储引擎的大叔们到现在为止设计了4种不同类型的行格式,分别是Compact、Redundant、Dynamic和Compressed行格式。
我们可以在创建或修改表的过程中指定行格式,如:
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称
下面我们展开介绍这四种行格式。
Compact行格式
Compact行格式示意图如图:
从图中可以看出,Compact行格式分为两部分:额外信息和真实数据。
额外信息
变长字段长度列表:
如一些varchar(n)、Text、BLOB类型属于变长字段类型。变长字段长度列表就是记录这些变长字段长度的。需要注意的是,这里是按字段顺序的逆序存放,举例。
字段名 | 值 | 长度 |
a | ‘a' |
1 |
b |
‘aa' |
2 |
c |
‘aaa' |
3 |
Compact行:
3|2|1 |
NULL值列表 | 头信息 | 1 |
2 |
3 |
NULL值列表:
为了节省空间,NULL值是不需要存储的。所以Compact把这一行中所有的NULL值统一管理起来,放到”NULL
值列表“中。每一个NULL值对应一个二进制位,NULL值列表中的数据是将二进制位逆序进行存放的。
二进制位1时,代表对应位置的列为NULL。二进制位为0时代表对应位置不为NULL。
例如:
字段名 |
值 |
长度 |
a |
‘a' |
1 |
b |
NULL | 0 |
c |
NULL | 0 |
NULL值列表二进制表示为011,十六进制为0x03
Compact行:
1 |
0x03 | 头信息 |
1 |
头信息:
除了变长字段长度列表、NULL值列表之外,还有一个用于描述记录的记录头信息,它是由固定的5个字节组成。5个字节也就是40个二进制位,不同的位代表不同的意思,如图:
这些二进制位代表的详细信息如下表:
名称 |
大小(单位:bit) |
描述 |
预留位1 |
1 |
没有使用 |
预留位2 |
1 |
没有使用 |
delete_mask |
1 |
标记该记录是否被删除 |
min_rec_mask |
1 |
B+树的每层非叶子节点中的最小记录都会添加该标记 |
n_owned |
4 |
表示当前记录拥有的记录数 |
heap_no |
13 |
表示当前记录在记录堆的位置信息 |
record_type |
3 |
表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录 |
next_record |
16 |
表示下一条记录的相对位置 |
大家不要被这么多的属性和陌生的概念给吓着,我这里只是为了内容的完整性把这些位代表的意思都写了出来,现在没必要把它们的意思都记住,记住也没啥用,现在只需要看一遍混个脸熟,等之后用到这些属性的时候我们再回过头来看。
记录真实数据
这里再看一下Compact的行格式。
数据部分为用户插入的真实数据。这里需要注意的是MySQL主键生成策略。如果用户定义有主键,则使用用户定的列为主键。否则查找是否定义有Unique唯一列作为主键,如果Unique列也没有,MySQL则增加一列隐藏列Row_ID作为主键。
InnoDB数据页的存储结构
数据页是InnoDB的基本存储单位,一页为16KB。数据页的结构如下所示:
文件头部 |
页面头部 |
最小记录和最大记录 |
用户记录 |
空闲空间 |
页面目录 |
文件尾部 |
名称 | 描述 |
文件头部 |
页的一些通用信息 |
页面头部 |
数据页专有的一些信息 |
最小记录和最大记录 |
两个虚拟行 |
用户记录 |
实际存储内容 |
空闲空间 |
页內尚未使用空间 |
页面目录 |
页內记录相对位置 |
文件尾部 |
校验页的完整性 |
以上为页的结构和各部分的简介。那么数据页中的真实数据是怎么存放的呢?这个要从前面的行格式说起。
Compact行格式的头信息暗藏玄机,如图所示。
图中头信息展开后,包含如下几部分:
名称 |
大小(单位:bit) |
描述 |
预留位1 |
1 |
没有使用 |
预留位2 |
1 |
没有使用 |
delete_mask |
1 |
标记该记录是否被删除 |
min_rec_mask |
1 |
B+树的每层非叶子节点中的最小记录都会添加该标记 |
n_owned |
4 |
表示当前记录拥有的记录数 |
heap_no |
13 |
表示当前记录在记录堆的位置信息 |
record_type |
3 |
表示当前记录的类型,0表示普通记录,1表示B+树非叶节点记录 |
简化后如图所示。
MySQL会自动生成两条虚拟记录,存放在数据页的最小记录和最大记录中。
应该注意到,行数据头信息中的next_record,它标识从当前记录到吓一条记录的偏移量。下一条记录并不是按插入顺序的下一条,而是按主键大小排序的,由小到大的下一条记录。而且规定,本页中最小记录的下一条就是本页的第一条数据,最后一条数据的下一条就是最大纪录,如图所示。
删除行后,数据仍然会存在,只是行的头信息的delete_mark会标识为1。如果再有插入的数据,可能会复用已经删除的空间。
页目录
以上我们了解到,页中的数据是按照主键大小串成一串。头尾为页的虚拟记录。如果我们想定位到某一行数据,该怎么处理呢?如执行SQL(student_Id为Student表的主键):
Select * from Student Where student_Id = ‘201011201'
此时,最笨的办法就是按照过滤条件student_Id = ‘201011201’,从每一页的第一条数据查到最后一条。但是如果数据量大的情况下,这种办法的效率就堪忧了。
那么,既然页中行是按照主键大小来存储的,是否可以对页面中的行数据进行分组?
答案是肯定的。
MySQL将数据页的用户数据分组进行管理,页內第一组和最后一组分别为虚拟的最小记录和最大记录,其中不包含标记为已删除的数据。每个组最后一条记录的头信息中n_owned属性表示该组有多少条记录。(n_owned表示当前行在组内的偏移量)
将每个组最后一条记录的偏移量取出来,放到页面中的页面目录部分,这些目录成为槽。所以,这个页面的目录由槽组成。
通常,组内数据为5行。
分组是按照下边的步骤进行的:
MySQL将不同的数据页用双向链表连接起来,页内部用单向链表连接。
InnoDB 聚簇索引
加入没有索引,查找数据需要先遍历数据页,定位到目标数据所在页后,按照链表顺序,从最小记录开始遍历,直到找到目标数据或者最大纪录为止。
前面提到了数据页内有槽的存在,槽其实就是一个数据组的目录。那么,数据页是否也可以有一个类似于槽的目录呢?
答案是肯定的。MySQL使用每一个数据页的最小主键值最为Key,数据页的页号作为page_no,生成一个数据页的目录。如下图所示。
以图中页9为例,数据页9中最小主键值为12,因此,目录项为12/9。如果想查找主键值为100的数据,因为100>12 并且 100<209,因此数据定位于目录项3中,也就是页9中。然后,使用二分法找到对应的槽,进而找到槽对应的行数据。一次使用索引查找数据的操作完成。这里的目录就是对应到MySQL的索引。
MySQL索引的结构如图所示。
其实就是讲上述的页目录归置为一个特殊的页。页中行数据有两个字段,分别为主键Key和page_no。但是怎么区分是数据页还是这个特殊的页呢?大家还记得头信息中有个字段为record_type吗?它的取值如下:
也就是说,数据页的普通数据record_type为0,最大最小分别为3、2,上述特殊的页为1,称为目录项记录。
目录页的存储容量是有限的,如果数据量达到临界值,一个目录项就满足不了需求了。需要增加目录项页来保存目录索引。如果数据量较大,那么目录项页页会变得非常多,如此一来,查询又变得困难。其实解决方案很简单,那就是再给目录项页建立一个目录。就像是一个多级目录一样,大目录套着小目录。一层又一层,大概就是这个意思:
以此类推,目录层数可以一直往上抽象,直到只剩一个目录页为止。
再重新审视这个图,那不就是一棵树吗?其实他的名字叫B+树。
从图中可以看出,树的数据全都在叶子上,目录索引节点中不含数据。存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表。这样一来,索引和数据在一起,索引即数据,数据即索引,这里的索引也叫做聚簇索引。
二级索引
聚簇索引是以主键为基准建立的查找树。但是如果索引建立在了非主键字段上怎么处理呢?那就是二级索引。
我们可以多建几棵树。不同的树按照不同的排序规则生长。如我们使用非主键C2建立查找树。
这棵树是按照C2字段值将数据进行从小到大排序生成。叶子节点每行数据仅有两个字段:C2和数据主键。如果按照C2查询数据,使用这课树快速定位到行数据,然后拿到对应的主键,再使用聚簇索引查找目标数据即可。
联合索引
联合索引是将多列作为索引字段建立B+树。但是实际上MySQL只会为一个联合索引建立一个B+树。如使用C2和C3作为联合索引,规则如下:
每条目录项记录都由c2、c3、页号这三个部分组成,各条记录先按照c2列的值进行排序,如果记录的c2列相同,则按照c3列的值进行排序。