一、索引
1. 索引的介绍
索引是存储引擎用于快速找到数据记录的一种数据结构,类似一本书的目录。MySQL在进行数据查找时,首先查看查询条件是否命中某条索引,符合则通过索引查找相关数据,如果不符合则需要全表扫描。
索引是在存储引擎中实现的,因此每种存储引擎的索引都不一定完全相同。
同时存储引擎可以定义每张表的最大索引数和最大索引长度。所有存储引擎支持每个表至少16个索引,总索引长度至少为256个字节。有些存储引擎支持更多的索引数和更大的索引长度。
优点:
- 提高数据的检索效率,降低数据库的IO成本。
- 通过创建唯一索引,可以保证数据库表中每一行数据的唯一性。
- 对于有依赖关系的子表和父表联合查询时,可以提高查询速度。
- 在使用分组和排序进行查询时,可以显著减少查询中分组和排序的时间。
缺点:
- 创建索引和维护索引需要耗费时间,并且随着数据量的增加,所耗费的时间也会增加。
- 索引需要占一定的磁盘空间。
- 虽然索引大大提高了查询速度,但是却降低了更新表的数据。当对表的数据进行增加,修改,删除时,索引也要动态维护。
提示:
索引可以提高查询速度,但是会影响插入记录的速度,这种情况下,最好的办法是可以先删除表中的索引,然后再插入数据,插入完成后重新创建索引。
2. InnoDB中索引的推演
2.1 没有索引之前的查找
先来看一个精确匹配的例子:
SELECT [列名列表] FROM 表名 WHERE 列名 = xxx;
首先需要了解数据是以页为单位进行存储的。
在一页中查找:
假设数据量很小,只有一页数据。
-
以主键为搜索条件:
由于主键一般是递增的特殊性,可以使用二分法来进行查找
-
以其他列为搜索条件:
由于在数据页中并没有对非主键列建立所谓的页目录,所以无法通过二分来进行查找。这种情况只能从最小记录开始依次遍历单链表(页)中的每条记录,然后对比是否符合搜索条件。很显然效率很低。
在很多页中查找:
大部分情况下数据量都不只一页,在很多页中查找记录可以分为两个步骤:
- 定位到记录所在的页
- 从所在的页中查找相应的记录
在没有索引的情况下,无论是主键列还是非主键列,我们都无法快速定位到数据在哪一页,所以只能从第一页开始往下找,在每一页中根据上面所写的(在一页数据中查找)的方式进行查找。很显然,我们需要遍历所有的数据页,效率很低。
2.2 索引的设计
前置知识:
建一个表:
这个新建的 index_demo 表中有2个INT类型的列,1个CHAR(1)类型的列,而且我们规定了c1列为主键。
这个表使用 Compact 行格式来实际存储记录的。这里我们简化了index_demo表的行格式示意图:
我们只在示意图里展示记录的这几个部分:
- record_type:记录头信息的一项属性,表示记录的类型,0表示普通类型,1表示目录项记录,2表示最小记录,3表示最大记录。
- next_record:记录头信息的一项属性,表示下一条记录相对于本记录的地址偏移量。
- 各个列的信息:这里记录的是在index_demo表中的三个列:c1,c2,c3。
- 其他信息:除了上述3种信息以外的所有信息,包括其他隐藏列的值以及记录的额外信息。
1. 一个简单的索引设计方案
为什么我们需要遍历整张表呢?因为各个页中的记录并没有规律,所以不得不遍历所有的数据项。
因此我们可以建立一个目录,类似图书馆中的分类一样,提高查找的效率。
建立目录需要完成下面的要求:
- 下一个数据页中记录的主键值必须大于上一个页中记录的主键值。
- 给所有的页建立一个目录项,每个目录项包含 该页中主键的最小值 和 页号。
2. InnoDB中的索引方案:
(1)第一次迭代 :目录项记录的页
从图中可以看出来,我们新分配了一个编号为30的页来专门存储目录项记录。
这里再次强调 目录项记录 和普通的 用户记录 的不同点:
- 目录项记录的record_type是1,而普通用户记录的record_type是0。
- 目录项记录只有主键值和页的编号两个列, 而普通用户记录的列包括用户自己定义的,以及其他各种隐藏列。
相同点:
- 两者使用一样的数据页,都会为主键值生成 Page Directory(页目录),从而在按照主键进行查找时可以使用二分法来加快查询速度。
(2)第二次迭代:多个目录项记录的页
由于页的大小是有限的,一页可能存储不了目录项,因此需要多页来存储目录项记录。
(3)第三次迭代:目录项记录的目录页
如图,我们生成了一个存储更高级目录项的 页33 ,这个页中的两条记录分别代表页30和页32,如果用户记录的主键值在 [1, 320) 之间,则到页30中查找更详细的目录项记录,如果主键值 不小于320 的话,就到页32中查找更详细的目录项记录。
我们可以用下边这个图来描述它:
这个数据结构,它的名称是 B+树 。
(4)B+Tree:
一个B+树的节点其实可以分成好多层,规定最下边的那层,也就是存放我们用户记录的那层为第 0 层。
前面我们举的例子,所用的数据量都很少。其实真实环境中一个页存放的记录数量是非常大的,假设所有存放用户记录的叶子节点代表的数据页可以存放 100条用户记录 ,所有存放目录项记录的内节点代表的数据页可以存放 1000条目录项记录 ,那么:
- 如果B+树只有1层,也就是只有1个用于存放用户记录的节点,最多能存放 100 条记录。
- 如果B+树有2层,最多能存放 1000×100=10,0000 条记录。
- 如果B+树有3层,最多能存放 1000×1000×100=1,0000,0000 条记录。
- 如果B+树有4层,最多能存放 1000×1000×1000×100=1000,0000,0000 条记录。相当多的记录!!!
所以一般情况下,我们 用到的B+树都不会超过4层 ,那我们通过主键值去查找某条记录最多只需要做4个页面内的查找(查找3个目录项页和一个用户记录页),又因为在每个页面内有所谓的 Page Directory (页目录),所以在页面内也可以通过 二分法 实现快速定位记录。
2.3 常见索引概念:
索引可以分为 2 种:聚簇(聚集)和非聚簇(非聚集)索引。我们也把非聚集索引称为二级索引或者辅助索引。
1. 聚簇索引:
(1)特点:
- 根据主键值的大小进行记录和页的排序,这包含三个方面:
- 页内:页内的记录是通过主键值大小排成一个单向链表的。
- 不同用户记录页之间:各个存放 用户记录的页 也是根据页中用户记录的主键大小顺序排成一个 双向链表 。
- 不同目录项记录的页之间:存放 目录项记录的页 分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键
大小顺序排成一个 双向链表 。
- B+树的叶子节点存储的是完整的用户记录,也就是存储了所有的列的值。
我们把具有这两种特性的B+树称为聚簇索引。
(2)优点:
- 数据访问快,因为聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引获取数据比非聚簇索引更快。
- 聚簇索引对于主键的 排序查找和范围查找 速度非常快。
- 按照聚簇索引排列顺序,查询显示一定范围的数据时,由于数据是紧密相连的,不需要从多个数据块中提取数据,减少了大量的IO操作。
(3)缺点:
- **插入速度严重受插入顺序的影响,**按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键。
- 更新主键的代价很高 ,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新。
(4)限制:
- 对于MySQL数据库目前只有InnoDB支持聚簇索引,而MyISAM不支持聚簇索引。
- 每个表只能有一个聚簇索引,一般是这个表的主键。
- 如果没有定义主键,InnoDB会选择非空的唯一索引代替。如果没有这样的索引,InnoDB会隐式地定义一个主键作为聚簇索引。
- 由于聚簇索引的特性,InnoDB的主键尽量使用有序的顺序id,而不建议使用无序的id,比如UUID,MD5,HASH,字符串作为主键无法保证数据的顺序增长。
2. 非聚簇索引:
上面介绍的聚簇索引只能在搜索条件是主键时才能发挥作用,因为B+树中的数据都是依据主键进行排序的。那如果我们想通过别的列作为搜索条件该怎么办呢?答案是:多建几棵B+树,不同的B+树采用不同的排序规则。例如我们可以对c2列的大小作为数据页,页中记录的排序规则,再建一棵B+树,如图:
**回表:**我们根据这个以c2列大小排序的B+树只能确定我们要查找记录的主键值,所以如果我们想根据c2列的值查找到完整的用户记录的话,仍然需要到 聚簇索引 中再查一遍,这个过程称为 回表 。
聚簇索引和非聚簇索引之间的区别:
- 聚簇索引的叶子节点存放的就是完整的数据记录,而非聚簇索引的叶子节点存放的是数据位置(只有主键值,需要回表操作)。
- 一个表只能有一个聚簇索引,但是可以有多个非聚簇索引。
- 对于数据的查询效率来说,聚簇索引比非聚簇索引快(需要回表),对于数据的增删改来说,效率会比非聚簇索引低。
3. 联合索引:
我们也可以同时以多个列的大小作为排序规则,也就是同时为多个列建立索引,比方说我们想让B+树按照 c2和c3列 的大小进行排序,这个包含两层含义:
- 先把各个记录和页按照c2列进行排序。
- 在记录的c2列相同的情况下,采用c3列进行排序。
本质上联合索引也是二级索引。
2.4 InnoDB的B+树的补充说明:
1. 根页面位置往年不动
B+树的形成过程是这样的;
- 每当为某个表创建一个B+树索引时,都会为这个索引创建一个根节点页面。最开始表中没有数据时,这个根节点中既没有用户记录,也没有目录项记录。
- 随后向这个表添加数据,会陆续把用户记录存储到这个根节点中。
- 当根节点可用空间用完了,继续插入记录时,此时会将根节点的所有记录复制到一个新分配的页中,比如页a,同时创建一个新的页,比如页b。这时新插入的数据会根据键值的大小被分配到页a或者页b中,而根节点便升级为存储目录项记录的页。
这个过程需要注意的是:**一个B+树索引的根节点自诞生开始,便不会移动。**这样当我们需要用到这个索引时,都会从一个固定的地方来访问这个根节点。
2. 内节点中目录项记录的唯一性
内节点:除了叶子节点外的节点。
上面我们对内节点内容的表示都是 索引列+页号,其实是不严谨的。
假设有索引所处的状态是这样的:
此时我们如果向插入一条新的纪录,其中c1,c2,c3的值为:9,1,‘c’,那么在修改这棵B+树时就遇到了问题:此时页3中的目录项记录是由c2+页号的值构成的,而有两条记录的c2值都是一样的,那么新插入的数据应该放到页4还是页5呢?
因此,我们需要**保证在B+树的同一层节点的目录项记录除页号外的字段是唯一的。**所以对于二级索引的内节点的目录项纪录的内容实际上是由三个部分组成的:
- 索引列的值
- 主键值
- 页号
这样就能够确保各目录项记录除页号外是唯一的。
3. MyISAM中的索引方案
即使多个存储引擎支持同一种类型的索引,但是他们的实现原理也是不同的。Innodb和MyISAM默认的索引是Btree索引;而Memory默认的索引是Hash索引。
MyISAM引擎使用 B+Tree 作为索引结构,叶子节点的data域存放的是 数据记录的地址 。
3.1 MyISAM索引的原理
在InnoDB中,索引即数据,因为聚簇索引的B+树的叶子节点已经包含了完整的用户记录了。而MyISAM虽然也是采用B+树,但是索引和数据是分开存储的:
- **将表的记录按照插入顺序单独存储到一个文件中,称之为数据文件。**由于插入数据时并没有刻意按照主键的大小排序,所以我们并不能够通过二分法快速查找数据。
- **使用MyISAM存储引擎的表会将索引信息另外存储到一个文件中,称为索引文件。在索引的叶子节点存储的并不是完整的用户记录,而是主键值+数据记录地址 **的组合。
因此MyISAM的检索方式:先通过B+树搜索算法搜索索引,如果指定的索引值存在,那么就会根据data域中的地址,读取相应的数据记录。
3.2 MyISAM和InnoDB的对比
MyISAM的索引方式都是非聚簇索引,InnoDB包含一个聚簇索引。
- 在InnoDB存储引擎中,我们只需要根据主键值对 聚簇索引 进行一次查找就能找到对应的记录,而在MyISAM 中却需要进行一次 回表 操作,意味着MyISAM中建立的索引相当于全部都是 二级索引 。
- InnoDB的数据文件本身就是索引文件,而MyISAM索引文件和数据文件是 分离的 ,索引文件仅保存数据记录的地址。
- InnoDB的非聚簇索引data域存储相应记录 主键的值 ,而MyISAM索引记录的是 地址 。换句话说,InnoDB的所有非聚簇索引都引用主键作为data域。
- MyISAM的回表操作是十分快速的,因为是拿着地址偏移量直接到文件中取数据的,反观InnoDB是通过获取主键之后再去聚簇索引里找记录,虽然说也不慢,但还是比不上直接用地址去访问。
- InnoDB要求表 必须有主键 ( MyISAM可以没有 )。如果没有显式指定,则MySQL系统会自动选择一个可以非空且唯一标识数据记录的列作为主键。如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整型。
4. 索引的代价
索引虽好,但不可乱来。它在时间和空间上都有消耗:
-
空间上的代价:
每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用 16KB 的存储空间,一棵很大的B+树由许多数据页组成,那就是很大的一片存储空间。
-
时间上的代价:
每次对表中的数据进行 增、删、改 操作时,都需要去修改各个B+树索引。
而且,B+树每层节点都是按照索引列的值 从小到大的顺序排序 而组成了 双向链表 。不论是叶子节点中的记录,还是内节点中的记录(也就是不论是用户记录还是目录项记录)都是按照索引列的值从小到大的顺序而形成了一个单向链表。而增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些 记录移位 , 页面分裂 、 页面回收 等操作来维护好节点和记录的排序。
如果我们建了许多索引,每个索引对应的B+树都要进行相关的维护操作,会给性能拖后腿。
5. MySQL数据结构选择的合理性
5.1 全表遍历
不借助索引,对整张表进行遍历。
5.2 Hash结构
Hash函数,又称散列函数。能够大幅度提高我们的检索效率。
Hash算法是通过某种确定性的算法(比如MD5,SHA1,SHA2,SHA3)将输入转为输出。相同的输入永远可以得到相同的输出。
加速查找速度的数据结构,常见的有两类:
- 树:例如平衡二叉树,增删改查的平均时间复杂度都是O(log2N)
- Hash:例如HashMap,增删改查的平均时间复杂度都是O(1)
采用Hash进行检索效率非常高,基本上一次检索就能够找到,而B+树需要自顶向下一次查找,多次访问结点,多次进行IO操作,从效率上来说Hash比B+树更快。
那既然Hash结构效率更高,为什么索引要设计成树形的呢?
- **Hash索引仅能满足 =、<> 和 IN 查询。**如果进行范围查询,哈希型的索引,时间复杂度会被退化成O(N),而树形的有序性,依然可以保持O(log2N)的高效率。
- **Hash索引中数据存储是没有顺序的,**在 ORDER BY的情况下,还需要对数据进行重新排序。
- 对于联合查询,Hash值是将联合索引键值合并在一起后计算的,无法保证唯一性。
- 对于等值查询来说,通常Hash的效率更高,但是当索引列的重复值很多时,效率会大大降低。这是因为当Hash冲突时,需要遍历桶中的行指针逐一比较,非常耗时。所以,Hash索引通常不会用到重复值多的列上,比如性别,年龄等。
Hash索引的适用性
虽然Hash索引有很多限制,但有一些场景采用Hash索引效率更高,比如在键值数据库中,Redis存储的核心就是Hash表。
MySQL中的Memory存储引擎支持Hash存储,把某个字段设置成Hash索引,比如字符串类型的字段,进行Hash计算后长度可以缩短到几个字节。当字段的重复度低,并且经常需要等值查询时,采用Hash索引是个不错的选择。
**InnoDB本身是不支持Hash索引的,但是提供了自适应Hash索引。**如果某个数据经常被访问,并且满足一定条件时,就会把这个数据页的地址放到Hash表中。这样下次查询时,就可以直接找到这个页面的位置。
可以通过 show variables like '%innodb_adaptive_hash_index'
来查看是否开启了自适应Hash索引,默认是开启的。
5.3 二叉搜索树
如果利用二叉搜索树作为索引结构,那么磁盘的IO次数和索引树的高度是相关的。
- 二叉搜索树的特点
- 一个节点只能有两个子节点
- 左子节点 < 本节点 <= 右子节点
理想的二叉搜索树构建后是这样的:
但是也有可能有些二叉搜索树创建后是这样的:
这样就退化成O(N)的时间复杂度了。因此为了减少磁盘的IO次数,就需要尽量降低树的高度,所以就引入了AVL树。
5.4 AVL树
为了解决上面二叉搜索树退化成链表的问题,人们提出了平衡二叉搜索树,又称AVL树,它在二叉搜索树的基础上增加了约束:
它是一棵空树或者它的左右子树的高度差的绝对值不超过1,并且左右子树都是一棵平衡二叉树。
常见的平衡二叉树有很多种:平衡二叉搜索树,红黑树,数堆,伸展树。
一般说的平衡二叉树就是指平衡二叉搜索树。
当数据很多时,平衡二叉搜索树的高度也会很高,这意味着磁盘IO操作次数多,针对这种情况,如果我们把二叉树改成M叉树呢?
当M等于3时,同样的节点可以存储成这样:
5.5 B-Tree
B树的英文是Balance Tree,也就是多路平衡查找树。它的高度远小于平衡二叉搜索树的高度。
B树的结构如下图所示:
B树作为多路平衡查找树,它的每一个节点都包含M个子节点,M被称为B树的阶。
**每个磁盘块中包含了关键字和子节点的指针。**如果一个磁盘块包含了x个关键字,那么指针数就是x+1。
对于一个100阶的B树来说,如果有3层的话最多可以存储约100万条数据。对于大量的数据来说,采用B树的结构是非常适合的,因为树的高度要远小于二叉树的高度。
小结:
- B树在插入和删除节点的时候如果导致树不平衡,就会自动调整节点的位置来保持平衡。
- 关键字分布在整棵树中,即叶子节点和非叶子节点都存放数据(和B+树的不同之处),搜索有可能在非叶子节点结束。
- 其搜索性能等价于在关键字集合中做了一次二分查找。
5.6 B+Tree
B+树也是一种多路搜索树,基于B树进行了改进。相比于B-Tree,B+Tree更适合文件索引系统。
B+Tree和B-Tree的差异主要在以下几点:
- 对于B+树,有k个孩子的节点就有k个关键字,也就是子节点数=关键字数;而在B树中,子节点数量等于关键字数+1。
- 对于B+树,非叶子节点的关键字也会同时保存在叶子节点中,并且是在子节点中所有关键字的最小(最大)。
- 对于B+树,非叶子节点仅用于索引,不保存数据记录,跟记录相关的信息保存在叶子节点中;而在B树中,非叶子节点既保存索引,也保存数据记录。
B+Tree的中间节点并不存储数据,这样有什么好处?为什么说B+Tree是B-Tree的改进?
-
**B+树的查询效率更稳定。**因为B+树每次只有访问到叶子节点才能找到对应的数据,而在B树中,非叶子节点也会存储数据,这样就会造成查询效率不稳定的情况,有时候需要访问到叶子节点才能获取关键字,有时候又只需要在非叶子节点就能获取关键字。
-
**B+树的查询效率更高。**这是因为通常B+树比B树更加矮胖(阶数更大,高度更低),查询所需要的磁盘IO也会更少,这是因为B树的叶子节点存放完整的数据记录,相同的数据页大小,能够存放子节点的容量就相对少了。同样的磁盘页大小,B+树可以存储更多的节点关键字。
-
**在范围查询上,B+树的查询效率也更高。**这是因为所有的数据都存放在叶子节点上,叶子节点之间有指针连接,数据也是递增的,这使得我们范围查找可以通过指针连接查找;而在B树中则需要通过中序遍历才能完成范围查找,效率相对较低。
在MySQL采用的索引结构是B+树,但是B树和B+树各有各的应用场景,并不能踩一捧一。
思考题:
-
为了减少IO,索引树会一次性加载吗?
答:不会,数据库索引是存储在磁盘中的,当数据量很大时,必然会导致索引大小也会很大,是不可能一次性加载到内存中的,而是逐一加载磁盘页。
-
B+树的存储能力如何?为何说一般查找行记录,最多只需要1-3次磁盘IO?
答:InnoDB存储引擎的页的大小为16KB,一般表的主键是INT类(占用4个字节)或者BIGINT(占用8个字节),而指针的大小也一般为4或8个字节,也就是一个页(也就是B+树的一个节点)能够存储 16KB / (8B+8B) = 1K 个键值,为了方便计算,这里的K取值为103,也就是说一个深度为3的B+树能够存储 103 * 103 * 103 = 10亿条记录!(这里假定一个数据页也存储103 条行记录数据)。
实际情况中每个节点可能不能填满,因此在数据库中,B+Tree的高度一般是2-4层。但是MySQL的InnoDB存储引擎是将根节点常驻在内存中的,因此说查找某一条记录最多只需要1-3次磁盘IO。
-
为什么说B+树比B树更适合实际应用中操作系统的文件索引和数据库索引呢?
答:
一是B+树的读写代价更小。由于B+树的非叶子节点没有存放数据记录,因此同一磁盘页可以存放更多的索引,一次性读入内中的需要查找的关键字就更多,IO读写次数就降低了。
二是B+树的查询效率更加稳定。由于B+树的数据都存放在叶子节点,因此查询时必须从根节点到叶子节点一次遍历,导致每一关键字的查询效率相当。
-
Hash索引和B+树索引的区别?
前面已经有详细说明了,这里再总结概括一下:
一是Hash索引不能进行范围查找,B+树可以。因为Hash索引是无序的,而B+树的叶子节点是由有序的链表连接。
二是Hash索引不支持联合索引的最左侧原则,B+树可以。对于联合索引,Hash索引是将索引键合在一起后计算Hash值的,不会针对每个索引单独计算。因此如果需要用到联合索引中最左侧的单个或者几个索引时,联合索引是无法使用的。
三是Hash索引不支持ORDER BY排序,这也是因为Hash索引是无序的。同理,Hash索引也无法进行模糊查询,而B+树使用LIKE 进行模糊查询时,如果是进行后模糊查询的话(比如%结尾),就可以起到优化作用。
-
Hash索引和B+树索引是在建索引时手动指定的吗?
对于InnoDB和MyISAM是不支持Hash索引的,默认是B+树索引。InnoDB提供自适应Hash索引,无需手动指定。
5.7 R-Tree
R-Tree在MySQL中很少使用,仅支持geometry数据类型。
举个R-Tree的应用场景:查找20英里内的餐厅。一般情况下我们会把餐厅的坐标x,y分成两个字段存储在数据库中,一个记录经度,一个记录维度。这样我们就需要遍历所有的餐厅获取它的位置信息,然后计算是否满足要求。如果餐厅的数量是100家,那么我们就需要判断100次,显然效率是很低的。
R树就很好的解决这种空间搜索问题,它把B树的思想扩展到了多维空间,相比于B-Tree,R-Tree的优势在于范围查找。
由于很少使用,且不是重点,因此这里也不过多介绍。