索引是存储引擎用于快速找到数据记录的一种数据结构。进行数据查找时,首先查看查询条件是否命中某条索引,符合则可以通过索引查找相关数据,如果不符合则要全表扫描,即需要一条一条地查找记录,直到找到与条件符合的记录。
假如给数据使用二叉树
进行存储,如下图所示:
对字段Col2添加了索引,相当于在硬盘上为Col2维护了一个索引的数据结构,二叉搜索树
。二叉搜索树的每个结点存储的是(K,V)结构
,key是Col2,value是该key所在行的文件指针。例如:该二叉搜索树的根节点是(34,0x07)。现在对Col2添加了索引,这时候查询Col2=89这条记录的时候会先查找该二叉搜索树,读34到内存,89>34
,继续查找右侧数据,读89到内存,89 == 89
,找到数据。
目的:减少磁盘IO的次数,加快查询速率。
MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。
索引的本质:索引是数据结构
。你可以简单理解为“排好序的快速查找数据结构”,满足特定查找算法。这些数据结构以某种方式指向数据, 这样就可以在这些数据结构的基础上实现 高级查找算法 。
(1)类似大学图书馆建书目索引,提高数据检索的效率,降低数据库的IO成本
,这也是创建索引最主要的原因。
(2)通过创建唯一索引
,可以保证数据库表中每一行数据的唯一性
。
(3)在实现数据的参考完整性方面
,可以加速表和表之间的连接
。换句话说,对于有依赖关系的子表和父表联合查询时,可以提高查询速度。
(4)在使用分组和排序子句进行数据查询
时,可以显著减少查询中分组和排序的时间 ,降低了CPU的消耗
。
增加索引也有许多不利的方面,主要表现在如下几个方面:
(1)创建索引和维护索引要耗费时间
,并且随着数据量的增加,所耗费的时间也会增加。
(2)索引需要占磁盘空间 ,除了数据表占数据空间之外,每一个索引还要占一定的物理空间
, 存储在磁盘上 ,如果有大量的索引,索引文件就可能比数据文件更快达到最大文件尺寸。
(3)虽然索引大大提高了查询速度,同时却会降低更新表的速度 。
当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度。
以主键为搜索条件
可以在页目录中使用二分法
快速定位到对应的槽,然后遍历该槽对应分钟中的记录可快速找到指定的记录。
以其他列为搜索条件
在数据页中没有对非主键列建立页目录,因此无法用二分法快速定位相应的槽,只能从最小记录开始依次遍历单链表中的每条记录。
在大部分情况下,表中存放的记录是非常多的,需要很多数据页才能存储。在很多数据页中查找记录可以分为两个步骤:
在没有索引的情况下,不论是根据主键列或者其他列的值进行查找,由于我们并不能快速的定位到记录所在的页,所以只能从第一个页沿着双向链表一直往下找
,在每一个页中根据我们上面的查找方式去查找指定的记录。因为要遍历所有的数据页,所以这种方式显然是很耗时的。如果一个表有一亿条记录呢?此时索引应运而生。
建表
mysql> CREATE TABLE index_demo(
-> c1 INT,
-> c2 INT,
-> c3 CHAR(1),
-> PRIMARY KEY(c1)
-> ) ROW_FORMAT=Compact;
Query OK, 0 rows affected (0.57 sec)
mysql> desc index_demo;
+-------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+---------+------+-----+---------+-------+
| c1 | int | NO | PRI | NULL | |
| c2 | int | YES | | NULL | |
| c3 | char(1) | YES | | NULL | |
+-------+---------+------+-----+---------+-------+
3 rows in set (0.00 sec)
效果如下:
将记录放到数据页里的示意图如下:
根据某个搜索条件查找一些记录时,为什么要遍历所有的数据页?因为各个页中的记录并没有规律,不得不依次遍历所有的数据页。
若想快速定位需要查找的记录在哪些数据页中?可以为快速定位记录所在的数据页建立一个目录,而建这个目录必须完成下面的事情:
下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值。
假设:每个数据页最多能存放3条记录,然后向index_demo表中插入3条记录。
mysql> INSERT INTO index_demo VALUES(1,2,'u'),(3,9,'d'),(5,3,'y')
那么,这些记录已经按照主键值的大小串联成一个单向链表,如图所示:
从图中可以看出,index_demo表中的3条记录被插入到编号为10的数据页(record_type=0)中,此时再插入一条记录:
mysql> INSERT INTO index_demo VALUES(4,4,'a')
因为页10最多只能放3条记录,因此需要再分配一个新页。
新分配的数据页编号可能不是连续的,只是通过维护着上一个页和下一个页的编号而建立了链表关系。
在页10中用户记录最大的主键值是5,而页28中有一条记录的主键值是4,因为5>4,所以不符合下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值的要求,所以要进行一次记录移动,将主键值为5的记录移动到页28中,然后将主键值为4的记录插入到页10中,示意图如下:
在对页中的记录进行增删改操作的过程中,需要通过一些记录移动的操作来保持这个状态一直成立——这个过程为页分裂。
给所有的页建立一个目录项。
因为数据页的编号可能不连续,因此在向index_demo表中插入许多条记录后,可能是这样的:
这些16kb的页在物理存储上是不连续的,如果想从这么多页中根据主键值快速定位某些记录所在的页,需要做个目录,每个页对应一个目录项,每个目录项包括两部分:
以页28为例,对应目录项2,这个目录项中包含着该页的页号28以及该页中用户记录的最小主键值5。那么需要将几个目录项在物理存储器上连续存储,就可以实现根据主键值快速查找某条记录的功能。
举例:查找主键值为20的记录,具体查找过程分两步:
1、从目录项中根据二分法快速确定出主键值为20的记录在目录项3中,对应的页为9
2、在页9中定位具体的记录
到这里,针对数据页做的简易目录就好了,这个目录有一个别名,称为索引。
上面简易方案中有如下几个问题:
1、需要非常大的连续的存储空间才能把所有的目录项都放下,对记录数量非常多的表是不现实的
2、常会对记录进行增删,如果将页28中的记录都删除了,意味着目录项2也不必存在,需要将目录项2后的目录项都向前移动一个,操作效率差
因此,需要一种可以灵活管理所有目录项的方式。
在InnoDB中怎么区分一条记录是普通的用户记录还是目录项记录?
使用记录头信息中的record_type属性,各个取值代表的意思如下:
0 表示普通记录
1表示目录项记录
2 表示最小记录
3 表示最大记录
目录项记录和普通用户记录的不同点:
一个页只有16KB大小,能存放的目录项记录是有限的,如果表中的数据太多,以至于一个数据页不足存放所有的目录项记录,怎么处理?
生成新的目录项记录的页。
此时,根据主键值查找一条用户记录大致需要3个步骤:
1、确定目录项记录页
2、通过目录项记录页确定用户记录真实所在的页
3、在真实存储用户记录的页中定位到具体的记录
和数据页一样,目录项记录页也是不连续的,如果表中的数据非常多,会产生很多存储目录项记录页,怎么根据主键值快速定位一个存储目录项记录的页?
为这些存储目录项记录的页再生成一个更高级的目录,像多级目录。
如图,生成一个存储更高级目录项的页33,这个页中的两条记录分别代表页30和页32,如果用户记录的主键值在[1,320)之间,则到页30中查找更详细的目录项记录。
随着表中记录的增加,这个目录的层级会继续增加,简化一下,可以用下图描述:
这个结构是——B+树。
一张表只能有一个聚簇索引
聚簇索引不是一种单独的索引类型,而是一种数据存储方式(所有的用户记录都存储在叶子节点)。
索引即数据,数据即索引
1、使用记录主键值的大小进行记录和页的排序,包括三方面含义:
2、B+树的叶子节点存储的是完整的用户记录(存储了所有列的值,包括隐藏列)
上面的聚簇索引只能在搜索条件是主键值时才能发挥作用。
那么想以别的列作为搜索条件,应该怎么办?
答案:可以多建几棵B+树,不同的B+树中的数据采用不同的排序规则。比如,采用c2列的大小作为数据页、页中记录的排序规则,再建一棵B+树。
和聚簇索引的不同点:
以查找c2列的值为4的记录为例,查找过程如下:
1、确定目录页记录项,根据根页面44,可以快速定位到目录项记录所在的页为页42(2<4<9)
2、通过目录项记录页确定用户记录真实所在的页
3、在真实存储用户记录的页中定位到具体的记录
4、由于该B+树的叶子节点中的记录只存储了c2列和主键列,因此必须再根据主键值到聚簇索引中查找一遍完整的用户记录
因此,引出了一个重要的概念:回表。
根据以c2列大小排序的B+树只能确定要查找记录的主键值,要查找完整的用户记录的话,需要到聚簇索引中再查找一遍,这个过程称为回表。
因为这种按照非主键列建立的B+树需要一次回表操作才能定位到完整的用户记录,因此被称为二级索引/辅助索引。
可以同时以多个列的大小作为排序规则,即同时为多个列建立索引,例如让B+树按照c2和c3列的大小进行排序,有两层含义:
1、将各个记录和页按照c2列进行排序
2、在c2列相同的情况下,采用c3列进行排序
示意图:
注意:
联合索引只会建立一棵B+树,但是如果是为c2和c3列分别建立索引会建立两棵B+树。
B+树形成过程:
一个B+树索引的根节点自诞生之日起,便不会移动。
主要针对非聚簇索引。
需要保证在B+树的同一层内节点的目录项记录除页号字段外是唯一的。
对二级索引的内节点的目录项记录的内容实际上由三部分构成。
如果一个大的目录中只存放一个子目录会导致目录的层级非常多,而最后那个存放真实数据的目录中只能存放一条记录,因此InnoDB的一个数据页至少要存放两条记录。