4. 深入浅出索引(上)

本篇文章主要对索引中用到的数据结构做了介绍

一.索引的常见模型

1.哈希表

创建一个数组,数组下标为key,值为value。比如原数据为x,利用哈希函数f(x)得到对应的key再用h[key]得到对应value。但可能遇到x不同但是key相同的冲突情况,如果遇到冲突,就拉一个链表处理

大概是下图这样
《4. 深入浅出索引(上)》

哈希表的缺点:由于哈希表存储的数值是不连续的,没有规律,所以在区间查询上效率很低,比如找id>=5且id<=10的数据,这时候哈希表要把id=[5,10]都放进哈希函数算一遍再查一下判断是否存在这样的数据,且出现冲突的时候也会增加查询的负担,最坏情况下会变成一个单链表(所有数据的哈希值都相同)

小结:哈希表适用于只有等值查询的场景,对于区间查询效率很低

2.有序数组

有序数组在等值查询和范围查询场景中的性能就都非常优秀。以根据身份证号查名字的例子,如果我们使用有序数组来实现的话,示意图如下所示(按照身份证号递增排序):

《4. 深入浅出索引(上)》

等值查询:那么查要查ID.card.n2的名字,只要用二分查找即可,时间复杂度为O(logn)

对于区间查询:你要查身份证号在[ID_card_X, ID_card_Y]区间的 User,可以先用二分法找到 ID_card_X(如果不存在 ID_card_X,就找到大于 ID_card_X 的第一个 User),然后向右遍历,直到查到第一个大于 ID_card_Y 的身份证号,退出循环。

有序数组的缺点:如果要插入新数据的话对于有序数组而言,时间复杂度最坏O(n),所以用有序数组插入数据成本较高,所以,有序数组索引只适用于静态存储引擎,即存储一组不会修改的数据

3.二叉搜索树

二叉搜索树特点:每个节点的左儿子小于父节点,父节点又小于右儿子

二叉搜索树的查询时间复杂度O(log(N))和更新节点时间复杂度都是O(log(N)) (要保持这棵树是平衡二叉树,这里N是树的深度)。虽然二叉搜索树在查询和更新节点上时间复杂度已经很低了。

但是实际上大多数的数据库存储却并不使用二叉树。其中一个原因是,索引不止存在内存中,还要写到磁盘上。当节点很多的时候,因为树高过高,一次查询可能要访问多个数据块,极其浪费时间

解决方法:为了让一个查询尽量少地读磁盘,就必须让查询过程访问尽量少的数据块。那么,我们就不应该使用二叉树,而是要使用“N 叉”树。这里,“N 叉”树中的“N”取决于数据块的大小。(N 叉树由于在读写上的性能优点,以及适配磁盘的访问模式,已经被广泛应用在数据库引擎中了。)

二叉搜索树的缺点:当树的深度较大的时候,二叉搜索树对于定值查询需要的次数会增加,且区间查询的效率可能会很慢,比如下图中找比5大的数,那么先找到5的节点之后,要回溯上去找大的数,如果5在比较深的节点,那么返回查找的过程效率会很差

《4. 深入浅出索引(上)》

4.B树

①所有节点关键字是按递增次序排列,并遵循左小右大原则;

②除了叶子结点外,其他节点都能有多个子节点,且每个节点都能存储数据

③一个节点可以同时存多个值,其他的特点和搜索二叉树一样,就导致树会变矮很多,查找速度也会快很多,但是和平衡二叉树一样,区间查询的时候回旋查找的问题依旧存在

《4. 深入浅出索引(上)》

5.B+树

①在B数树结构很像,叶子有一个链表(这个链表可以是双链表,mysql索引中就是用双链表),把插入的关键字从小到大进行了排序,且非叶子节只点存key,叶子节点key和value都存,value是数据的地址

B+②此时如果要找id大于5的数,直接搜到5的叶子节点后靠链表找下去就行了,就能解决回旋查找的问题,范围查找速度非常高,所以在排序的时候要使用索引排序

③由于节点存储信息的不同,B+tree对空间利用率更高

《4. 深入浅出索引(上)》

二.InnoDB 的索引模型

在 MySQL 中,索引是在存储引擎层实现的,所以并没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样。而即使多个存储引擎支持同一种类型的索引,其底层的实现也可能不同。由于 InnoDB 存储引擎在 MySQL 数据库中使用最为广泛,所以下面我就以 InnoDB 为例进行分析

在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。又因为前面我们提到的,InnoDB 使用了 B+ 树索引模型,所以数据都是存储在 B+ 树中的。每一个索引在 InnoDB 里面对应一棵 B+ 树。

