1. 索引是什么
1.1 初识索引
+----+---------+------+
| id | name | age |
+----+---------+------+
| 1 | 帅哥1 | 30 |
| 2 | 帅哥2 | 18 |
| 3 | 帅哥3 | 25 |
| 4 | 帅哥4 | 21 |
| 5 | 帅哥5 | 29 |
| 6 | 帅哥6 | 35 |
+----+---------+------+
上表中,如果要寻找到id为6的数据,最差的情况则是进行6次IO操作,才能获取到数据。
若是使用二叉树的数据结构进行存储,则只需要3次IO即可获取到数据。
(不使用索引,也有可能一次就会获取到数据,所以在数据量少的情况下,不建议使用索引,且使用索引是需要额外使用存储空间的,对增删改也会造成性能影响)
MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。
索引的本质:索引是数据结构。你可以简单理解为“排好序的快速查找数据结构”,满足特定查找算法。这些数据结构以某种方式指向数据, 这样就可以在这些数据结构的基础上实现 高级查找算法 。
1.2 索引的优缺点
优点:
- 类似大学图书馆建书目索引,提高数据检索的效率,降低 数据库的IO成本 ,这也是创建索引最主要的原因
- 通过创建唯一索引,可以保证数据库表中每一行 数据的唯一性
- 在实现数据的参考完整性方面,可以 加速表和表之间的连接 。换句话说,对于有依赖关系的子表和父表联合查询时,可以提高查询速度。
- 在使用分组和排序子句进行数据查询时,可以显著 减少查询中分组和排序的时间 ,降低了CPU的消耗。
缺点:
- 创建索引和维护索引要 耗费时间 ,并且随着数据量的增加,所耗费的时间也会增加
- 索引需要占 磁盘空间 ,除了数据表占数据空间之外,每一个索引还要占一定的物理空间, 存储在磁盘上 ,如果有大量的索引,索引文件就可能比数据文件更快达到最大文件尺寸
- 虽然索引大大提高了查询速度,同时却会 降低更新表的速度 。当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度。
2. 如何创建索引
3. 索引的数据结构
3.1 行格式
CREATE TABLE index_demo(
c1 INT,
c2 INT,
c3 CHAR(1),
PRIMARY KEY(c1)
) ROW_FORMAT = Compact;
ROW_FORMAT(行格式)
- record_type :记录头信息的一项属性,表示记录的类型, 0 表示普通记录、 2 表示最小记录、 3 表示最大记录、 1目录项记录
- next_record :记录头信息的一项属性,表示下一条地址相对于本条记录的地址偏移量,我们用箭头来表明下一条记录是谁
- 各个列的值 :这里只记录在 index_demo 表中的三个列,分别是 c1 、 c2 和 c3
- 其他信息 :除了上述3种信息以外的所有信息,包括其他隐藏列的值以及记录的额外信息
把记录放到页中如下图所示
3.2 简单的索引设计方案
接下来以一个例子进行演示
CREATE TABLE index_demo(
c1 INT,
c2 INT,
c3 CHAR(1),
PRIMARY KEY(c1)
) ROW_FORMAT = Compact;
insert into index_demo values(1,4,'u'),(3,9,'d'),(5,3,'y')
这三条数据按照上图的存储结构存储如下图所示
接下来再次插入一条数据
insert into index_demo values(4,4,'a')
我们目前假设一页存储3条数据(但是实际上真正的存储远不止如此),页10已经存满数据了,所以此时不得不再次分配一页来存储 (4,4,‘a’) 这条数据
注意:因为新分配的数据页编号可能并不是连续的。他们只是通过维护着上一页和下一页的编号而建立了链表关系,另外,页10中最大值为5,页28中有一条数据为4,因为5>4,所以这并不符合下一页的数据主键最小值值必须大于上一页的主键最大值的要求,所以再插入(4,4,‘a’) 这条数据时需要伴随着一次记录移动,把主键值为5的记录移动到页28中,主键值为4的记录移动到页10中
页分裂:
这表明了在对页的记录进行增删改操作时,我们必须通过一些诸如记录移动的操作来始终保持上述状态一直成立
(下一页的数据主键最小值值必须大于上一页的主键最大值)这个过程我们成为页分裂,例如当前页存满了,再次分配一个新的页进行存储,这也称为页分裂。
由于数据页的编号可能是不连续的,所以在向index_demo表插入很多记录后,如下图所示
因为这些数据页在物理存储上不是连续的,所以如果想在这些页中根据主键值快速定位某条记录所在页数,我们需要给其做一个目录,每个页对应一个目录,每个目录包括下面两个部分:
- 页的用户记录中最小的主键值,我们使用key来进行表示
- 页号,我们使用page_no来进行表示
例如查找主键为20的记录,可以直接定位到在目录项3,页9上,因为(12<20<209),然后进入页9去寻找主键为20的记录。
上述就是数据页的简单目录(索引)
3.3 简单索引的迭代
上述情况中,如果页的数量很大,则对应的目录项也会很大,那么我们也可以将目录项的一条条记录构建为页,我们可以将目录项构建的页称为目录页,数据构成的页成为数据页,那么目录页和数据页如何区分呢?上述讲解到了行记录的概念,是通过record_type进行区分的。
record_type :记录头信息的一项属性,表示记录的类型, 0 表示普通记录、 2 表示最小记录、 3 表示最大记录、 1目录项记录
优化后如下图:
上图中,目录页和数据页的不同处:
- record_type
- 目录页中的一条条目录项只有主键值页页编号,而普通的数据页的数据不仅仅可以包含recort_type,字段等,还有InnoDB自己添加的隐藏列(注意:不同的存储引擎对应的索引的数据结构不同,这里概述的是InnoDB的索引的数据结构)
- 记录头信息里有min_rec_mask的属性,只存储于目录项中,在每一目录页中,主键值最小的目录项的min_rec_mask = 1,其余的为0。
相同点:
- 两者都是使用页的方式来存储,都会为主键生成Page Directory(页目录),从而在按照主键值进行查找时可以使用二分法进行查找,加快查询速度
现在以查找主键为 20 的记录为例,根据某个主键值去查找记录的步骤就可以大致拆分成下边两步:
- 先到存储 目录项记录 的页,也就是页30中通过 二分法 快速定位到对应目录项,因为 12 < 20 <
209 ,所以定位到对应的记录所在的页就是页9。 - 再到存储用户记录的页9中根据 二分法 快速定位到主键值为 20 的用户记录。
随着目录页的数量级变大,如下图所示
现在因为存储目录项记录的页不止一个,所以如果我们想根据主键值查找一条用户记录大致需要3个步骤,以查找主键值为 20 的记录为例:
- 确定 目录项记录页
我们现在的存储目录项记录的页有两个,即 页30 和 页32 ,又因为页30表示的目录项的主键值的范围是 [1, 320) ,页32表示的目录项的主键值不小于 320 ,所以主键值为 20 的记录对应的目录项记录在 页30 中。 - 通过目录项记录页 确定用户记录真实所在的页 。在一个存储 目录项记录 的页中通过主键值定位一条目录项记录的方式说过了。
- 在真实存储用户记录的页中定位到具体的记录
所以我们可以针对现有的目录页再给它创建一层目录页,如下图
如图,我们生成了一个存储更高级目录项的 页33 ,这个页中的两条记录分别代表页30和页32,如果用户记录的主键值在 [1, 320) 之间,则到页30中查找更详细的目录项记录,如果主键值 不小于320 的话,就到页32中查找更详细的目录项记录。
这个数据结构,它的名称是 B+树
一个B+树的节点其实可以分成好多层,规定最下边的那层,也就是存放我们用户记录的那层为第 0 层,之后依次往上加。之前我们做了一个非常极端的假设:存放用户记录的页 最多存放3条记录 ,存放目录项记录的页 最多存放4条记录 。其实真实环境中一个页存放的记录数量是非常大的,假设所有存放用户记录的叶子节点代表的数据页可以存放 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 条记录。
你的表里能存放 100000000000 条记录吗?所以一般情况下,我们 用到的B+树都不会超过4层 ,那我们通过主键值去查找某条记录最多只需要做4个页面内的查找(查找3个目录项页和一个用户记录页),又因为在每个页面内有所谓的 Page Directory (页目录),所以在页面内也可以通过 二分法 实现快速定位记录。
一页16KB(叶子节点存放100条,则1条数据160B,若一条数据量大的情况,则只是叶子节点存放的数据个数小,目录页的存储数量还是不变)
3.4 常见索引概念
索引按照物理实现方式,索引可以分为 2 种:聚簇(聚集)和非聚簇(非聚集)索引。我们也把非聚集索引称为二级索引或者辅助索引。
3.4.1 聚簇索引
特点:
- 使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:页内 的记录是按照主键的大小顺序排成一个 单向链表 。各个存放 用户记录的页 也是根据页中用户记录的主键大小顺序排成一个 双向链表 。存放 目录项记录的页 分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个 双向链表 。
- B+树的 叶子节点 存储的是完整的用户记录。所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。
优点:
- 数据访问更快 ,因为聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快
- 聚簇索引对于主键的 排序查找 和 范围查找 速度非常快按照聚簇索引排列顺序,查询显示一定范围数据的时候,由于数据都是紧密相连,数据库不用从多个数据块中提取数据,所以 节省了大量的io操作 。
缺点:
- 插入速度严重依赖于插入顺序 ,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键
- 更新主键的代价很高 ,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新
二级索引访问需要两次索引查找 ,第一次找到主键值,第二次根据主键值找到行数据
限制:
- Mysql中只有InnoDB支持聚簇索引,MyISAM不支持聚簇索引
- 由于数据物理存储排序方式只能有一种,所以每个Mysql的表只能有一个聚簇索引,一般情况下是主键
- 如果没有定义主键,InnoDB会选择非空的唯一索引来代替,如果没有这样的索引,InnoDB会隐式的定义一个主键来作为聚簇索引
- 为了充分的利用聚簇索引的聚簇特性,所以InnoDB表的主键尽量选择有序的顺序id,不建议使用无序的id,
3.4.2 二级索引(辅助索引、非聚簇索引)回表
这是我们的表结构
CREATE TABLE `index_demo` (
`c1` int(11) NOT NULL,
`c2` int(11) DEFAULT NULL,
`c3` char(1) DEFAULT NULL,
PRIMARY KEY (`c1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
如果我们想要使用别的字段进行查找该怎么办呢?例如c2,我们可以利用c2来建立索引,这就称为二级索引(辅助索引、非聚簇索引)
注意观察此时的叶子节点,存放的数据不是完整的数据,只存放了c2的值和主键的值(非聚簇索引的叶子节点是不存放隐藏列的)。(没有存放c3哦)
回表:我们根据这个以c2列大小排序的B+树只能确定我们要查找记录的主键值,所以如果我们想根据c2列的值查找到完整的用户记录的话,仍然需要到 聚簇索引 中再查一遍,这个过程称为 回表 。也就是根据c2列的值查询一条完整的用户记录需要使用到 2 棵B+树
因为c2不是主键,如果为c2创建索引,它是非聚簇索引,是不存放具体的每一行的数据的,所以根据c2构建的B+树寻找到对应的数据的主键,还得去聚簇索引构建的B+树去根据非聚簇索引构建的B+树得到的主键值去寻找对应的数据,相当于进行了两个B+树的遍历。
到这里也清楚了聚簇索引和非聚簇索引的区别以及优缺点。
3.4.3 联合索引
我们也可以同时以多个列的大小作为排序规则,也就是同时为多个列建立索引,比方说我们想让B+树按照 c2和c3列 的大小进行排序,这个包含两层含义:
- 先把各个记录和页按照c2列进行排序。
- 在记录的c2列相同的情况下,采用c3列进行排序
注意一点,以c2和c3列的大小为排序规则建立的B+树称为 联合索引 ,本质上也是一个二级索引。它的意思与分别为c2和c3列分别建立索引的表述是不同的,不同点如下:
- 建立 联合索引 只会建立如上图一样的1棵B+树。
- 为c2和c3列分别建立索引会分别以c2和c3列的大小为排序规则建立2棵B+树。
3.5 InnoDB的B+树索引的注意事项
3.5.1 根页面万年不动
在介绍B+树索引时,当时是先有数据页再有目录页的,但是实际上的不是这样,真实的情况如下
- 当为表创建一个B+树索引(聚簇索引不是人为创建的,默认就有)的时候,都会为这个索引创建一个根节点最开始没有数据时,根节点既没有用户记录,也没有目录项记录
- 随后向表中插入数据,先把用户记录存到根节点中
- 当根节点可用空间满的时候,会将根节点的数据复制到新开辟的一个页中,对新的页进行页分裂,此时根节点就不存储用户记录了,转而存储目录页,当根节点的容量不够存储目录项时,会复制目录项到新开辟的一个页,继续对该页进行页分裂,此时根节点存储 “更高级” 的目录项(这个更高级指的是在树结构的高度上的更高级)后续同理。
注意:一个B+树的索引的根节点自诞生起便不会移动,我们对表创建索引时,他的根节点的页号会被记录,后续InnoDB存储引擎需要用到索引时,直接取出根节点的页号进行查询即可。
3.5.2 内节点中目录项记录的唯一性