InnoDB 索引深入剖析
InnoDB页
将数据划分为若干个页(page),以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。
InnoDB行格式
我们平时是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式或者记录格式。
记录的额外信息:分别是变长字段长度列表、NULL值列表和记录头信息
【1】变长字段长度列表:MySQL中比如 VARCHAR(M)、VARBINARY(M)、各种 TEXT类型,各种 BLOB类型这些数据类型的列称为变长字段,变长字段中存储多少字节的数据是不固定的。所以这些变长字段占用的存储空间分为两部分:真正的数据内容、占用的字节数。
在行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表,各变长字段数据占用的字节数按照列的顺序逆序存放 。变长字段长度列表中只存储值为 非NULL 的列内容占用的长度,值为 NULL 的列的长度是不储存的 。并不是所有记录都有这个变长字段长度列表部分,比方说表中所有的列都不是变长的数据类型的话,这一部分就不需要有。
【2】NULL值列表:表中的某些列可能存储 NULL值,如果把这些 NULL值都放到记录的真实数据中存储会很占地方,所以Compact行格式把这些值为 NULL的列统一管理起来,存储到 NULL值列表中,它的处理过程是这样的:
① 首先统计表中允许存储 NULL的列有哪些;
② 如果表中没有允许存储 NULL 的列,则NULL值列表也不存在了;
【3】记录头信息
1 #先创建一个表:
2 mysql> CREATE TABLE page_demo(
3 -> c1 INT,
4 -> c2 INT,
5 -> c3 VARCHAR(10000),
6 -> PRIMARY KEY (c1)
7 -> ) CHARSET=ascii ROW_FORMAT=Compact;
8 Query OK, 0 rows affected (0.03 sec)
主键的生成策略
优先使用用户自定义主键作为主键,如果用户没有定义主键,则选取一个 Unique键作为主键,如果表中连 Unique键都没有定义的话,则 InnoDB会为表默认添加一个名为 row_id的隐藏列作为主键。
这个新创建的 page_demo表有3个列,其中 c1和 c2列是用来存储整数的,c3列是用来存储字符串的。需要注意的是,我们把 c1 列指定为主键,所以在具体的行格式中 InnoDB就没必要为我们去创建那个所谓的 row_id 隐藏列了。简化后的行格式示意图就是这样:
InnoDB数据页结构
数据页代表的这块 16KB大小的存储空间可以被划分为多个部分,不同部分有不同的功能,各个部分如图所示:
每当我们插入一条记录,都会从Free Space 部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到 User Records部分,当 Free Space部分的空间全部被 User Records部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了,这个过程的图示如下:
这里我们继续上面的 page_demo例子:
1 #先创建一个表:
2 mysql> CREATE TABLE page_demo(
3 -> c1 INT,
4 -> c2 INT,
5 -> c3 VARCHAR(10000),
6 -> PRIMARY KEY (c1)
7 -> ) CHARSET=ascii ROW_FORMAT=Compact;
8 Query OK, 0 rows affected (0.03 sec)
9
10 #向page_demo表中插入几条记录:
11 mysql> INSERT INTO page_demo VALUES(1, 100, 'aaaa'),
12 (2, 200, 'bbbb'),
13 (3, 300, 'cccc'),
14 (4, 400, 'dddd');
15 Query OK, 4 rows affected (0.00 sec)
16 Records: 4 Duplicates: 0 Warnings: 0
那么,这些记录的示意图就是:
【1】delete_mask:这个属性标记着当前记录是否被删除。这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后把其他的记录在磁盘上重新排列需要性能消耗,所以只是打一个删除标记而已。所有被删除掉的记录都会组成一个所谓的垃圾链表,在这个链表中的记录占用的空间称之为所谓的可重用空间,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。
【2】min_rec_mask:B+树的每层非叶子节点中的最小记录都会添加该标记,min_rec_mask值都是0,意味着它们都不是B+树的非叶子节点中的最小记录。
【3】n_owned:在页目录分组时使用,每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的 n_owned属性表示该记录拥有多少条记录,也就是该组内共有几条记录。
【4】heap_no:这个属性表示当前记录在本页中的位置,从图中可以看出来,我们插入的4条记录在本页中的位置分别是:2、3、4、5。heap_no值为0和1的记录,称为伪记录或者虚拟记录。这两个伪记录一个代表最小记录,一个代表最大记录。
【5】record_type:这个属性表示当前记录的类型,一共有4种类型的记录,0表示普通记录,1表示B+树非叶节点记录,2表示最小记录,3表示最大记录。
【6】next_record:它表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。比方说第一条记录的 next_record值为32,意味着从第一条记录的真实数据的地址处向后找 32个字节便是下一条记录的真实数据。下一条记录指得并不是按照我们插入顺序的下一条记录,而是按照主键值由小到大的顺序的下一条记录。而且规定 Infimum记录(也就是最小记录) 的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录)
从图中可以看出来,我们的记录按照主键从小到大的顺序形成了一个单链表。如果从中删除掉一条记录,这个链表也是会跟着变化的,比如我们把第2条记录删掉:
mysql> DELETE FROM page_demo WHERE c1 = 2;
Query OK, 1 row affected (0.02 sec)
第2条记录并没有从存储空间中移除,而是把该条记录的 delete_mask值设置为1。
第2条记录的 next_record值变为了0,意味着该记录没有下一条记录了。
第1条记录的 next_record指向了第3条记录。
还有一点你可能忽略了,就是最大记录的 n_owned值从5变成了4。
因为主键值为2的记录被我们删掉了,但是存储空间却没有回收,如果我们再次把这条记录插入到表中:
mysql> INSERT INTO page_demo VALUES(2, 200, 'bbbb');
Query OK, 1 row affected (0.00 sec)
Page Directory(页目录)
现在我们了解了记录在页中按照主键值由小到大顺序串联成一个单链表,那如果我们想根据主键值查找页中的某条记录该咋办呢?设计 InnoDB的大叔们为我们的记录也制作了一个类似的目录,他们的制作过程是这样的:
【1】将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
【2】每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的 n_owned属性表示该记录拥有多少条记录,也就是该组内共有几条记录。
【3】将每个组的最后一条记录的地址偏移量单独提取出来,用作查找。
注意:这个页目录是为主键服务的。
对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1-8 条之间,剩下的分组中记录的条数范围只能在是 4-8 条之间。分组是按照下边的步骤进行:
【1】初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
【2】之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
【3】在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。
我们再添加 12条记录看看效果:
1 mysql> INSERT INTO page_demo VALUES(5, 500, 'eeee'), (6, 600, 'ffff'), (7, 700, 'gggg'), (8, 800, 'hhhh'), (9, 900, 'iiii'), (10, 1000, 'jjjj'), (11, 1100, 'kkkk'), (12, 1200, 'llll'), (13, 1300, 'mmmm'), (14, 1400, 'nnnn'), (15, 1500, 'oooo'), (16, 1600, 'pppp');
2 Query OK, 12 rows affected (0.00 sec)
3 Records: 12 Duplicates: 0 Warnings: 0
因为把16条记录的全部信息都画在一张图里太占地方,让人眼花缭乱的,所以只保留了用户记录头信息中的 n_owned和next_record属性。因为各个槽代表的记录的主键值都是从小到大排序的,所以我们可以使用所谓的二分法来进行快速查找。所以在一个数据页中查找指定主键值的记录的过程分为两步:
【1】通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最大的那条记录。
【2】通过记录的 next_record属性遍历该槽所在的组中的各个记录。
比方说我们想找主键值为6的记录,过程是这样的:计算中间槽的位置:(0+4)/2=2,所以查看槽2对应记录的主键值为8,又因为8 > 6,所以设置high=2,low保持不变。重新计算中间槽的位置:(0+2)/2=1,所以查看槽1对应的主键值为4,又因为4 < 6,所以设置low=1,high保持不变。因为high – low的值为1,所以确定主键值为 6的记录在槽2对应的组中。我们可以很轻易的拿到槽1对应的记录(主键值为4),该条记录的下一条记录就是槽2中主键值最小的记录,该记录的主键值为5。所以我们可以从这条主键值为5的记录出发,遍历槽2中的各条记录。注意:若查到数据在槽2的分组中,由于槽2是指向最后一个记录,所以需要向上找一个槽位,定位到上一个槽位最后一行,然后再向下找。
File Header(文件头部)
File Header针对各种类型的页都通用,也就是说不同类型的页都会以 File Header作为第一个组成部分,它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁等。
FIL_PAGE_OFFSET:每一个页都有一个单独的页号,就跟你的身份证号码一样,InnoDB通过页号来可以唯一定位一个页。
FIL_PAGE_PREV和FIL_PAGE_NEXT:FIL_PAGE_PREV 和 FIL_PAGE_NEXT就分别代表本页的上一个和下一个页的页号。这样通过建立一个双向链表把许许多多的页就都串联起来了
索引的代价
【空间上的代价】:每建立一个索引都要为它建立一棵 B+树,每一棵 B+树的每一个节点都是一个数据页,一个页默认会占用16KB的存储空间。
【时间上的代价】:每次对表中的数据进行增、删、改操作时,都需要去修改各个 B+树索引。
B+树每层节点都是按照索引列的值从小到大的顺序排序而组成了双向链表。不论是叶子节点中的记录,还是内节点中的记录(也就是不论是用户记录还是目录项记录)都是按照索引列的值从小到大的顺序而形成了一个单向链表。而增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些记录移位,页面分裂、页面回收啥的操作来维护好节点和记录的排序。
B+树索引适用的条件
首先,B+树索引并不是万能的,并不是所有的查询语句都能用到我们建立的索引。下边介绍几个我们可能使用 B+树索引来进行查询的情况。
1 CREATE TABLE person_info(
2 id INT NOT NULL auto_increment,
3 name VARCHAR(100) NOT NULL,
4 birthday DATE NOT NULL,
5 phone_number CHAR(11) NOT NULL,
6 country varchar(100) NOT NULL,
7 PRIMARY KEY (id),
8 KEY idx_name_birthday_phone_number (name, birthday, phone_number)
9 );
person_info表会为聚簇索引和 idx_name_birthday_phone_number索引建立 2棵B+树。在记录结构中只保留 name、birthday、phone_number、id这四个列的真实数据值,所以示意图就长这样:
内节点中存储的是目录项记录,叶子节点中存储的是用户记录(由于不是聚簇索引,所以用户记录是不完整的,缺少 country列的值)。
【1】先按照 name列的值进行排序。
【2】如果 name列的值相同,则按照 birthday列的值进行排序。
【3】如果 birthday列的值也相同,则按照 phone_number的值进行排序。
全值匹配
如果我们的搜索条件中的列和索引列一致的话,这种情况就称为全值匹配,比方说下边这个查找语句:
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27' AND phone_number = '15123983239';
【1】因为 B+树的数据页和记录先是按照 name列的值进行排序的,所以先可以很快定位 name列的值是 Ashburn的记录位置。
【2】在 name列相同的记录里又是按照 birthday列的值进行排序的,所以在 name列的值是 Ashburn的记录里又可以快速定位birthday列的值是 ’1990-09-27’的记录。
【3】如果很不幸,name和 birthday列的值都是相同的,那记录是按照 phone_number列的值排序的,所以联合索引中的三个列都可能被用到。
调换name、birthday、phone_number这几个搜索列的顺序对查询的执行过程是没有影响的。
匹配左边的列
SELECT * FROM person_info WHERE name = 'Ashburn';
或者包含多个左边的列也行:
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27';
只有左边的列才能匹配,下边的语句就用不到这个B+树索引:
SELECT * FROM person_info WHERE birthday = '1990-09-27';
因为 B+树的数据页和记录先是按照 name列的值排序的,在 name列的值相同的情况下才使用 birthday列进行排序,也就是说name列的值不同的记录中 birthday的值可能是无序的。如果我们想使用联合索引中尽可能多的列,搜索条件中的各个列必须是联合索引中从最左边连续的列。
匹配列前缀
person_info表上建立的联合索引 idx_name_birthday_phone_number会先用 name列的值进行排序。也就是说这些字符串的前n个字符,也就是前缀都是排好序的,所以对于字符串类型的索引列来说,我们只匹配它的前缀也是可以快速定位记录的,比方说我们想查询名字以’As’开头的记录,那就可以这么写查询语句:
SELECT * FROM person_info WHERE name LIKE 'As%';
由于所有记录都是由链表连起来的(记录之间用单链表,数据页之间用双链表),所以他们之间的记录都可以很容易的取出来。找到这些记录的主键值,再到聚簇索引中回表查找完整的记录。如果对多个列同时进行范围查找的话,只有对索引最左边的那个列进行范围查找的时候才能用到 B+树索引,比方说这样:
SELECT * FROM person_info WHERE name > ‘Asa’ AND name < ‘Barlow’ AND birthday > '1980-01-01';
上边这个查询可以分成两个部分:
【1】通过条件name > ‘Asa’ AND name < ‘Barlow’来对name进行范围。
【2】对这些name值不同的记录继续通过birthday > ‘1980-01-01’条件继续过滤。
对于联合索引 idx_name_birthday_phone_number来说,只能用到 name列的部分,而用不到 birthday列的部分,因为只有name值相同的情况下才能用 birthday列的值进行排序,而这个查询中通过 name进行范围查找的记录中可能并不是按照 birthday列进行排序的,所以在搜索条件中继续以 birthday列进行查找时是用不到这个 B+树索引的。
精确匹配某一列并范围匹配另外一列
虽然对多个列都进行范围查找时只能用到最左边那个索引列,但是如果左边的列是精确查找,则右边的列可以进行范围查找,比方说这样:
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday > '1980-01-01' AND birthday < '2000-12-31' AND phone_number > '15100000000';
由于 name列是精确查找,所以通过 name = ‘Ashburn’条件查找后得到的结果的 name值都是相同的,它们会再按照 birthday的值进行排序。所以此时对 birthday列进行范围查找是可以用到 B+树索引的。phone_number > ‘15100000000’,通过 birthday的范围查找的记录的 birthday的值可能不同,所以这个条件无法再利用 B+树索引了,只能遍历上一步查询得到的记录。
用于排序
有的时候可能查询的结果集太大以至于不能在内存中进行排序的话,还可能暂时借助磁盘的空间来存放中间结果,排序操作完成后再把排好序的结果集返回到客户端。在MySQL中,把这种在内存中或者磁盘上进行排序的方式统称为文件排序(英文名:filesort)。
但是如果ORDER BY子句里使用到了我们的索引列,就有可能省去在内存或文件中排序的步骤,比如下边这个简单的查询语句:
SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;
因为这个 B+树索引本身就是按照上述规则排好序的,所以直接从索引中提取数据,然后进行回表操作取出该索引中不包含的列就好了。
使用联合索引进行排序注意事项
ORDER BY的子句后边的列的顺序也必须按照索引列的顺序给出,如果给出 ORDER BY phone_number, birthday, name的顺序,那也是用不了 B+树索引。
不可以使用索引进行排序的几种情况
【1】ASC、DESC混用:对于使用联合索引进行排序的场景,我们要求各个排序列的排序顺序是一致的,也就是要么各个列都是 ASC规则排序,要么都是 DESC规则排序。
【2】WHERE 子句中出现非排序使用到的索引列:如果 WHERE子句中出现了非排序使用到的索引列,那么排序依然是使用不到索引的,比方说这样:
SELECT * FROM person_info WHERE country = ‘China’ ORDER BY name LIMIT 10;
这个查询只能先把符合搜索条件 country = ‘China’的记录提取出来后再进行排序,是使用不到索引。
【3】排序列包含非同一个索引的列:有时候用来排序的多个列不是一个索引里的,这种情况也不能使用索引进行排序,比方说:
SELECT * FROM person_info ORDER BY name, country LIMIT 10;
【4】排序列使用了复杂的表达式:要想使用索引进行排序操作,必须保证索引列是以单独列的形式出现,而不是修饰过的形式,比方说这样:
SELECT * FROM person_info ORDER BY UPPER(name) LIMIT 10;
InnoDB 索引深入剖析的更多相关文章
- 剖析Mysql的InnoDB索引
摘要: 本篇介绍下Mysql的InnoDB索引相关知识,从各种树到索引原理到存储的细节. InnoDB是Mysql的默认存储引擎(Mysql5.5.5之前是MyISAM,文档).本着高效学习的目的,本 ...
- MySQL之InnoDB索引面试学习笔记
写在前面 想要做好后台开发,终究是绕不过索引这一关的.先问自己一个问题,InnoDB为什么选择B+树作为默认索引结构.本文主要参考MySQL索引背后的数据结构及算法原理和剖析Mysql的InnoDB索 ...
- MySQL的InnoDB索引原理详解
摘要 本篇介绍下Mysql的InnoDB索引相关知识,从各种树到索引原理到存储的细节. InnoDB是Mysql的默认存储引擎(Mysql5.5.5之前是MyISAM,文档).本着高效学习的目的,本篇 ...
- MyISAM和InnoDB索引区别
MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址.下图是MyISAM索引的原理图: 图8 这里设表一共有三列,假设我们以Col1为主键,则图8是一个MyISAM表 ...
- MySQL的InnoDB索引原理详解 (转)
摘要: 本篇介绍下Mysql的InnoDB索引相关知识,从各种树到索引原理到存储的细节. InnoDB是Mysql的默认存储引擎(Mysql5.5.5之前是MyISAM,文档).本着高效学习的目的,本 ...
- mysql InnoDB 索引小记
0.索引结构 1).MyISAM与InnoDB索引结构比较,如下: 2).MyISAM的索引结构 主键索引和二级索引结构很像,叶子存储的都是索引以及数据存储的物理地址,其他节点存储的仅仅是索引信息.其 ...
- myisam和innodb索引实现的不同
1.MyISAM 使用B+Tree 作为索引结构,叶子节点的data存放指针,也就是记录的地址.对于主键索引和辅助索引都是一样的.2.InnoDB 也使用B+Tree作为索引结构,也别需要注意的是,对 ...
- InnoDB 索引原理
InnoDB索引原理 索引能够提高访问的速率 B+树索引(最为常用和最为有效).全文索引.哈希索引. 数据库中的B+树索引可以分为聚集索引和辅助索引,但是不管是聚集还是辅助的索引,其内部都是B+树,是 ...
- InnoDB索引
名词解释 clustered index(聚集索引) 对(primary key)主键索引的一种表述.InnoDB表存储是基于primary key列来组织的,这样做可以加快查询和排序速度.为了获得最 ...
- 转 InnoDB索引
原文: http://blog.codinglabs.org/articles/theory-of-mysql-index.html InnoDB索引实现 虽然InnoDB也使用B+Tree作为索引结 ...
随机推荐
- C# 调用https接口 安全证书问题 解决方法
原文链接: https://blog.csdn.net/lizaijinsheng/article/details/127321758 说明: 如果是用https的话,由于没有证书,会报错:基础连接已 ...
- dead code?
public static void main(String[] args) { DriverBase dbase = new DriverBase(); dbase.driverBase(); dr ...
- jQuery测试用例-W3school
$("div").scrollLeft(100); // 设置滚动条的位置 $(document).ready(function(){ $("button"). ...
- 19 request请求数据大全
1 request.path_info # 获取当前用户请求的url # http://127.0.0.1:8000/customer/list/ ----> /customer/list/ # ...
- nginx 添加ssl认证,访问静态资源 笔记
1.查看是否开启SSL模块, /usr/local/nginx/sbin ./nginx -V 2.没有开启,进入到nginx源码包目录下: 3编译 ./configure --prefix= ...
- web端测试的测试点和注意事项【转载】
文章来源:作者:simplesally 出处:https://www.cnblogs.com/simple1025/ [转载] 工作中接触了不同类型的web端系统,内容不同,需求不同,测试关注点也 ...
- mysql之数据类型-第三篇
mysql数据库中的每个列都应该有适当的数据类型,用于限制或允许该列中存储的数据.mysql的数据类型分别有整数,浮点数和定点数类型,日期和时间类型,字符串类型和二进制类型等. 整数类型 数值型数据类 ...
- HDFS、Ceph、GFS、GPFS、Swift、Lustre……容器云选择哪种分布式存储更好?
HDFS.Ceph.GFS.GPFS.Swift.Lustre--容器云选择哪种分布式存储更好?-51CTO.COM 容器云在使用分布式存储时,HDFS.CEPH.GFS.GPFS.Swift等分布式 ...
- excel的几个常用方法
--笔记开始: 1.if(条件,真值,假值),类似于编程语言中的三元运算符.条件为真时返回真值,条件为假时返回假值. 2.match(目标值,查找区域,查找类型),一般查找类型为0(等值查找),查找区 ...
- PostgreSQL备份与恢复命令
postgresql备份与恢复相关命令 --备份用户的数据库bct的所有内容pg_dump -U 用户名 -d 库名 -f xxxXXXxxx.sql--删除原有数据库dropdb -U 用户名 -f ...