为什么使用索引
索引是存储引擎用于快速找到数据记录的一种数据结构。进行数据查找时,首先查看查询条件是否命中某条索引,符合则可以通过索引查找相关数据,如果不符合则要全表扫描,即需要一条一条地查找记录,直到找到与条件符合的记录。
假如给数据使用二叉树
进行存储,如下图所示:
对字段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)虽然索引大大提高了查询速度,同时却会降低更新表的速度 。
当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度。
InnoDB中索引推演
索引之前的查找
在一个页中查找
-
以主键为搜索条件
可以在页目录中使用
二分法
快速定位到对应的槽,然后遍历该槽对应分钟中的记录可快速找到指定的记录。 -
以其他列为搜索条件
在数据页中没有对非主键列建立页目录,因此无法用二分法快速定位相应的槽,只能从最小记录开始依次遍历单链表中的每条记录。
在很多页中查找
在大部分情况下,表中存放的记录是非常多的,需要很多数据页才能存储。在很多数据页中查找记录可以分为两个步骤:
- 定位到记录所在的页
- 在所在页中查找相应的记录
在没有索引的情况下,不论是根据主键列或者其他列的值进行查找,由于我们并不能快速的定位到记录所在的页,所以只能从第一个页沿着双向链表一直往下找
,在每一个页中根据我们上面的查找方式去查找指定的记录。因为要遍历所有的数据页,所以这种方式显然是很耗时的。如果一个表有一亿条记录呢?此时索引应运而生。
设计索引
建表
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)
- record_type : 记录头信息的一项属性,表示记录的类型, 0 表示普通记录、1表示目录项记录、 2 表示最小记录、 3 表示最大记录。
- next_record : 记录头信息的一项属性,表示下一条地址相对于本条记录的地址偏移量,我们用箭头来表明下一条记录是谁。
- 各个列的值 :这里只记录在 index_demo 表中的三个列,分别是 c1 、 c2 和 c3 。
- 其他信息 :除了上述3种信息以外的所有信息,包括其他隐藏列的值以及记录的额外信息。
效果如下:
将记录放到数据页里的示意图如下:
一个简单的索引设计方案
根据某个搜索条件查找一些记录时,为什么要遍历所有的数据页?因为各个页中的记录并没有规律,不得不依次遍历所有的数据页。
若想快速定位需要查找的记录在哪些数据页中?可以为快速定位记录所在的数据页建立一个目录,而建这个目录必须完成下面的事情:
-
下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值。
假设:每个数据页最多能存放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的页在物理存储上是不连续的,如果想从这么多页中根据主键值快速定位某些记录所在的页,需要做个目录,每个页对应一个目录项,每个目录项包括两部分:
- 页的用户记录中最小的主键值,key
- 页号,page_no
以页28为例,对应目录项2,这个目录项中包含着该页的页号28以及该页中用户记录的最小主键值5。那么需要将几个目录项在物理存储器上连续存储,就可以实现根据主键值快速查找某条记录的功能。
举例:查找主键值为20的记录,具体查找过程分两步:
1、从目录项中根据二分法快速确定出主键值为20的记录在目录项3中,对应的页为9
2、在页9中定位具体的记录
到这里,针对数据页做的简易目录就好了,这个目录有一个别名,称为索引。
InnoDB中的索引方案
迭代1次:目录项记录的页
上面简易方案中有如下几个问题:
1、需要非常大的连续的存储空间才能把所有的目录项都放下,对记录数量非常多的表是不现实的
2、常会对记录进行增删,如果将页28中的记录都删除了,意味着目录项2也不必存在,需要将目录项2后的目录项都向前移动一个,操作效率差
因此,需要一种可以灵活管理所有目录项的方式。
在InnoDB中怎么区分一条记录是普通的用户记录还是目录项记录?
使用记录头信息中的record_type属性,各个取值代表的意思如下:
0 表示普通记录
1表示目录项记录
2 表示最小记录
3 表示最大记录
目录项记录和普通用户记录的不同点:
- record_type值
- 目录项记录只有主键值和页编号,而普通用户记录是用户自己定义的。
- 记录头信息中有一个min_rec_mask属性,只有在存储目录项记录的页中的主键值最小的目录项记录的min_rec_mask值为1,其他别的记录的min_rec_mask值为0.
迭代2次:多个目录项记录的页
一个页只有16KB大小,能存放的目录项记录是有限的,如果表中的数据太多,以至于一个数据页不足存放所有的目录项记录,怎么处理?
生成新的目录项记录的页。
此时,根据主键值查找一条用户记录大致需要3个步骤:
1、确定目录项记录页
2、通过目录项记录页确定用户记录真实所在的页
3、在真实存储用户记录的页中定位到具体的记录
迭代3次:目录项记录页的目录页
和数据页一样,目录项记录页也是不连续的,如果表中的数据非常多,会产生很多存储目录项记录页,怎么根据主键值快速定位一个存储目录项记录的页?
为这些存储目录项记录的页再生成一个更高级的目录,像多级目录。
如图,生成一个存储更高级目录项的页33,这个页中的两条记录分别代表页30和页32,如果用户记录的主键值在[1,320)之间,则到页30中查找更详细的目录项记录。
随着表中记录的增加,这个目录的层级会继续增加,简化一下,可以用下图描述:
这个结构是——B+树。
常见索引概念
聚簇索引
一张表只能有一个聚簇索引
聚簇索引不是一种单独的索引类型,而是一种数据存储方式(所有的用户记录都存储在叶子节点)。
索引即数据,数据即索引
特点
1、使用记录主键值的大小进行记录和页的排序,包括三方面含义:
- 页内记录是按照主键的大小顺序排成一个单向链表
- 各个存放用户记录的页,也是根据页中用户记录的主键大小顺序排成一个双向链表
- 存放目录项记录的页分为不同的层次,在同一层次中的页是根据页中目录项记录的主键大小顺序排成一个双向链表。
2、B+树的叶子节点存储的是完整的用户记录(存储了所有列的值,包括隐藏列)
典型的聚簇索引(按主键)
优点
- 数据访问更快,将索引和数据保存在同一个B+树中
- 聚簇索引对于主键的排序查找和范围查找速度很快
- 可以节省大量的io操作
缺点
- 插入速度严重依赖插入顺序,按照主键的顺序插入是最快的方式,否则会出现页分裂,严重影响性能。对InnoDB表,一般定义一个自增的ID列为主键。
- 更新主键的代价很高
- 二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值查找到行数据。
限制
- 目前只有InnoDB数据引擎支持聚簇索引,而MyISAM不支持
- 每个MySQL的表只能有一个聚簇索引,一般是该表的主键。
- 如果没有定义主键,InnoDB会选择一个非空的唯一索引代替。
- 为了充分利用聚簇索引的特性,InnoDB表的主键列尽量选择有序的顺序id,而不建议用无序的id,如UUID、MD5、HASH、字符串列作为主键无法保证数据的顺序增长。
二级索引(非聚簇索引、辅助索引)
上面的聚簇索引只能在搜索条件是主键值时才能发挥作用。
那么想以别的列作为搜索条件,应该怎么办?
答案:可以多建几棵B+树,不同的B+树中的数据采用不同的排序规则。比如,采用c2列的大小作为数据页、页中记录的排序规则,再建一棵B+树。
和聚簇索引的不同点:
- 页内的记录按照c2列的大小顺序排成一个单向链表。
- 各个存放用户记录的页是根据页中记录的c2列大小顺序排列成一个双向链表
- 存放目录项记录的页分为不同的层次。
- 叶子节点存储的不是完整的用户记录,只是c2列+主键两个列的值。
以查找c2列的值为4的记录为例,查找过程如下:
1、确定目录页记录项,根据根页面44,可以快速定位到目录项记录所在的页为页42(2<4<9)
2、通过目录项记录页确定用户记录真实所在的页
3、在真实存储用户记录的页中定位到具体的记录
4、由于该B+树的叶子节点中的记录只存储了c2列和主键列,因此必须再根据主键值到聚簇索引中查找一遍完整的用户记录
因此,引出了一个重要的概念:回表。
回表
根据以c2列大小排序的B+树只能确定要查找记录的主键值,要查找完整的用户记录的话,需要到聚簇索引中再查找一遍,这个过程称为回表。
因为这种按照非主键列建立的B+树需要一次回表操作才能定位到完整的用户记录,因此被称为二级索引/辅助索引。
联合索引
可以同时以多个列的大小作为排序规则,即同时为多个列建立索引,例如让B+树按照c2和c3列的大小进行排序,有两层含义:
1、将各个记录和页按照c2列进行排序
2、在c2列相同的情况下,采用c3列进行排序
示意图:
注意:
- 每条目录项记录由c2、c3、页号三部分组成,各条记录先按照c2列的值进行排序。
- B+树叶子节点处的用户记录由c2、c3和主键c1列组成。
联合索引只会建立一棵B+树,但是如果是为c2和c3列分别建立索引会建立两棵B+树。
InnoDB的B+树索引的注意事项
根页面的位置不动
B+树形成过程:
- 每当为某个表创建一个B+树索引的时候,会为这个索引创建一个根节点页面。最开始表中没有数据的时候,每个B+树索引对应的根节点中没有用户记录,也没有目录项记录
- 向表中插入用户记录,先将用户记录存储到这个根节点中
- 当根节点中的可用空间用完时继续插入记录,此时会将根节点中所有记录复制到一个新分配的页,然后对这个新页进行页分裂操作,得到另一个新页b。这时新插入的记录根据键值的大小进行分配,而根节点升级为存储目录项记录的页。
一个B+树索引的根节点自诞生之日起,便不会移动。
非叶子节点中目录项记录的唯一性
主要针对非聚簇索引。
需要保证在B+树的同一层内节点的目录项记录除页号字段外是唯一的。
对二级索引的内节点的目录项记录的内容实际上由三部分构成。
- 索引列的值
- 主键值
- 页号
一个页面最少存储2条记录
如果一个大的目录中只存放一个子目录会导致目录的层级非常多,而最后那个存放真实数据的目录中只能存放一条记录,因此InnoDB的一个数据页至少要存放两条记录。