索引的定义
MySQL官方对索引的定义为:索引(Index)是协助MySQL高效获取数据的数据结构。
本质上,索引的目的是为了提升查询效率,经过不断地缩小想要获取数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件,也就是说,有了这种索引机制,咱们能够老是用同一种查找方式来锁定数据。
能够类比银行的保险柜,好比你要找归属你的保险柜子。若是没有索引,你须要拿着钥匙,一个个的保险柜的试过去才能找到属于你的保险柜。可是若是有了索引,并且保险柜可以以物理分区的方式存在在对应的区域,同时你能够根据钥匙上的编号(A1003-10-17),找到保险柜所在 A1003的存放房间,找到存放室保险柜的第10排,再找到第17个位置,找到属于你的保险柜,这个定位就快不少了。在没有索引的状况下,要想完成这个事情仍是比较困难的。
索引的原理
除了保险柜以外,生活中能够引出不少相似的索引例子,如字典词典的目录、图书馆的检索录、火车的座次表等。
它们的原理一致:不断的缩小数据范围来筛选数据,并把随机数据变成顺序数据,方便咱们更快地锁定数据。
这种索引的理解一样适用咱们的数据库查询,可是数据库会有不少更复杂的状况,除了等值查询外,还有范围查询(>、<、between、in)、模糊查询(like)、并集查询(or)、交集查询(and)等等。这就要求数据库选择更加复杂和成熟的方式来应对全部问题。
根据咱们上面保险柜的案例,能够对数据按照必定规则进行拆分,这样匹配的范围就下降了,可是这远远不够知足数据库复杂的查询要求。因而,数据库系统的设计者从查询算法的角度进行优化。
其中最基本的查询算法是顺序查找(linear search),这种算法复杂度为O(n),在数据量很大时就很不理想了,并且数据量越大,计算越复杂。
但不要紧,强大的计算机科学提供了更多优秀的查找算法,好比二分查找(binary search)、二叉树查找(binary tree search)等。
可是这些查找算法都要求应用于特定的数据结构之上,如二分查找要求被检索数据有序,而二叉树查找只能基于二叉查找树结构上操做,数据自己的组织结构不可能彻底知足各类数据结构,理论上也没法同时要求将多列都按顺序进行组织。
所以,
在数据以外,数据库系统还维护着知足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就能够在这些数据结构上实现高级查找算法。这种数据结构,就是索引。
这与上面MySQL官方对索引的定义遥相呼应了。
看下面的图:
图举例了一种索引方式。右边是一个数据表,这边一共模拟了两列七行的数据,
字段1的是数据记录的物理地址(实际应用中逻辑上相邻的记录在磁盘上并不必定物理相邻,这边主要为了举例)。为了加快
字段2的查找,能够维护一个左边所示的二叉查找树,每一个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就能够运用二叉查找在O(log2n)O(log2n)的复杂度内获取到相应数据。
这是索引的一种表现形式,可是实际的数据库系统中比较广泛是采用B+树来实现的。B+树中的B表明平衡(balance),不是二叉(binary)。由于B+树是从最先的平衡二叉树演化而来的,因此咱们能够先了解下二叉查找树、平衡二叉树(AVLTree)和平衡多路查找树(B-Tree),由于B+树是由这些树逐步演进而来。
二叉查找树
二叉树具备如下性质:左子树的键值小于根的键值,右子树的键值大于根的键值。 因此左中右是依次递增的一个过程。
以下图所示就是一棵二叉查找树,
观察该二叉树有有以下发现,深度为1的节点的查找次数为1,深度为2的查找次数为2,深度为n的节点的查找次数为n,所以其平均查找次数为 (1+2+2+3+3+3+3) / 7 = 2.4次。
二叉查找树也能够是以下结构(一样知足二叉树 左 < 中 < 大的特性),一样是7,21,35,42,51,77,89 这七个数字,也能够按照下图的方式来构造:
可是这棵二叉树的查询效率就低了,平均查找次数为(1+2+3+4+5+6+6)/7=3.8次。
所以若想二叉树的查询效率尽量高,须要这棵二叉树是平衡的,从而引出新的定义:AVL树(即平衡二叉树)。
平衡二叉树(AVL Tree)
平衡二叉树(AVL树)在符合二叉查找树的条件下,
还知足任何节点的两个子树的高度最大差为1。下面的两张图片,左边是AVL树,它的任何节点的两个子树的高度差<=1;右边的不是AVL树,其根节点的左子树高度为3,而右子树高度为1,高度差>1;
同理,在平衡二叉树进行插入或删除节点,也可能致使AVL树失去平衡,这种失去平衡的二叉树能够有四种状态:LL(左左)、RR(右右)、LR(左右)、RL(右左)。
看下图示:
咱们来逐一看下这几种状态。
LL(LeftLeft),即 左左。是指插入或删除一个节点后,根节点的左孩子(Left Child)的左孩子(Left Child)还有非空节点,致使根节点的左子树比右子树高度>1,AVL树失去平衡。
RR(RightRight),即 右右。是指插入或删除一个节点后,根节点的右孩子(Right Child)的右孩子(Right Child)还有非空节点,致使根节点的右子树比左子树高度>1,AVL树失去平衡。
LR(LeftRight),即 左右。插入或删除一个节点后,根节点的左孩子(Left Child)的右孩子(Right Child)还有非空节点,致使根节点的左子树比右子树高度>1,AVL树失去平衡。
RL(RightLeft),即 右左。插入或删除一个节点后,根节点的右孩子(Right Child)的左孩子(Left Child)还有非空节点,致使根节点的右子树比左子树高度>1,AVL树失去平衡。
失去平衡的AVL树,能够经过旋转来修复,旋转的本质是将树的节点进行调整,达到恢复平衡的目的。下面逐一来看下。
LL的旋转:LL失去平衡的状况下,能够经过一次旋转让AVL树恢复平衡。步骤以下:
一、将根节点的左孩子做为新根节点。
二、将新根节点的右孩子做为原根节点的左孩子。
三、将原根节点做为新根节点的右孩子。
以下图所示:
RR的旋转:RR失去平衡的状况下,旋转方法与LL旋转相反,步骤以下:
一、将根节点的右孩子做为新根节点。
二、将新根节点的左孩子做为原根节点的右孩子。
三、将原根节点做为新根节点的左孩子。
以下图所示:
LR的旋转:LR失去平衡的状况下,须要进行两次旋转,步骤以下:
一、围绕根节点的左孩子进行RR旋转。
二、围绕根节点进行LL旋转。
以下图所示,它转了两次,最后恢复成一棵AVL树:
RL的旋转:RL失去平衡的状况下也须要进行两次旋转,旋转方法与LR旋转相反,步骤以下:
一、围绕根节点的右孩子进行LL旋转。
二、围绕根节点进行RR旋转。
以下图所示,它转了两次,最后恢复成一棵AVL树:
平衡多路查找树(B-Tree)
咱们知道,磁盘这种存储设备是以磁盘块(block)为基本单位的,而B-树也是基于这种存储方式设计的平衡查找树。
因此当咱们从系统磁盘读取数据时,以磁盘块(block)为基本单位映射到内存中,位于同一个磁盘块中的数据会被一次性读取出来,而不是只取须要的数据。InnoDB存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位。InnoDB存储引擎中默认每一个页的大小为16KB,可经过参数innodb_page_size将页的大小设置为4K、8K、16K,咱们能够在命令窗口输入如下脚本查看:
1 mysql> show variables like 'innodb_page_size';
2 +------------------+-------+
3 | Variable_name | Value |
4 +------------------+-------+
5 | innodb_page_size | 16384 |
6 +------------------+-------+
7 1 row in set
而系统一个磁盘块的存储空间每每没有这么大,所以InnoDB每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小16KB。
InnoDB在把磁盘数据读入到磁盘时会以页为基本单位,在查询数据时若是一个页中的每条数据都能有助于定位数据记录的位置,
这将会减小磁盘I/O次数,提升查询效率。
B-Tree结构的数据可让系统高效的找到数据所在的磁盘块。为了描述B-Tree,首先定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不一样的记录,key值互不相同。
一棵m阶的B-Tree有以下特性:
1. 每一个节点最多有m个孩子。
2. 除了根节点和叶子节点外,其它每一个节点至少有Ceil(m/2)个孩子。
3. 若根节点不是叶子节点,则至少有2个孩子
4. 全部叶子节点都在同一层,且不包含其它关键字信息
5. 每一个非终端节点包含n个关键字信息(P0,P1,…Pn, k1,…kn)
6. 关键字的个数n知足:ceil(m/2)-1 <= n <= m-1
7. ki(i=1,…n)为关键字,且关键字升序排序。
8. Pi(i=1,…n)为指向子树根节点的指针。P(i-1)指向的子树的全部节点关键字均小于ki,但都大于k(i-1)
B-Tree中的每一个节点根据实际状况能够包含大量的关键字信息和分支,以下图所示为一个3阶的B-Tree:
每一个节点占用一个盘块的磁盘空间,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个键值数据划分红的三个范围域对应三个指针指向的子树的数据的范围域。以根节点为例,两个键值数据为33和66,P1指针指向的子树的数据范围为小于33,P2指针指向的子树的数据范围为33~66之间,P3指针指向的子树的数据范围为大于66。
模拟查找关键字55的过程:
一、根据根节点找到磁盘块Disk1,读入内存。第1次操做磁盘I/O。
二、比较键值55在区间(33,66),找到磁盘块Disk1的指针P2。
三、根据P2指针找到磁盘块Disk3,读入内存。第2次操做磁盘I/O。
四、比较键值55在区间(39,62),找到磁盘块Disk3的指针P2。
五、根据P2指针找到磁盘块Disk8,读入内存。第3次操做磁盘I/O。
六、在Disk8中的键值列表中找到关键字55。
经过上面的操做过程,发现须要3次磁盘I/O操做,和3次内存查找操做。因为内存中的关键字是一个有序表结构,能够利用二分法查找提升效率。而3次磁盘I/O操做是影响整个B-Tree查找效率的决定因素。
B-Tree相对于AVLTree缩减了节点个数,使每次磁盘I/O取到内存的数据都发挥了做用,从而提升了查询效率。
B+Tree
B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构。
从上面的B-Tree结构图中能够看到每一个节点中不只包含数据的key值,还有data值。而每个页的存储空间是有限的,若是data数据较大时将会致使每一个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时一样会致使B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。在B+Tree中,全部数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样能够大大加大每一个节点存储的key值数量,下降B+Tree的高度,提升查找效率。
B+Tree相比较于B-Tree的不一样点:
一、非叶子节点只存储键值信息。
二、全部叶子节点之间都有一个链指针。
三、数据记录都存放在叶子节点中。
将上面的B-Tree优化,因为B+Tree的非叶子节点只存储键值信息,假设每一个磁盘块能存储4个键值及指针信息,则变成B+Tree后其结构以下图所示:
一般在B+Tree上有两个头指针,一个指向根节点,另外一个指向关键字最小的叶子节点,并且全部叶子节点(即数据节点)之间是一种链式环结构。所以能够对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另外一种是从根节点开始,进行随机查找。
可能上面例子中只有22条数据记录,看不出B+Tree的优势,下面作一个推算:
InnoDB存储引擎中页的大小为16KB,通常表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也通常为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(由于是估值,为方便计算,这里的K取值为〖10〗^3)。也就是说一个深度为3的B+Tree索引能够维护10^3 * 10^3 * 10^3 = 10亿 条记录。
实际状况中每一个节点可能不能填充满,所以在数据库中,B+Tree的高度通常都在2~4层。mysql的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只须要1~3次磁盘I/O操做。
数据库中的B+Tree索引能够分为汇集索引(clustered index)和辅助索引(secondary index)。上面的B+Tree示例图在数据库中的实现即为汇集索引,汇集索引的B+Tree中的叶子节点存放的是整张表的行记录数据。辅助索引与汇集索引的区别在于辅助索引的叶子节点并不包含行记录的所有数据,而是存储相应行数据的汇集索引键,即主键。当经过辅助索引来查询数据时,InnoDB存储引擎会遍历辅助索引找到主键,而后再经过主键在汇集索引中找到完整的行记录数据。
总结
根据上面,二叉查找树,红黑树等数据结构也能够用来实现索引,可是文件系统及数据库系统广泛采用B+Tree做为索引结构(
目前MySQL的MYISAM 和 INNODB 都是采用B+Tree做为索引结构),这是由于B+Tree索引的设计是以计算机磁盘存储结构为理论基础的。
索引以索引文件的形式存储在磁盘上,当采用B+Tree查找的时候,产生磁盘I/O消耗对性能的影响比其余方式小不少(
评价一个数据结构做为索引的优劣最重要的指标就是在查找过程当中磁盘I/O操做次数的渐进复杂度)。
换句话说,索引的结构组织要尽可能减小查找过程当中磁盘I/O的存取次数,而B+Tree无疑是较优的算法。