1. 为什么使用索引
没有索引的情况下,数据分配在硬盘的不同位置,读取数据时摆臂需要前后摆动寻找数据,非常耗时。
即使数据顺序摆放,也需进行6次IO操作:加载一条记录到内存,对比数据是否为所需。
假如给数据使用 二叉树 这样的数据结构进行存储,如下图所示
数据查找时首先查看查询语句是否命中某条索引,符合则通过索引查找相关数据,不符则全表扫描。
2. 索引及其优缺点
2.1 索引概述
MySQL官方对索引的定义为:索引是帮助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;
使用 Compact 行格式来实际存储记录的。简化版index_demo表的行格式示意图:
我们只在示意图里展示记录的这几个部分:
record_type:记录头信息的一项属性,表示记录的类型,0表示普通记录、2表示最小记录、3表示最大记录、1后面会讲。next_record:记录头信息的一项属性,表示下一条地址相对于本条记录的地址偏移量,我们用 箭头来表明下一条记录是谁。各个列的值:这里只记录 c1 、 c2 和 c3 。其他信息:除了上述3种信息以外的所有信息,包括其他隐藏列的值以及记录的额外信息。
把一些记录放到页里的示意图就是:
3.2.1 一个简单的索引设计方案
根据某个搜索条件查找记录时为什么要遍历所有的数据页呢?因为各个页中的记录并没有规律,我们并不知道我们的搜索条件匹配哪些页中的记录。所以我们可以为快速定位记录所在的数据页而建立一个目录,建这个目录必须完成下边这些事:
- 下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值。
- 给所有的页建立一个目录项。
目录项包含指向的页号和该页中记录的最小主键
具体查找步骤(例:主键为20):
- 先从目录项中根据
二分法快速确定出主键值为20的记录在目录项3中(因为 12 < 20 < 209 ),它对应的页是页9。 - 在 页9 中使用二分法
至此,针对数据页做的简易目录就搞定了。这个目录有一个别名,称为 索引 。
3.2.2 InnoDB中的索引方案
① 迭代1次:目录项记录的页 我们把前边使用到的目录项放到数据页中的样子就是这样:
新分配了一个编号为30的页来专门存储目录项记录。这里再次强调
目录项记录 和普通的 用户记录 的不同点:
目录项记录的record_type值是1,而普通用户记录的值是0。- 目录项记录只有
主键值和页的编号两个列,而普通的用户记录的列是用户自己定义的,另外还有InnoDB自己添加的隐藏列。 - 了解:记录头信息里还有一个叫
min_rec_mask的属性,只有在存储目录项记录的页中(即页30)的主键值 最小的目录项记录(即1,10) 的值为 1 ,其他别的记录的值都是 0 。
相同点: 两者用的是一样的数据页,都会为主键值生成 Page Directory (页目录),从而在按照主键值进行查找时可以使用 二分法 来加快查询速度。
② 迭代2次:多个目录项记录的页
我们插入了一条主键值为320的用户记录之后需要两个新的数据页:
- 为存储该用户记录而新生成了
页31。 - 原先存储目录项记录的
页30的容量已满( 假设只能存储4条),所以需要一个新的页32来存放页31对应的目录项。
以查找主键值为 20 的记录为例:
- 确定 目录项记录页
我们现在的存储目录项记录的页有两个,即
页30和页32,又因为页30表示的目录项的主键值的 范围是[1, 320),页32表示的目录项的主键值不小于320,所以主键值为20的记录对应的目 录项记录在页30中。 - 通过目录项记录页
确定用户记录真实所在的页。二分法定位 - 在真实存储用户记录的页中定位到具体的记录。
③ 迭代3次:目录项记录页的目录页
如图,我们生成了一个存储更高级目录项的
页33 ,这个页中的两条记录分别代表页30和页32,如果用
户记录的主键值在 [1, 320) 之间,则到页30中查找更详细的目录项记录,如果主键值 不小于320 的
话,就到页32中查找更详细的目录项记录。
④ 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 (页目录),所以在页面内也可以通过 二分法 快速定位记录。
3.3 常见索引概念
按照物理实现方式分为2种:聚簇(聚集)和非聚簇(非聚集)索引。我们也把非聚集索引称为二级索引或者辅助索引。
3.3.1 聚簇索引
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式(所有用户记录存储在叶子节点)。也就是所谓的索引即数据,数据即索引
术语 聚簇 表示数据行和相邻的键值聚簇的存储到一起 特点
- 使用记录 主键值的大小进行记录和页的排序,这包括三个方面的含义:
页内的记录是按照主键的大小顺序排成一个单向链表。- 各个存放
用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表。 - 存放
目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表。
- B+树的
叶子节点存储的是完整的用户记录。 指这个记录中存储了所有列的值(包括隐藏列)。
我们把具有这两种特性的B+树称为 聚簇索引,InnoDB 会自动创建聚簇索引
优点
数据访问更快,聚簇索引将索引和数据保存在同一个B+树中,因此从中获取数据比非聚簇索引更快- 聚簇索引对于主键的
排序查找和范围查找速度非常快 - 查询显示一定范围数据时,由于数据都是紧密相连,数据库不用从多个数据块中提取数据,所以
节省了大量的io操作。
缺点
插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据
限制
- 只有InnoDB支持 聚簇索引
- 由于数据物理存储排序方式只能有一种,一张表中
只能有一个聚簇索引,通常为主键 - 若没定义主键, InnoDB会选择
非空的唯一索引代替,还是没有则自己隐式定义一个主键
3.3.2 二级索引(辅助索引、非聚簇索引)
聚簇索引只能在搜索条件为 主键 时起效,若我们想以别的列作为搜索条件该怎么办?
多建几颗 B+树
比如:用 c2 列的大小作为数据页、页中记录的排序规律再建一颗B+树
该树与聚簇索引的不同:
- 使用记录 c2 列的大小进行记录和页的排序
- 叶子节点只存储 c2 和 主键
- 目录项记录 变为 c2 + 页号
我们可以通过 B+树 快速定位到指定的 c2,但叶子节点中的记录并不完整,所以我们需要进行回表
回表
想要获得完整数据,我们必须通过主键值到 聚簇索引 中再搜索一遍,即 回表
问题:为什么要回表呢?直接把完整用户记录放到叶子节点不行吗?
回答:数据冗余,若每建一个二级索引都需要复制一份,磁盘早炸了
因为这种按照 非主键列 建立的B+树 需要回表才能定位到完整的记录,所以被称为 二级索引
小结:聚簇索引和非聚簇索引使用上的区别:
- 聚簇索引叶子节点存储
数据记录,非聚簇索引的叶子节点存储数据位置。非举聚簇索引不会影响数据表的物理存储顺序。 - 一个表
只能有一个聚簇索引,因为只能有一种排序存储方式,但可以有多个非聚簇索引 - 聚簇索引
查询效率高,但插入、删除等操作效率比非聚簇索引低
3.3 联合索引(非聚簇索引)
以多个列作为排序规则,即为多个列建立索引,比方说我们想让B+树按照 c2和c3列 的大小进行排序,这个包含两层含义:
- 先把各个记录和页按照c2列进行排序。
- 在记录的c2列相同的情况下,采用c3列进行排序
3.4 InnoDB的B+树索引的注意事项
根页面位置万年不动
前面介绍B+树时为了方便理解,先画了叶子节点后画存储目录项记录的内节点,而实际上B+树是这么形成的:
- 创建B+树索引时,为该索引创建一个
根节点页面,最开始没有数据。 - 插入用户记录时,先将用户记录插入
根节点 - 根节点
空间用完时继续插入记录,则根节点中所有记录复制到一个新分配的页a,对该页进行页分裂操作得到新页b。此时新插入的数据根据键值选择前往的页,而根节点升级为存储目录项记录的页。
InnoDB 用到该索引时 从固定的地方取出根节点的页号
内节点(目录节点)中目录项记录的唯一性
若某个构建出来的B+树如图:
我们项插入一条记录:{9,1,'c'},在修改时则碰到大问题:页3的两条目录项对应的 c2 列都为1,不知道该插到哪。
我们必须保证B+树的同一层内节点的目录项记录除页号外 有唯一性。所以二级索引内节点目录项记录需要三个部分:
- 索引列
- 主键
- 页号
这样我们插入时知道
9 > 7,插入页 5
一个页面最少存储2条记录
4. MyISAM中的索引方案
他们的实现原理也是不同的。Innodb和MyISAM默认的索引是Btree索引;而Memory默认的索引是Hash索引。
MyISAM引擎使用 B+Tree 作为索引结构,叶子节点的data域存放的是 数据记录的地址
4.2 MyISAM索引的原理
我们知道InnoDB中索引即数据,也就是聚簇索引那颗B+树包含所有用户记录。而MyISAM将索引和数据分开存储(进阶篇2中提过 .MYD 存储数据 .MYI 存储索引 )
- 将表中的记录
按照记录的插入顺序单独存储在一个文件中,称为数据文件。这个文件不划分数据页,有多少记录就塞多少。由于插入数据时没有刻意按照主键大小排序,所以不能用二分法查找。 - 使用
MyISAM的表把索引信息存储到索引文件中。MyISAM会单独为表的主键创建一个索引,只不过在索引的叶子节点中存储主键+数据记录地址。
4.3 MyISAM 与 InnoDB对比
MyISAM的索引方式都是非聚簇的。小结两种引擎中索引的区别:
- 在InnoDB中,只需要根据主键值 对
聚簇索引进行一次查找就能找到对应的记录,而在MyISAM中却需要进行一次回表(根据地址去查记录) 操作,意味着MyISAM中建立的索引相当于全部都是二级索引。 - InnoDB的数据文件本身就是索引文件,而MyISAM索引文件和数据文件是
分离的,索引文件仅保存数据记录的地址。 - InnoDB的非聚簇索引data域存储相应记录
主键的值,而MyISAM索引记录的是地址。 - MyISAM的回表操作是十分
快速的,因为是拿着地址偏移量直接到文件中取数据的,反观InnoDB是通 过获取主键之后再去聚簇索引里找记录,虽然说也不慢,但还是比不上直接用地址去访问。 - InnoDB要求表
必须有主键。如果没有显式指定,则MySQL系统会自动选择一个可以非空且唯一标识数据记录的列作为主键。如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整型。
小结:
- 知道InnoDB索引实现后,很容易明白
为什么不建议使用过长的字段作为主键,因为所有二级索引引用主键索引,过长的主键使得二级索引过大。 - 用非单调的字段作为主键在InnoDB中不是好主意,非单调的主键会造成在插入新记录时,数据文件为了维持B+树的特性而频繁分裂调整,十分低效。建议使用
自增字段作为主键
5. 索引的代价
- 空间上的代价
每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用16KB的存储空间 - 时间上的代价
每次对表中的数据进行增、删、改操作时,都需要去修改各个B+树索引。B+树每层节点都是按照索引列的值从小到大的顺序排序而组成了双向链表。不论是叶子节点中的记录,还是内节点中的记录都是按照索引列的值从小到大的顺序而形成了一个单向链表。而增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些记录移位 , 页面分裂 、页面回收等操作来维护好节点和记录的排序。如果我们建了许多索引,每个索引对应的B+树都要进行相关的维护操作,会给性能拖后腿。
6. MySQL数据结构选择的合理性
如何让索引的数据结构尽量减少硬盘的IO操作是一个关键的问题。
数据量较大时,为了减少内存的占用,数据库索引一般是存储在外部磁盘上的,当我们利用索引查询时,不可能加载完整的索引到内存,只能逐一加载,MySQL衡量查询效率的标准就是磁盘的IO次数。
6.1 全表遍历
略
6.2 Hash结构
哈希算法相同的输入永远可以得到相同的输出
举例:验证两个文件是否相同
将两个文件进行Hash函数运算,比较结果
加快查找速度的数据结构,常见两种:
- 树,例如平衡二叉搜索树,CRUD平均时间复杂度为
O(log2N) - 哈希,例如HashMap,CRUD平均时间复杂度为
O(1)
上图中哈希函数h有可能将两个不同的关键字映射到相同的位置,这叫做
碰撞 ,在数据库中一般采用 链接法 来解决。在链接法中,将散列到同一槽位的元素放在一个链表中,如下图所示:
Hash结构效率高,那为什么索引结构要设计成树型呢?
- Hash索引仅满足(=) (<>) 和 IN查询。而
范围查询的时间复杂度会退化为O(n),而树形有序的特性使其依然保持O(log2N) - 数据的存储
没有顺序,在 ORDER BY的情况下,使用 Hash索引还需对数据重新排序 - 对于联合索引,Hash值是将索引键合并计算的,无法对单个或几个索引键查询
- 若
索引列的重复值很多,效率会降低。如 性别
Hash索引的适用性:
- Redis的存储核心就是 Hash表
- MySQL的MEMORY存储引擎支持哈希存储,如果我们需要用到查询的临时表,可以选择MEMORY。把某个字段设置为Hash索引,比如字符串类型的字段,Hash后能缩短到几个字节。当字段重复度低,而且经常需要
等值查询时,hash索引是个不错的方法。 - InnoDB不支持Hash索引,但提供了
自适应Hash索引。如果某个数据经常被访问,当满足一定条件时,就会将这个数据页地址存放到Hash表中。下次查询时可以直接找到页面所在位置,让B+树也具备了Hash索引的优点。
采用自适应 Hash 索引目的是方便根据 SQL 的查询条件加速定位到叶子节点,特别是当 B+ 树比较深的时候,通过自适应 Hash 索引可以明显提高数据的检索效率。
我们可以通过 innodb_adaptive_hash_index 变量来查看是否开启了自适应 Hash
show variables like '%adaptive_hash_index';
6.3 二叉搜索树
如果我们利用二叉树作为索引结构,那么磁盘的IO次数和索引树的高度是相关的。
二叉搜索树的特点
- 节点只能有两个子节点
- 左子节点 < 本节点,右子节点 >= 本节点
为了提高查询效率,就需要 减少磁盘IO数 。为了减少磁盘IO的次数,就需要尽量 降低树的高度 ,需要把原来“瘦高”的树结构变的“矮胖”,树的每层的分叉越多越好。由此我们引入了AVL树
6.4 AVL树
又称平衡二叉搜索树,是最早提出的平衡二叉树,具有以下性质:
它是一棵空树或者它的左右两个子树的高度的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树
每访问一次节点就要一次磁盘IO操作,对于上树需要五次IO操作。而怎么样能再减少IO操作呢?
针对同样的数据,把二叉树改为M叉树(M > 2),当 M 为 3时,同样31个节点如下:
此时树的高度降低了,只需4次IO操作。所以我们需要把树从高瘦转为矮胖
6.5 B-Tree
多路平衡查找树,横杠是连接符,不是减号。
B树的每一个节点最多可包括M个子节点,M称为B树的阶。每个磁盘块中包括了关键字和子节点的指针。如果一个磁盘块包括了x个关键字,指针树则为x+1。
一个M阶(M>2)的B树有以下特性:
- 根节点的儿子数的范围:[2,M]
- 每个中间节点包含 k-1 个关键字和 k个孩子,k的取值范围为[ceil(M/2),M]
- 叶子节点包含k-1个关键字,没有孩子
- 假设中间节点节点的关键字为:Key[1], Key[2], …, Key[k-1],且关键字按照升序排序,即Key[i]P[k],其中 P[1] 指向关键字小于 Key[1] 的子树,P[i] 指向关键字属于 (Key[i-1], Key[i]) 的子树,P[k] 指向关键字大于 Key[k-1] 的子树。
- 所有叶子节点在同一层
上面那张图所表示的 B 树就是一棵 3 阶的 B 树。我们可以看下磁盘块 2,里面的关键字为(8,12),它 有 3 个孩子 (3,5),(9,10) 和 (13,15),你能看到 (3,5) 小于 8,(9,10) 在 8 和 12 之间,而 (13,15)大于 12,刚好符合刚才我们给出的特征。
然后我们来看下如何用 B 树进行查找。假设我们想要 查找的关键字是 9 ,那么步骤可以分为以下几步:
- 我们与根节点的关键字 (17,35)进行比较,9 小于 17 那么得到指针 P1;
- 按照指针 P1 找到磁盘块 2,关键字为(8,12),因为 9 在 8 和 12 之间,所以我们得到指针 P2;
- 按照指针 P2 找到磁盘块 6,关键字为(9,10),然后我们找到了关键字 9。
B树的搜索过程中,我们比较的次数并不少,但如果把数据读取出来然后在内存中进行比较,这个时间就是可以忽略不计的。而读取磁盘块本身需要进行 I/O 操作,消耗的时间比在内存中进行比较所需要的时间要多,是数据查找用时的重要因素。 B 树相比于平衡二叉树来说磁盘 I/O 操作要少 , 在数据查询中比平衡二叉树效率要高。所以只要树的高度足够低,IO次数足够少,就可以提高查询性能 。
小结:
- B树在插入、删除节点时会自动调整节点保证树的平衡
- 关键字集合分布在整棵树中,即叶子节点和非叶子节点都存放数据。
- 搜索性能等价于在关键字全集内做一次二分查找
再举例:
6.6 B+Tree
B+树也是一种多路搜索树,基于B树做了些改进,主流的DBMS都支持B+树。B+树更适合文件索引系统
B+ 树和 B 树的差异:
- 有k个孩子的节点就有k个关键字。即孩子数量=关键字数,B树中,孩子数量=关键字数+1。
- 非叶子节点的关键字也会同时存在在子节点中,并且是在子节点中所有关键字的最大(或最小)。
- 非叶子节点仅用于索引,不保存数据记录,跟记录有关的信息都放在叶子节点中。而B树中,非叶子节点既保存索引,也保存数据记录 。
- 所有关键字都在叶子节点出现,叶子节点构成一个有序链表,而且叶子节点本身按照关键字的大小从小到大顺序链接。
B+树和B树根本的差异是:B+树的中间节点并不直接存储数据,这样的好处是什么呢?
B+树查询效率更稳定,只有访问到叶子节点才能得到相应数据,而B树中非叶子节点也会存储数据B+树查询效率更高,B+树通常比B树更矮胖,IO操作更少。同样的磁盘页大小,B+树可以存储更多的节点关键字- 不光是单个关键字查询,
在查询范围上,B+树的效率也比B树高。所有关键字都出现在B+树的叶子节点中,叶子节点之间有指针连接。而在B树中则需要通过中序遍历才能完成查询范围的查找,效率低很多。
思考题:为了减少IO,索引树会一次性加载吗?
- 数据库索引存储在磁盘上,若数据量很大必然导致索引也很大
- 我们只能逐一加载每个磁盘页,因为磁盘页对应着索引树的节点
思考题:B+树的存储能力如何?为何说一般查找行记录,最多只需1~3次磁盘IO
InnoDB中页的大小为 16KB,一般表的主键类型为INT(4字节)或BIGINT(8字节),指针类型一般也为4或8字节,所以一个页(B+树上的一个节点)中大概存储 16KB/(8B+8B) = 1K个键值。取估值1000,则深度为3的B+树索引可以维护 1000^3 = 10亿条记录
实际情况下每个节点可能填不满,因此数据库中B+树的高度一般在2~4层,MySQL中InnoDB在设计中将根节点常驻内存,所以最多只需1~3次磁盘IO
思考题:为什么说B+树比B-树更适合实际应用中操作系统的文件索引和数据库索引?
- 磁盘读写代价更低
B+树内部节点并没有指向关键字具体信息的指针,因此内部节相对比B树小,如果把所有同一内部节点的关键字存放在同一盘块,那么盘块所容纳的关键字也越多。一次性读入内存中的关键字也越多,相对来说IO读写次数也就降低了。- B+树查询效率更加稳定
由于非终节点并不是指向文件内容的节点,只是叶子节点中关键字的索引。所以任何关键字的查找必须走一条从根节点到叶子节点的路。所有关键字查询路径长度相同,每一个数据的查询效率相当。
思考题:Hash 索引与 B+ 树索引的区别
- Hash索引`不能进行范围查询,因为Hash索引指向的数据无序,而B+树的叶子节点是有序的链表
- Hash索引
不支持联合索引的最左侧原则(即联合索引的部分索引无法使用)。Hash索引在计算Hash值的时候将索引键合并后计算,不会针对每个索引单独计算Hash值。- Hash索引
不支持 ORDER BY 排序,因为Hash索引指向的数据无序。同理我们也无法用Hash索引进行模糊查询,而B+树使用 LIKE 进行模糊查询InnoDB不支持Hash索引
思考题:Hash 索引与 B+ 树索引是在建索引的时候手动指定的吗?
主流的InnoDB和MyISAM默认使用B+树,并且无法使用Hash索引,InnoDB提供的自适应Hash是不需要手动指定的。如果是MEMORY/Heap和NDB则可以选择Hash索引