1. 为什么使用索引
假如给数据使用二叉树
这样的数据结构进行存储,如下图所示
2. 索引及其优缺点
2.1 索引概述
MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的数据结构。
索引的本质:索引是数据结构。你可以简单理解为“排好序的快速查找数据结构”,满足特定查找算法。这些数据结构以某种方式指向数据,这样就可以在这些数据结构的基础上实现高级查找算法
。
2.2 优点
类似于大学图书馆建书目索引,提高数据检索的效率,降低
数据库的IO成本
,这也是创建索引最为主要的原因。通过创建唯一索引,可以保证数据库表中每一行
数据的唯一性
。在实现数据的参考完整性方面,可以
加速表和表之间的连接
。换句话说,对于有依赖关系的子表和父表联合查询时,可以提高查询速度。在使用分组和排序子句进行数据查询时,可以显著
减少查询中分组和排序的时间
,降低了CPU的消耗。
2.3 缺点
增加索引也有许多不利的方面,主要表现在如下几个方面:
创建索引和维护索引要
耗费时间
,并且随着数据量的增加,所耗费的时间也会增加。索引需要占
磁盘空间
,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,存储在磁盘上
,如果有大量的索引,索引文件就可能比数据文件更快达到最大文件尺寸。虽然索引大大提高了查询速度,同时却会
降低更新表的速度
。当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度。
因此,选择使用索引时,需要综合考虑索引的优点和缺点。
3. InnoDB中索引的推演
3.1 索引之前的查找
先来看一个精确匹配的例子:
SELECT [列名列表] FROM 表名 WHERE 列名 = xxx;
在一个页中的查找
只需要在整个页中顺序遍历即可
在很多页中查找
在没有索引的情况下,不论是根据主键列或者其他列的值进行查找,由于我们并不能快速的定位到记录所在的页,所以只能从第一个页
沿着双向链表
一直往下找,在每一个页中根据我们上面的查找方式去查找指定的记录。因为要遍历所有的数据页,所以这种方式显然是超级耗时
的,如果一个表有一亿条记录呢?此时索引应运而生。
3.2 设计索引
建一个表
CREATE TABLE index_demo(
c1 INT,
c2 INT,
c3 CHAR(1),
PRIMARY KEY(c1)
) ROW_FORMAT = Compact;
这个新建的index_demo
表中有2个INT类型的列,1个CHAR(1)类型的列,而且我们规定了c1列为主键,这个表使用Compact
行格式来实际存储记录的。这里我们简化了index_demo表的行格式示意图:
我们只在示意图里展示记录得这几个部分:
record_type
:记录头信息的一项属性,表示记录得类型,0
表示普通记录、2
表示最小记录、3
表示最大记录、1
暂时还没用过,下面讲。next_record
:记录头信息的一项属性,表示下一条地址相对于本条记录得地址偏移量,我们用箭头来表示下一条记录是谁。各个列的值
:这里只记录在index_demo表中的三个列,分别是c1
、c2
和c3
.其他信息
:除了上述3种信息以外的所有信息,包括其他隐藏列的值以及记录得额外信息。
将记录格式示意图的其他信息项暂时去掉并把它竖起来的效果就是这样:
把一些记录放到页里的示意图就是:
1. 一个简单的索引设计方案
我们在根据某个搜索条件查找一些记录时为什么要遍历所有的数据页呢?因为各个页的记录并没有规律,我们并不知道我们的搜索条件匹配那些页中的记录,所以不得不依次遍历所有的数据页。所以如果我们想快速的定位到需要查找的记录有哪些数据页中该怎么办?我们可以为快速定位记录所在的数据页而建立一个目录,建这个目录必须完成下边这些事:
下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值。
给所有的页建立一个目录项。
所以我们为上边几个页做好的目录就像这样子:
以页28
为例,它对应目录项2
,这个目录项中包含着该页的页号28
以及该页中用户记录的最小主键值5
。我们只需要把几个目录项在物理存储器上连续存储(比如:数组),就可以实现根据主键值快速查找到某条记录的功能了。比如:查找主键值为20
的记录,具体查找过程分为两步:
先从目录项中根据
二分法
快速确定出主键值为20
的记录在目录项3
中(因为12<20<209),它对应的页是页9
。再根据前边说的在页中查找记录得方式去
页9
中定位具体的记录。
至此,针对数据页做的简易目录就搞定了。这个目录有一个别名,称为索引
。
2. InnoDB中的索引方案
1. 迭代1次:目录项记录的页
我们把前面使用到的目录项放到数据页中的样子就是这样:
从图中可以看出,我们新分配了一个编号为30的页来专门存储目录项记录。这样再次强调目录项记录
和普通的用户记录
的不同点:
目录项记录
的record_type
值是1
,而普通用户记录
的record_type
值是0
.目录项记录只有
主键值和页的编号
两个列,而普通的用户记录得列是用户自己定义的,可能包含很多列
,另外还有InnoDB自己添加的隐藏列。了解:记录头信息里还有一个叫
min_rec_mask
的属性,只有在存储目录项记录
的页中的主键值最小
的目录项记录的min_rec_mask
值为1
,其他的记录得min_rec_mask
的值都是0
。
相同点:两者用的都是一样的数据页,都会为主键值生成Page Directory
(页目录),从而在按照主键值进行查询时可以使用二分法
来加快查询速度。
现在以查找主键为20
的记录为例,根据某个主键值去查找记录得步骤就可以大致拆分为下边两步:
先到存储
目录项记录
得页,也就是页30
中通过二分法
快速定位到对应目录项,因为12<20<209,所以定位到对应的记录所在的页就是页9
。再到存储用户记录的
页9
中根据二分法
快速定位到主键值为20
的用户记录。
2. 迭代2次:多个目录项记录的页
从图中可以看出,我们插入了一条主键值为320的用户记录之后需要两个新的数据页:
为了存储该用户而新生成了
页31
。因为原先存储目录项记录得
页30的容量已满
(我们前面假设只能存储4条目录项记录),所以不得不需要一个新的页32
来存放页31
对应的目录项。
现在因为存储目录项记录得页不止一个,所以如果我们想要根据主键值查找一条用户记录大致需要3个步骤,以查找主键值为20
的记录为例:
确定
目录项记录页
我们现在的存储目录项记录的页有两个,即页30
和页32
,又因为页30
表示的目录项的主键值的范围是[1, 320)
,页32
表示的目录项的主键值不小于320,所以主键值为20
的记录对应在目录项记录在页30
中。通过目录项记录页
确定用户记录真实所在的页
。
在一个存储目录项记录
的页中通过主键值定位一条目录项记录的方式说过了。在真实存储用户记录得页中定位到具体的记录。
3. 迭代3次:目录项记录页得目录页(根节点)
如图,我们生成了一个存储更高级的目录项的页33
,这个页中的两条记录分别代表页30
和页32
,如果用户记录的主键值在[1, 320)
之间,则到页30
中查找更详细的目录项记录,如果主键值不小于320
的话,就到页32
中查找更详细的目录项记录。
我们可以用下边这个图来描述它:
这个数据结构,它的名称是B+树
。
4. B+Tree
一个B+树的节点其实可以分成好多层,规定最下边的那层,也就是存放我们用户记录的那层为第0
层,之后依次往上加。之前我们做了一个非常极端的假设:存放用户记录的页最多存放3条记录
,存放目录项记录的页最多存放4条记录
。其实真实环境中一个页存放的记录数量是非常大的,假设所有存放用户记录的叶子节点代表的数据页可以存放100条用户记录
,所有存放目录项记录的内节点代表的数据页可以存放1000条目录项记录
,那么:
如果B+树只有1层,也就是只有1个用于存放用户记录的节点,最多能存放
100
条记录。如果B+树有2层,最多能存放
1000x100=10,0000
条记录如果B+树有3层,最多能存放
1000x1000x100=1,0000,0000
条记录如果B+树有4层,最多能存放
1000x1000x1000x100=1000,0000,0000
条记录。相当多的记录!!!
你的表能存放1000,0000,0000条记录吗?所以一般情况下,我们用到的B+树都不会超过4层
,那我们通过主键值去查找某条记录最多只需要做4个页面内的查找(查找3个目录项页和一个用户记录页),又因为在每个页面内有所谓的Page Directory
(页目录),所以在页面内也可以通过二分法
实现快速定位记录。
3.3 常见索引概念
索引按照物理实现方式,索引可以分为2种:聚簇(聚集)和非聚簇(非聚集)索引。我们也把非聚集索引成为二级索引或者辅助索引。
1.聚簇索引
特点
使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:
页内
的记录是按照主键的大小排序排成一个单向链表。各个存放
用户记录的页
也是根据页中用户记录的主键大小排序排成一个双向链表
。存放
目录项记录的页
分为不同的层次,在同一层次
中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表
。
B+树的
叶子节点
存储的是完整的用户记录。所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。
优点
数据访问更快
,因为聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快聚簇索引对于主键的
排序查找
和范围查找
速度非常快按照聚簇索引排列顺序,查询显示一定范围数据的时候,由于数据都是紧密相连,数据库不用从多个数据块中提取数据,所以
节省了大量的I/O操作
。
缺点
插入速度严重依赖插入顺序
,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键更新主键的代价很高
,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新二级索引访问需要两次索引查找
,第一次找到主键值,第二次根据主键值找到行数据
2.二级索引(辅助索引、非聚簇索引)
概念:回表 我们根据这个以c2列
大小排序的B+树只能确定我们要查找记录的主键值,所以如果我们想根据c2列
的值查找到完整的用户记录的话,仍然需要到聚簇索引
中再查一遍,这个过程称为回表
。也就是根据c2列
的值查询一条完整的用户记录需要使用到2
棵B+树!
问题:为什么我们还需要一次回表
操作呢?直接把完整的用户记录放到叶子节点不OK吗?
这样维护起来非常麻烦,如果每一个索引都有用户记录,插入更新都要同时维护。
3.联合索引
我们也可以同时以多个列的大小作为排序规则,也就是同时为多个列建立索引,比方说我们想让B+树按照c2和c3列
的大小进行排序,这个包含两层含义:
先把各个记录和页按照c2列进行排序。
再记录的c2列相同的情况下,采用c3列进行排序。
注意一点,以c2和c3列的大小为排序规则建立的B+树称为联合索引,本质上也是一个二级索引。它的意思与分别为c2和c3列分别建立索引的表述是不同的,不同点如下:
简历联合索引智慧简历如上图一样的1棵B+树。
为c2和c3列分别创建索引会分别以c2和c3列的大小为排序规则建立2棵B+树。
3.4 InnoDB的B+树索引的注意事项
根页面位置万年不变
内节点中目录项记录的唯一性
一个页面最少存储2条记录
4. MyISAM中的索引方案
4.1 B-Tree支持的引擎
B树索引使用存储引擎如表所示:
即使多个存储引擎支持同一种类型的索引,但是他们的实现原理也是不同的。InnoDB和MyISAM默认的索引是B-Tree索引;而Memory默认的索引是Hash索引。
MyISAM引擎使用B+Tree
作为索引结构,叶子节点的data域存放的是数据记录的地址
。
4.2 MyISAM索引的原理
下面是MyISAM索引的原理图
如果我们在Col2上简历一个二级索引,则此索引的结构如下图所示:
4.3 MyISAM与InnoDB对比
MyISAM的索引方式都是“非聚簇”的,与InnoDB包含1个聚簇索引是不同的。小结两种引擎中索引的区别:
在
InnoDB
存储引擎中,我们只需要根据主键值对聚簇索引
进行一次查找就能找到对应的记录,而在MyISAM
中却需要进行一次回表
操作,意味着MyISAM中简历的索引相当于全部都是二级索引
。InnoDB的数据文件本身就是索引文件,而MyISAM索引文件和数据文件是
分离的
,索引文件仅保存数据记录的地址。InnoDB的非聚簇索引data域存储相应记录
主键的值
,而MyISAM索引记录的是地址
。换句话说,InnoDB的索引非聚簇索引都是引用主键作为data域。MyISAM的回表操作是十分
快速
的,因为是拿着地址偏移量直接到文件中取数据的,反观InnoDB是通过获取主键之后再去聚簇索引里找记录,虽然说也不慢,但还是比不上直接用地址取访问。InnoDB要求表
必须有主键
(MyISAM可以没有
)。如果没有显示指定,则MySQL系统会自动选择一个可以非空且唯一标识数据记录的列作为主键。如果不存在这种列,则MySQL自动为InnoDB表生成一个隐藏字段作为主键,这个字段长度为6个字节,类型为长整型。
5.索引的代价
索引是个好东西,可不能乱建,它在空间和时间上都会有消耗:
空间上的代价
每建立一个索引都要为它建立一颗B+树,每一颗B+树的每一个节点都是一个数据页,一个页默认会占用
16KB
的存储空间,一颗很大的B+树由许多数据页组成,那就是很大的一片存储空间。
时间上的代价
每次对表中的数据进行
增、删、改
操作时,都需要去修改各个B+树索引。而且我们讲过,B+树每层节点都是按照索引列的值从小到大的顺序排序
而组成了双向链表。不论是叶子节点中的记录,还是内节点中的记录(也就是不论是用户记录还是目录项记录)都是按照索引列的值从小到大的顺序而形成了一个单向链表。而增、删、改
操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些记录位移
,页面分裂
、页面回收
等操作来维护好节点和记录的排序。如果我们建了许多索引,每个索引对应的B+树都要进行相关的维护操作,会给性能拖后腿。
附录:算法的时间复杂度
同一个问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。