3.1 索引概述
-
MySQL官方对索引的定义为:
索引(Index)是帮助MySQL高效获取数据的『数据结构』
。 -
索引的本质:
索引是数据结构
。你可以简单理解为“排好序的快速查找数据结构
”,满足特定查找算法。这些数据结构以某种方式指向数据,这样就可以在这些数据结构的基础上实现高级查找算法。 -
索引是在存储引擎中实现的
,因此每种存储引擎的索引不一定完全相同,并且每种存储引擎不一定支持所有索引类型。同时,存储引擎可以定义每个表的最大索引数和最大索引长度。所有存储引擎支持每个表至少16个索引,总索引长度至少为256字节。有些存储引擎支持更多的索引数和更大的索引长度。
3.2 为什么使用索引
- 使用索引是为了
减少磁盘IO次数,加快查询效率
- 假如给数据使用
二叉树
这样的数据结构进行存储,如下图所示
3.3 索引的优缺点
3.3.1 优点
- 类似大学图书馆建书目索引,提高数据检索的效率,降低数据库的IO成本,这也是创建索引最主要的原因
- 通过创建唯一索引,可以保证数据库表中每一行数据的唯一性
- 在实现数据的参考完整性方面,可以加速表和表之间的连接 。换句话说,对于有依赖关系的子表和父表联合查询时,可以提高查询速度
- 在使用分组和排序子句进行数据查询时,可以显著 减少查询中分组和排序的时间,降低了CPU的消耗
3.3.2 缺点
- 创建索引和维护索引要耗费时间,并且随着数据量的增加,所耗费的时间也会增加
- 索引需要占磁盘空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间
- 虽然索引大大提高了查询速度,同时却会降低更新表的速度 。当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度
3.4 常见索引概念
- 索引按照物理实现方式,索引可以分为2种:
聚簇索引
和非聚簇索引
。我们也把非聚集索引称为二级索引
或者辅助索引。 一张表只能有一个聚簇索引,可以有多个二级索引
3.4.1 聚簇索引
-
聚簇索引并不只是一种索引类型,也是一种数据存储的方式。即
索引就是数据,数据就是索引
。 -
在聚簇索引中,所有完整的用户记录都存放在这个聚簇索引的叶子节点处。聚簇索引并不需要我们在MySQL语句中显式的使用INDEX语句去创建,
InnoDB存储引擎会自动的为我们创建聚簇索引
。 -
特点:
- 使用记录主键值的大小进行记录和页的排序。这包括三个方面的含义:
页内的记录
是按照主键的大小顺序排成一个单向链表
- 各个
存放用户记录的页
也是根据页中用户记录的主键大小顺序排成一个双向链表
- 同一层次中的
目录项页
也是根据页中目录项记录的主键大小顺序排成一个双向链表
- B+树的
叶子节点
存储的是完整的用户记录
- 使用记录主键值的大小进行记录和页的排序。这包括三个方面的含义:
-
优点:
数据访问更快
,因为聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快- 聚簇索引对于主键的
排序查找
和范围查找
速度非常快 - 按照聚簇索引排列顺序,查询显示一定范围数据的时候,由于数据都是紧密相连,数据库不用从多个数据块中提取数据,所以
节省了大量的io操作
-
缺点:
插入速度严重依赖于插入顺序
,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键更新主键的代价很高
,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新二级索引访问需要两次索引查找
,即回表
。第一次找到主键值,第二次根据主键值找到行数据
3.4.2 二级索引(辅助索引、非聚簇索引)
- 二级索引一般用来处理非主键创建的索引。如图所示,我们在c2字段上简历索引
回表
:我们根据这个以c2列大小排序的B+树只能确定我们要查找记录的主键值,所以如果我们想根据c2列的值查找到完整的用户记录的话,仍然需要到聚簇索引
中再查一遍,这个过程称为回表
。也就是根据c2列的值查询一条完整的用户记录需要使用到2
棵B+树!
3.4.3 联合索引
- 同时以多个列的大小作为排序规则,也就是同时为多个列建立索引,比方说我们想让B+树按照c2和c3列的大小进行排序,这个包含两层含义:
- 先把各个记录和页按照c2列进行排序
- 在记录的c2列相同的情况下,采用c3列进行排序
- 联合索引
本质上也是一个二级索引
3.5 InnoDB索引详解
3.5.1 InnoDB数据项的设计
- 建立一个表
CREATE TABLE index_demo(c1 INT, 2 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种信息以外的所有信息,包括其他隐藏列的值以及记录的额外信息
- 将记录格式示意图的其他信息项暂时去掉并把它竖起来的效果就是这样:
- 把一些记录放到页里的示意图就是
3.5.2 一个简单的索引设计方案
- 如果我们想快速的定位到需要查找的记录在哪些数据页,我们可以建立一个目录,建这个目录必须完成下边这些事:
- 下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值
- 给所有的数据页建立一个
目录项
- 所以我们为上边几个页做好的目录就像这样子
- 以页28为例,它对应目录项2,这个目录项中包含着该页的页号28以及该页中用户记录的最小主键值5 。把目录项放在一个页内,就可以实现根据主键值快速查找某条记录的功能了。比如:查找主键值为20的记录,具体查找过程分两步:
- 先从目录项中根据
二分法
快速确定出主键值为20的记录在 目录项3 中(因为 12 < 20 < 209 ),它对应的页是页9 - 同样使用
二分法
在页9中定位主键为20的记录
- 先从目录项中根据
- 至此,针对数据页做的简易目录就搞定了。这个目录有一个别名,称为
索引
3.5.3 InnoDB的索引方案
- 迭代一次
- 我们把前边使用到的目录项放到数据页中的样子就是这样:
- 从图中可以看出来,我们新分配了一个编号为30的页来专门存储目录项记录。这里再次强调
目录项记录
和普通的用户记录
的不同点:- 目录项记录的record_type值是1,而普通用户记录的record_type值是0。
- 目录项记录只有主键值和页的编号两个列,而普通的用户记录的列是用户自己定义的,可能包含很多列 ,另外还有InnoDB自己添加的隐藏列。
- 以查找主键值为20的记录,具体查找过程分两步:
- 先从目录项中根据
二分法
快速确定出主键值为20的记录在 目录项3 中(因为 12 < 20 < 209 ),它对应的页是页9 - 同样使用
二分法
在页9中定位主键为20的记录
- 先从目录项中根据
- 迭代两次:多个目录项纪录的页
- 从图中可以看出,我们插入了一条主键值为320的用户记录之后需要两个新的数据页:
- 为存储该用户记录而新生成了页31 。
- 因为原先存储目录项记录的页30的容量已满(我们前边假设只能存储4条目录项记录),所以不得不需要一个新的页32来存放页31对应的目录项。
- 以查找主键值为20的记录为例,具体查找过程分3步:
确定目录项记录页
。我们现在的存储目录项记录的页有两个,即页30和页32,又因为页30表示的目录项的主键值的范围是[1,320),页32表示的目录项的主键值不小于320,所以主键值为20的记录对应的目录项记录在页30中。- 通过目录项记录页
确定用户记录真实所在的页
- 在真实存储用户记录的页中定位到具体的记录
- 迭代3次:目录项记录页的目录页
- 如图,我们生成了一个存储更高级目录项的页33 ,这个页中的两条记录分别代表页30和页32,如果用户记录的主键值在[1,320)之间,则到页30中查找更详细的目录项记录,如果主键值不小于320的话,就到页32中查找更详细的目录项记录。
- 我们可以用下边这个图来描述它:
- 这个数据结构,它的名称是
B+树
- B+ Tree
- 一个B+树的节点其实可以分成好多层,规定
最下边的那层,也就是存放我们用户记录的那层为第0层
,之后依次往上加。 - 真实环境中一个页存放的记录数量是非常大的,假设所有存放用户记录的叶子节点代表的数据页可以存放100条用户记录 ,所有存放目录项记录的内节点代表的数据页可以存放1000条目录项记录,那么:
- 如果B+树只有1层,也就是只有1个用于存放用户记录的节点,最多能存放
100
条记录 - 如果B+树有2层,最多能存放
1000×100=100000
条记录 - 如果B+树有3层,最多能存放
1000×1000×100=100000000
条记录 - 如果B+树有4层,最多能存放
1000×1000×1000×100=100000000000
条记录。即一千亿条记录,这是相当多的记录!!!
- 如果B+树只有1层,也就是只有1个用于存放用户记录的节点,最多能存放
- 一个
数据表的记录能超过100000000000吗?不会
。所以一般情况下,我们用到的B+树都不会超过4层
,那我们通过主键值去查找某条记录最多只需要做4个页面内的查找
(查找3个目录项页和一个用户记录页),又因为在每个页面内有所谓的Page Directory(页目录),所以在页面内也可以通过二分法
实现快速定位记录。
3.5.4 InnoDB的B+树索引的注意事项
- 根页面位置万年不动:
- 每当为某个表创建一个B+树索引(聚簇索引不是人为创建的,默认就有)的时候,都会为这个索引创建一个根节点页面。最开始表中没有数据的时候,每个B+树索引对应的根节点中既没有用户记录,也没有目录项记录。
- 随后向表中插入用户记录时,先把用户记录存储到这个根节点中。
- 当根节点中的可用空间用完时继续插入记录,此时会将根节点中的所有记录复制到一个新分配的页。根节点便升级为存储目录项记录的页。
- 内节点中目录项记录的唯一性
- 为了让新插入记录能找到自己在那个页里,我们需要保证在B+树的同一层内节点的目录项记录除页号这个字段以外是唯一的。
- 主键值能够保证唯一性,所以二级索引的内节点的目录项记录的内容实际上是由三个部分构成的:
- 索引列的值
- 主键值
- 页号
- 不建议使用过长的字段作为主键,因为所有二级索引都引用主键索引,过长的主键索引会令二级索引变得过大。
- 一个页面最少存储2条记录
3.6 MyISAM中的索引方案
MyISAM引擎使用B+Tree作为默认索引结构
,只支持非聚簇索引
,叶子节点的data域存放的是数据记录的地址
3.6.1 MyISAM索引的原理
- 下图是MyISAM索引的原理图:
- 如果我们在Col2上建立一个二级索引,则此索引的结构如下图所示:
3.6.2 MyISAM与InnoDB对比
- 在InnoDB存储引擎中,我们只需要根据主键值对聚簇索引进行一次查找就能找到对应的记录,而在MyISAM中却需要进行一次回表操作,即
MyISAM中建立的索引相当于全部都是 二级索引
。 InnoDB的数据文件本身就是索引文件
;而MyISAM索引文件和数据文件是分离的
,索引文件仅保存数据记录的地址。- InnoDB的非聚簇索引data域存储相应记录主键的值,而MyISAM索引记录的是地址。换句话说,InnoDB的所有非聚簇索引都引用主键作为data域。
MyISAM的回表操作是十分快速的
,因为是拿着地址偏移量直接到文件中取数据的;反观InnoDB是通过获取主键之后再去聚簇索引里找记录,效率不如MyISAMInnoDB要求表必须有主键(MyISAM可以没有)
。如果没有显式指定,则MySQL系统会自动选择一个可以非空且唯一标识数据记录的列作为主键。如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整型。
3.7 索引的数据结构
3.7.1 全表遍历
略
3.7.2 Hash结构
-
Hash结构一般采用数组+链表(JDK 1.7)或者数组+链表+红黑树(JDK 1.8+),理论上,增删改查的时间复杂度都为O(1)。
-
Hash索引使用的存储引擎如下:
索引/存储引擎 | MyISAM | InnoDB | Memory |
---|---|---|---|
Hash索引 | 不支持 | 不支持 | 支持 |
- Hash结构效率高,那为什么索引结构要设计成树型呢?
- 原因1:
Hash 索引仅能满足(=) (<>)和IN查询,无法进行范围查询
。如果进行范围查询,哈希型的索引,时间复杂度会退化为O(n);而树型的“有序”特性,依然能够保持O(log2N)的高效率。 - 原因2:Hash索引还有一个
致命缺陷
,数据的存储是没有顺序的
,在ORDER BY的情况下,使用Hash索引还需要对数据重新排序。 - 原因3:
对于联合索引的情况,Hash值是将联合索引键合并后一起来计算的
,无法对单独的一个键或者几个索引键进行查询。 - 原因4:对于等值查询来说,通常Hash 索引的效率更高,不过也存在一种情况,就是索引列的重复值如果很多,效率就会降低。这是因为遇到Hash冲突时,需要遍历桶中的行指针来进行比较,找到查询的关键字,非常耗时。所以,Hash 索引通常不会用到重复值多的列上,比如列为性别、年龄的情况等。
- 原因1:
- Hash索引的适用性
- 在键值型(Key-Value)数据库中,Redis存储的核心就是Hash表。
- MySQL中的Memory存储引擎支持Hash存储,如果我们需要用到查询的临时表时,就可以选择Memory存储引擎,把某个字段设置为Hash索引,比如字符串类型的字段,进行Hash计算之后长度可以缩短到几个字节。当字段的重复度低,而且经常需要进行等值查询的时候,采用Hash索引是个不错的选择。
InnoDB本身不支持Hash索引,但是提供自适应Hash索引(Adaptive Hash Index)
。什么情况下才会使用自适应Hash索引呢?如果某个数据经常被访问,当满足一定条件的时候,就会将这个数据页的地址存放到Hash表中。这样下次查询的时候,就可以直接找到这个页面的所在位置。这样让B+树也具备了Hash索引的优点。采用自适应Hash索引目的是方便根据SQL的查询条件加速定位到叶子节点,特别是当B+树比较深的时候,通过自适应Hash索引可以明显提高数据的检索效率
。
3.7.3 二叉搜索树
-
如果我们利用二叉搜索树作为索引结构,那么磁盘IO次数和树的高度是相关的
-
二叉搜索树的特点:
-
一个结点只能有两个子结点,即结点的出度不能超过2
-
一个结点的左子节点比该结点小,右子结点比该结点大
-
-
特殊情况:
- 该树也属于二分查找树,但已经
退化
为一个链表,其查找的时间复杂度为O(n)。为了避免这种情况,引入平衡二叉搜索树(AVL树)
3.7.4 AVL树
- AVL树是为了解决二叉搜索树退化为链表而提出的,全称平衡二叉搜索树,又简称
平衡二叉树
。它在二叉搜索树的基础上增加了约束:- 一棵AVL树是一棵空树或它的左右两树的高度差的绝对值不超过1
- 左右子树也是AVL树
- 数据查询的时间主要依赖于磁盘V/O的次数,如果我们采用二叉树的形式,即使通过平衡二叉搜索树进行了改进,树的深度也是
O(log2n)
,当n比较大时,深度也是比较高的,比如下图的情况:
-
针对同样的数据,如果我们把二叉树改成M叉树(M>2)呢?当M=3时,同样的31个节点可以由下面的三叉树来进行存储,
高度降低了1
:
3.7.5 B-Tree
- B树的全称是Balance Tree,即多路平衡查找树
- B树的结构如图所示:
- 一个M阶的B树(M>2)有以下的特性:
- 根节点的儿子数的范围是 [2,M]
- 每个中间节点包含k-1个关键字和k个孩子,孩子的数量 = 关键字的数量 +1,k的取值范围为[ceil(M/2), M]
- 叶子节点包括k-1个关键字(叶子节点没有孩子),k的取值范围为 [ceil(M/2), M]
- 中间节点节点的关键字按照升序排序
- 所有叶子节点位于同一层
关键字分布在整棵树中,叶子结点和非叶子结点都存放数据
- B树的查找:假设我们想要查找的关键字是9,那么步骤可以分为以下几步:
- 我们与根节点的关键字(17,35)进行比较,9小于17那么得到指针P1
- 按照指针P1找到磁盘块2,关键字为(8,12),因为9在8和12之间,所以我们得到指针P2
- 按照指针P2找到磁盘块6,关键字为(9,10),然后我们找到了关键字9
- B树相比于平衡二叉树来说磁盘I/O操作要少 ,在数据查询中比平衡二叉树效率要高。所以
只要树的高度足够低,I/O次数足够少,就可以提高查询性能
。
3.7.6 B+Tree
-
B+树也是一种
多路搜索树
,基于B树做出了改进
,主流的DBMS都支持B+树的索引方式,比如MySQL。相比于B-Tree,B+Tree适合文件索引系统。 -
B+Tree和B树的差异:
- B+树中,有k个孩子的节点就有k个关键字,即
孩子数量 = 关键字数量
。B树中,孩子数量 = 关键字数+1
- 非叶子节点的关键字也会同时存在在子节点中,并且是在子节点中所有关键字的最大(或最小)
- B+树的非叶子节点仅用于索引,不保存数据,数据存放在叶子节点中。B树非叶子结点和叶子结点都用于存放数据。
- B+树的所有关键字都在叶子节点出现,照关键字的大小从小到大顺序链接,构成一个有序链表。
- B+树中,有k个孩子的节点就有k个关键字,即
-
B+树对于B树的优势:
B+树的查询效率更高
。这是因为通常B+树比B树更矮胖
(阶数更大,深度更低),查询所需要的磁盘I/O也会更少。同样的磁盘页大小,B+树可以存储更多的节点关键字。在范围查询上,B+树的效率也比B树高
。这是因为所有关键字都出现在B+树的叶子节点中,叶子节点之间会有指针,数据又是递增的,这使得我们范围查找可以通过指针连接查找。而在B树中则需要通过中序遍历才能完成查询范围的查找,效率要低很多。
3.7.7 R树
- R树就很好的
解决了这种高维空间搜索问题
。它把B树的思想很好的扩展到了多维空间
,采用了B树分割空间的思想,并在添加、删除操作时采用合并、分解结点的方法,保证树的平衡性。因此,R树就是一棵用来存储高维数据的平衡树。相对于B-Tree,R-Tree的优势在于范围查找。 - R-Tree在MySQL很少使用,仅支持geometry数据类型 ,支持该类型的存储引擎只有myisam、bdb、innodb、ndb、archive几种。
- R树在现实领域中能够解决的例子:存储地理的经纬度坐标。
3.8 思考题
-
为了减少IO,索引树会一次性加载吗?
答:当我们利用索引查询时候,是不可能将全部几个G的索引都加载进内存的,我们能做的只能是:逐一加载每一个磁盘页,因为磁盘页对应着索引树的节点。
-
为什么说B+树比B-树更适合实际应用中操作系统的文件索引和数据库索引?
答:B+树的磁盘读写代价更低。B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
-
Hash 索引与 B+ 树索引的区别
答:Hash索引不能进行范围查询,而B+树索引可以,因为Hash索引的数据是无序的,而B+树的叶子结点是有序的。因为Hash索引是无序的,故不支持ORDER BY和模糊查询。
Hash索引不支持最左侧原则
,即联合索引无法使用
。而B+树可以。 -
Hash 索引与 B+ 树索引是在建索引的时候手动指定的吗?
答:InnoDB和MyISAM存储引擎,都会默认采用B+树索引,无法使用Hash索引。InnoDB提供的自适应Hash是不需要手动指定的。如果是Memory/Heap和NDB存储引擎,是可以进行选择Hash索的。