下面举一个例子来讲解索引相关的概念:

假设一个表的内容如下:

主键索引(一级索引)和非主键索引(二级索引)的概念:

主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。

非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。

比如,现在有个章表里有自增长的id

如下图:

《4. 深入浅出索引(上)》

基于主键索引和普通索引的查询的区别?

如果语句是 select * from T where ID=500,即主键查询方式,则只需要搜索 ID 这棵 B+ 树;

如果语句是 select * from T where k=5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表。

也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。

索引维护

B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护。以上面这个图为例,如果插入新的行 ID 值为 700,则只需要在 R5 的记录后面插入一个新记录。如果新插入的 ID 值为 400,就相对麻烦了,需要逻辑上挪动后面的数据,空出位置。

而更糟的情况是,如果 R5 所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂(关于页分裂的讲解在文章下面。因为页面分裂的目的的就是为了保证主键主键能够保持递增,因为这里插入了id=400的数据,所以会发生数据跨页的挪动)。在这种情况下,性能自然会受影响。

除了性能外,页分裂操作还影响数据页的利用率。原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约 50%。

当然有分裂就有合并。当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程。

自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。

自增主键在建表语句中一般是这么定义的: NOT NULL PRIMARY KEY AUTO_INCREMENT。

补充:数据页面,数据区,页分裂的概念:

参考博客:https://blog.csdn.net/weixin_35765731/article/details/113613473?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162151140216780366526837%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=162151140216780366526837&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~times_rank-1-113613473.first_rank_v2_pc_rank_v29&utm_term=mysql%E4%BB%80%E4%B9%88%E6%98%AF%E9%A1%B5%E5%88%86%E8%A3%82&spm=1018.2226.3001.4187

**1.数据页:**在InnoDB存储引擎中,数据页是InnoDB磁盘管理的最小的数据单位,数据页的默认大小为16KB。单个数据页的大小并不是一成不变的。

数据页的结构如下
《4. 深入浅出索引(上)》

2.数据区:在MySQL的设定中,同一个表空间内的一组连续的数据页为一个extent(区),默认区的大小为1MB,页的大小为16KB。16*64=1024,也就是说一个区里面会有64个连续的数据页。连续的256个数据区为一组数据区。

3.数据页分裂问题:那假设你自定义了主键索引,而且你自定义的这个主键索引并不一定是自增的。

然后随着你将数据写入。就导致后一个数据页中的所有行并不一定比前一个数据页中的行的id大。这时就会触发页分裂的逻辑。

页分裂的目的就是保证:后一个数据页中的所有行主键值比前一个数据页中主键值大。

经过分裂调整,可以得到下面的这张图。

《4. 深入浅出索引(上)》

谈谈索引的使用场景

从性能和存储空间方面考量,自增主键往往是更合理的选择。

在选择用什么作为主键索引的时候,要考虑一下下面两个因素:从性能和存储空间方面考量。

问题一:业务中有身份证作为唯一字段,问是否能将其作为主键

答:用身份证作为主键本身没有问题,但是在存储空间上会有大量的浪费。因为用到主键索引的时候,会有一级索引和二级索引(概念上面已经提到了),主键索引的叶子节点存的是整行数据。非主键索引的叶子节点内容是主键的值,那么此时如果以身份证作为主键,那么每个二级索引的叶子节点占用约 20 个字节,而如果用整型做主键,则只要 4 个字节,如果是长整型(bigint)则是 8 个字节。

显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。

问题二:如果表中只有一个索引的情况

答:如果此时该表只有一个索引,就不用考虑这个问题了。而且经常需要使用这个业务字段查询数据,该字段值也唯一,这时我们就可以设置其为主键。即使该字段长度不是整型递增,索引维护较为困难一点,但是避免搜索两棵树,大大地提高了查询效率。查询往往比更新多

(这里我的理解是当只有一个可以作为索引的字段且这个字段唯一的时候我们可以用把这个字段当作主键,比如一张表里我只存了所有身份证号,那么虽然这时占用节点的空间多,但是这样就只要一级所索引树就能查询了,大大提高了效率)

《4. 深入浅出索引(上)》

小结:

B+ 树能够很好地配合磁盘的读写特性,减少单次查询的磁盘访问次数。由于 InnoDB 是索引组织表,一般情况下我会建议你创建一个自增主键,这样非主键索引占用的空间最小。但事无绝对,还是要根据实际场景分析。

    原文作者:Aki--
    原文地址: https://blog.csdn.net/qq_39687662/article/details/117091611
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