Mysql底层原理四:B+树索引

B+树索引(索引的原理)

1.前言

前边我们详细唠叨了InnoDB数据⻚的7个组成部分,知道了各个数据⻚可以组成⼀个双向链表,⽽每个数据⻚中的记录会按照主键值从⼩到⼤的顺序组成⼀个单向链 表,每个数据⻚都会为存储在它⾥边⼉的记录⽣成⼀个⻚⽬录,在通过主键查找某条记录的时候可以在⻚⽬录中使⽤⼆分法快速定位到对应的槽,然后再遍历该槽 对应分组中的记录即可快速找到指定的记录(如果你对这段话有⼀丁点⼉疑惑,那么接下来的部分不适合你,返回去看⼀下数据⻚结构吧)。⻚和记录的关系示 意图如下:

2.没有索引的查找

2.1 在一个页中查找

  • 以主键为搜索条件
  • 以其他列为搜索条件
    对⾮主键列的查找的过程可就不这么幸运了,因为在数据⻚中并没有对⾮主键列建⽴所谓的⻚⽬录,所以我们⽆法通过⼆分法快速定位相应的槽。这种情况下 只能从最⼩记录开始依次遍历单链表中的每条记录,然后对⽐每条记录是不是符合搜索条件。很显然,这种查找的效率是⾮常低的。

2.2 在很多个页中查找

在很多页的时候,我们应该是先定位到对应的页,然后到页中根据槽来查找。但是在没有索引的情况下,由于我们并不能快速定位到记录所在的页,所以只能从第一个页沿着链表遍历所有的页,是不是超级超级蠢。

3.一个简单的索引方案

问题:为什么我们要遍历所有的数据页拿?

因为各个页之间没有规律,我们并不知道怎么去匹配记录,所以不得不遍历所有的数据页。如果我们想快速定位到需要查找的记录该怎么办,其实参照记录目录的方法,我们是不是也可以建立一个有序的的目录🙏,建立这个目录必须完成以下这些事:

  1. 下一个页的记录主键值必须大于 上一个页记录的主键值
  2. 页分裂(一个页放不下的时候,分成两个页)时,依旧要保证上面说的关系。

1)准备工作

建表:

mysql>	CREATE	TABLE	index_demo(->    	c1	INT,->    	c2	INT,->    	c3	CHAR(1),->    	PRIMARY	KEY(c1)->	)	ROW_FORMAT	=	Compact;
Query	OK,	0	rows	affected	(0.03	sec)

插入数据:

mysql>	INSERT	INTO	index_demo	VALUES(1,	4,	'u'),	(3,	9,	'd'),	(5,	3,	'y');
Query	OK,	3	rows	affected	(0.01	sec)
Records:	3 	Duplicates:	0 	Warnings:	0

这时候的页如图所示:

2)插入数据测试

再插入一条记录:

mysql>	INSERT	INTO	index_demo	VALUES(4,	4,	'a');
Query	OK,	1	row	affected	(0.00	sec)

因为页10只能放3个记录,所以我们不能不再分配一个页:

问题:为什么页号不是连续的?

新分配的数据页编号可能并不是连续的,也就是我们使用的页空间上并不是连续的。它们只是通过维护上下页来建立链表关系。另外,⻚10中⽤户记录最⼤的主键值是5,⽽⻚28中有⼀条记录的主键值是4,因为5 > 4,所 以这就不符合下⼀个数据⻚中⽤户记录的主键值必须⼤于上⼀个⻚中⽤户记录的主键值的要求,所以在插⼊主键值为4的记录的时候需要伴随着⼀次记录移动,也 就是把主键值为5的记录移动到⻚28中,然后再把主键值为4的记录插⼊到⻚10中,这个过程的示意图如下:

3)  给页建目录

以页28为例,它对应目录项2,这个目录项包含主键值,页号。比方说我们想查找主键值为20的记录,具体步骤粪两步:

  1. 先从⽬录项中根据⼆分法快速确定出主键值为20的记录在⽬录项3中(因为 12 < 20 < 209),它对应的⻚是⻚9。
  2. 再根据前边说的在⻚中查找记录的⽅式去⻚9中定位具体的记录。

记住,这个目录有个别名,就是索引,哈哈

4. InnoDB的索引方案

4.1  上述的索引方案有什么问题?

  1. 当表中数据不断增多的时候,目录项非常耗费空间,这对于记录数比较多的表不太现实
  2. 我们时常会对记录做增删,假设我们把页28中的记录全部删除了,页28也就没有必要存在 ,同时目录项2也没有存在的必要了。

设计InnoDB的大叔们灵光乍现,发现这些目录项其实和数据页长的差不多,只不过目录项存储的列值时主键和页号,所以他们复用了数据页的结构来存储目录项,为了区分,利用 头信息的record_type属性。 它的各个取值代表意思如下:

  • 0:普通记录
  • 1:目录项
  • 2:最小记录
  • 3:最大记录

4.2 最终实现的样子

从图中可以看出,我们新分配了一个编号为30的页用来专门存储目录项记录。这里再次强调一遍 目录项记录和普通的 用户记录的不同点:

  • 目录项记录的record_type值是1,而普通用户记录的record_type值是0。
  • 目录项记录只有主键值和页的编号两个列,而普通的用户记录是用户自己定义的,可能包含很多列,另外还有InnoDB自己添加的隐藏列。
  • 还记得我们之前在唠叨记录头信息的时候说过一个 min_rec_mask的属性么,只有在存储目录项的页中的**主键值的目录项记录的min_rec_mask值为1,**其他都是0

假设我们按照某个主键20去查找记录,大致步骤分以下几步:

  1. 先看页的目录,也就是30的页通过二分法(这里指的是槽)找到对应的目录项记录,因为12<20<209,所以定位到页9。
  2. 再到页9中,根据二分法快速定位到对应的用户记录。

问题:虽然说目录项记录中只存储主键值和对应的页号,比用户记录需要的空间存储小多了,但是一个页也就16kb,能存放的目录项记录也是有限的,那如果表中的记录数据太多,以至于一个数据页不足以存放所有的目录项记录,该怎么办?

4.3 当一个页目录不够的时候怎么办

当然是再准备一个目录页啦,~ 为了⼤家更好的理解新分配⼀个⽬录项记录⻚的过程,我们假设⼀个存储⽬录项记录的⻚最多只能存放4条⽬录项记录(请 注意是假设哦,真实情况下可以存放好多条的),所以如果此时我们再向上图中插⼊⼀条主键值为320的⽤户记录的话,那就需要分配⼀个新的存储⽬录项记录的⻚喽:

从图中可以看出,我们插入一个主键值为320的时候需要两个数据页:

  • 为存储该记录,新增页31
  • 因为原先存储的目录项记录30已经满了,所以新生成一个目录项页32

假设我们还是要查找主键为20的记录,大致步骤分以下几步:

  1. 确定目录项记录页(先确定记录在哪个目录页内),我们现在的存储⽬录项记录的⻚有两个,即⻚30和⻚32,⼜因为⻚30表示的⽬录项的主键值的范围是[1, 320),⻚32表示的⽬录项的主键值不⼩于320,所以 主键值为20的记录对应的⽬录项记录在⻚30中。
  2. 确定记录所在页(确定真实记录在哪个页内),在一个存储目录项记录的页中通过主键值二分法定位的一条目录项记录
  3. 在数据页中查找主键为20的记录

4.4 当一层页目录越来越多时怎么办

问题:如果数据量特别大,目录页就会越来越多,查询性能就会低下,这时候怎么办?

给页目录再生成一个目录,就像这样:

如图,我们⽣成了⼀个存储更⾼级⽬录项的⻚33,这个⻚中的两条记录分别代表⻚30和⻚32,如果⽤户记录的主键值在[1, 320)之间,则到⻚30中查找更详细的⽬ 录项记录,如果主键值不⼩于320的话,就到⻚32中查找更详细的⽬录项记录。

如果画的更加好一点,最终的样子是这样的:

这玩意是不是长得特别像倒过来的数呀,上头是树根,下头是树叶,我们称之为 B+数

无论是存放用户记录的数据页还是存放目录像记录的数据页,都放到这个B+数结构中了,所以我们都称之为节点。**从图中我们可以看出用户记录时存放到B+树最底层的节点上,我们称之为叶子节点。**其余用来存放目录项的节点称之为非叶子节点。

存储能力:

4.5 常说的聚簇索引是什么

我们上边介绍的B+树本身就是一个目录,或者说本身就是一个索引,它有两个特点:

  1. 使用记录主键值的大小进行记录和页的排序,这包括以下三个方面的含义:
    • ⻚内的记录是按照主键的⼤⼩顺序排成⼀个单向链表。
    • 各个存放⽤户记录的⻚也是根据⻚中⽤户记录的主键⼤⼩顺序排成⼀个双向链表。
    • 存放⽬录项记录的⻚分为不同的层次,在同⼀层次中的⻚也是根据⻚中⽬录项记录的主键⼤⼩顺序排成⼀个双向链表。
  2. B+树的叶子节点存储的是完整的用户记录(这个记录中存储了所有列的值

我们把具有这两种特性B+树称之为聚簇索引 。所有完整的⽤户记录都存放在这个聚簇索引的叶⼦节点处。这种聚簇索引并不需要我们在MySQL语句中显式的使⽤INDEX 语句去创建(后边会介绍索引相关的语句),InnoDB存储引擎会⾃动的为我们创建聚簇索引。另外有趣的⼀点是,在InnoDB存储引擎中,聚簇索引就是数据的存储 ⽅式(所有的⽤户记录都存储在了叶⼦节点),也就是所谓的索引即数据,数据即索引。

4.6 二级索引

问题:上面所说的索引都是围绕主键建立的,当搜索条件是其他列怎么办?

我们可以多建⼏棵B+树,不同的B+树中的数据采⽤不同的排序规则。⽐⽅说我们⽤c2列的⼤⼩作为数据⻚、⻚中记录的排序规则,再建⼀棵B+树,效果如下 图所示:

这个B+树与上面说的聚簇索引有几处不同:

  1. 页内使用记录c2列的大小顺序成一个单链表
  2. 各个存放⽤户记录的⻚也是根据⻚中记录的c2列列⼤⼩顺序排成⼀个双向链表。
  3. 存放⽬录项记录的⻚分为不同的层次,在同⼀层次中的⻚也是根据⻚中⽬录项记录的c2列⼤⼩顺序排成⼀个双向链表。
  4. B+树的叶子节点存储的并不是完整的用户记录,而只是c2列+主键这两个列的值。
  5. 目录像记录中也不再是主键+页号的搭配,而变成了c2列+页号的搭配。

假设我们现在想通过c2列为4的记录为例,查找过程如下:

  1. 根据最高层级的页44确定下一级的目录页42,因为(2<4<9)
  2. 根据页42可以找到真实数据所在的页
    在页42中可以快速定位到实际存储用户记录的页,但是由于c2列并没有唯一性约束,所以c2列值为4的记录可能分布在多个数据页中,又因为2<4<=4,所以可以确定用户记录存放到页34和页35
  3. 到真实页之后,再通过二分法槽找到具体的记录
  4. 最重要的一步二级索引的就是回表:根据主键再去聚簇索引中查找一遍完整的用户记录

问题:为什么用回表操作,不是将数据存储到叶子节点那?

如果把完整的⽤户记录放到叶⼦节点是可以不⽤回表,但是太占地 ⽅了呀~相当于每建⽴⼀棵B+树都需要把所有的⽤户记录再都拷⻉⼀遍,这就有点太浪费存储空间了。因为这种按照⾮主键列建⽴的B+树需要⼀次回表操作才可以 定位到完整的⽤户记录,所以这种B+树也被称为⼆级索引(英⽂名secondary index),或者辅助索引。由于我们使⽤的是c2列的⼤⼩作为B+树的排序规则,所以 我们也称这个B+树为为c2列建⽴的索引。

4.7 联合索引

我们也可以同时以多个列的大小作为排序规则,建立索引,比方说我们想让B+树按照c2和c3列的大小进行排序,这个包含两层含义:

  1. 先把各个记录和页按照c2列排序
  2. 在c2列相同的情况下,采用c3列进行排序

为c2列和c3列建立的索引的示意图如图:

如图所示,我们 需要注意以下几点:

  • 每个目录项记录都是由c2、c3和页号组成,各条记录先按照c2排序,再按照c3排序
  • 叶子节点是由c2、c3和主键c1组成。

其实,本质也是个二级索引,只是无论目录项记录还是数据页记录多了个列而已。

5.InnoDB索引细节

5.1 索引的生成过程 (比较重要)

  1. 每当为某个表创建一个B+树索引的时候,就会为这个索引创建一个根节点页面。最开始既没有目录页,也没有数据页。
  2. 随后向表中插入数据时,先把用户记录存储到根节点中。
  3. 继续向根节点插入数据后,此时根节点的数据会复制到一个新的页,比如页a,然后对这个页做页分裂,得到一个新页,比如页b。往后新插入的数据就会放到a或者b中,根节点就升级成了目录页

一个b+树索引的根节点自诞生之日后,就不会再移动。这样只要我们对某个表建⽴⼀个索引,那么它的根节点的⻚号便会被记录 到某个地⽅,然后凡是InnoDB存储引擎需要⽤到这个索引的时候,都会从那个固定的地⽅取出根节点的⻚号,从⽽来访问这个索引。

⼩贴⼠: 跟⼤家剧透⼀下,这个存储某个索引的根节点在哪个⻚⾯中的信息就是传说中的数据字典中的⼀项信息,关于更多数据字典的内容, 后边会详细唠叨,别着急哈。

5.2 非唯一二级索引

如果我们的数据是这样的:

如果二级索引中目录项记录的内容只是索引列+页号的话,那么图示:

这时候,如果我们想插入一行记录(9,1,‘c’),那么这个时候遇到一个很大的问题,这时候这条记录是插到页4还是页5那?

为了能够让插入的记录能找到自己所在的页,**我们需要保证B+树同一层节点的目录项记录除了页号以外这条记录是唯一的。**所以这时候我们需要再加入一个列:主键。

也就是我们把主键值也添加到⼆级索引内节点中的⽬录项记录了,这样就能保证B+树每⼀层节点中各条⽬录项记录除⻚号这个字段外是唯⼀的,所以我们为c2列建 ⽴⼆级索引后的示意图实际上应该是这样⼦的:

这时候,当我们想插入一条(9,1,‘c’)时,先比较c2列,如果c2列是一样的,再比较主键列,主键列肯定是不一样的,9>7,所以新纪录应该被插入到页5中。

6.总结

6.1 如果没有索引的查找会是很恐怖的,无论是在一个页还是多个页中查找,基本都是遍历的逻辑。

6.2 当我们想通过建立一个类似书籍目录一样的结构来索引数据页的时候,会存在很多问题,比如耗空间、多一种设计实现方式

6.3 最终,我们通过复用数据页的结构来索引数据页,这个叫做目录页,目录页的主要作用就是索引页

6.4 不同的索引具体实现方式

  • 聚簇索引,数据页主键+用户记录,目录页主键+页号
  • 二级唯一索引:数据页排序列+主键,目录页排序列+主键
  • 二级非唯一索引:数据页排序列+主键,目录页排序列+主键+页号 (非唯一的话,目录页多个主键列
  • 联合索引:数据页排序列1+排序列2+主键,目录页排序列1+排序列2+页号

6.5索引的生成方式

参照 5.1,结束peace

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/304358.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python球球大作战

文章目录 写在前面球球大作战程序设计注意事项写在后面 写在前面 安装pygame的命令&#xff1a; pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pygame球球大作战 《球球大作战》是一款简单易上手、充满趣味性和竞技性的休闲手游。游戏的核心玩法可以用一句话概…

(React Hooks)前端八股文修炼Day9

一 对 React Hook 的理解&#xff0c;它的实现原理是什么 React Hooks是React 16.8版本中引入的一个特性&#xff0c;它允许你在不编写类组件的情况下&#xff0c;使用state以及其他的React特性。Hooks的出现主要是为了解决类组件的一些问题&#xff0c;如复杂组件难以理解、难…

qt-C++笔记之QLabel加载图片

qt-C笔记之QLabel加载图片 —— 2024-04-06 夜 code review! 文章目录 qt-C笔记之QLabel加载图片0.文件结构1.方法一&#xff1a;把图片放在项目路径下&#xff0c;在 .pro 文件中使用 DISTFILES添加图片文件1.1.运行1.2.qt_test.pro1.3.main.cpp 2.方法二&#xff1a;不在 .pr…

《深入Linux内核架构》第2章 进程管理和调度 (2)

目录 2.4 进程管理相关的系统调用 2.4.1 进程复制 2.4.2 内核线程 2.4.3 启动新程序 2.4.4 退出进程 本专栏文章将有70篇左右&#xff0c;欢迎关注&#xff0c;订阅后续文章。 2.4 进程管理相关的系统调用 2.4.1 进程复制 1. _do_fork函数 fork vfork clone都最终调用_…

计算机网络 Telnet远程访问交换机和Console终端连接交换机

一、实验要求和内容 1、配置交换机进入特权模式密文密码为“abcd两位班内学号”&#xff0c;远程登陆密码为“123456” 2、验证PC0通过远程登陆到交换机上&#xff0c;看是否可以进去特权模式 二、实验步骤 1、将一台还没配置的新交换机&#xff0c;利用console线连接设备的…

数学建模-最优包衣厚度终点判别法-二(K-Means聚类)

&#x1f49e;&#x1f49e; 前言 hello hello~ &#xff0c;这里是viperrrrrrr~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#xff…

OpenAI曾转录100万小时视频数据,训练GPT-4

4月7日&#xff0c;纽约时报在官网发布了一篇名为《科技巨头如何挖空心思&#xff0c;为AI收集数据》的技术文章。 纽约时报表示&#xff0c;OpenAI曾在2021年几乎消耗尽了互联网有用的文本数据源。为了缓解训练数据短缺的难题&#xff0c;便开发了知名开源语音识别模型Whispe…

Leetcode 394. 字符串解码

心路历程&#xff1a; 这道题看到括号直接想到栈&#xff0c;五分钟新题直接秒了&#xff0c;一开始以为需要两个栈分别存储数字和非数字&#xff0c;后来发现一个栈就够了&#xff0c;思路如图&#xff1a; 这道题考察的应该是队栈这两种数据结构的转换&#xff0c;因为每次…

C语言比较两个字符串是否相等是很容易的

一、概要 两个字符串char str1[n]和char str2[n] while循环&#xff0c;开始前i置为0&#xff0c;如果两个字符串都没有到末尾&#xff0c;且str1[i]str2[i]&#xff0c;则i&#xff0c;循环继续 循环结束之后&#xff0c;如果两个字符串都到了末尾(str1[i]\0 &&…

Java零基础入门-Java反射机制

一、概述 我们都听说过java有个反射机制&#xff0c;通过反射机制我们可以更深入的控制程序的运行过程。例如&#xff0c;在程序进入到运行期间&#xff0c;由用户输入一个类名&#xff0c;然后我们可以动态获取到该类拥有的所有类结构、属性名和方法&#xff0c;甚至还可以任意…

Vue3---基础1(认识,创建)

变化 相对于Vue2&#xff0c;Vue3的变化&#xff1a; 性能的提升 打包大小减少 41% 初次渲染快 55%&#xff0c;更新渲染快133% 内存减少54% 源码的升级 使用 proxy 代替 defineProperty 实现响应式 重写虚拟 DOM 的实现和 Tree-shaking TypeScript Vue3就可以更好的支持TypeSc…

PHP 伪协议:使用 php://input 访问原始 POST 数据

文章目录 参考环境PHP 伪协议概念为什么需要 PHP 伪协议&#xff1f; php://input为什么需要 php://input&#xff1f;更灵活的数据处理减小性能压力 发送 POST 数据HackBarHackBar 插件的获取 $_POST打开 HackBar 插件通过 HackBar 插件发起 POST 请求 基操 enable_post_data_…

Linux——fork复制进程

1)shell: 在计算机科学中&#xff0c;Shell俗称壳&#xff08;用来区别于核&#xff09;&#xff0c;是指“为使用者提供操作界面”的软件&#xff08;command interpreter&#xff0c;命令解析器&#xff09;。它类似于DOS下的COMMAND.COM和后来的cmd.exe。它接收用户命令&…

SpringBoot中的Redis的简单使用

在Spring Boot项目中使用Redis作为缓存、会话存储或分布式锁等组件&#xff0c;可以简化开发流程并充分利用Redis的高性能特性。以下是使用Spring Boot整合Redis的详细步骤&#xff1a; 1. 环境准备 确保开发环境中已安装&#xff1a; Java&#xff1a;用于编写和运行Spring…

微服务-6 Gateway网关

一、网关搭建 此时浏览器访问 localhost:10010/user/list 后正常返回数据&#xff0c;说明网关已生效&#xff0c;其原理流程图如下&#xff1a; 二、网关过滤器 作用&#xff1a;处理一切进入网关的请求和微服务响应。 1. 网关过滤器的分类&#xff1a; a. 某个路由的过滤器 …

LeetCode Meditations:合并 K 排序列表

描述 合并K分类列表 状态&#xff1a; 您有一系列 k 链接-列表 lists &#xff0c;每个链接-列表按升序排序。 合并所有链接-列表为一个排序的链接-列出并返回。 例如&#xff1a; Input: lists [[1, 4, 5], [1, 3, 4], [2, 6]] Output: [1, 1, 2, 3, 4, 4, 5, 6] Explanatio…

地理信息系统(ArcGIS)在水文水资源、水环境中的应用

刘老师&#xff08;副教授&#xff09;&#xff1a;来自北京重点高校资深专家&#xff0c;长期从事水资源与水环境、流域污染控制与管理、非点源模拟与控制、环境信息系统开发、环境遥感与GIS应用等领域的研究&#xff0c;发表多篇Sci论文、具有资深的技术底蕴和专业背景。 1、…

MapTracker:Tracking with Strided Memory Fusion for Consistent Vector HD Mapping

参考代码&#xff1a;MapTracker 动机与出发点 为了提升帧间检测的稳定性通常会添加时许信息&#xff0c;这个可以BEV特征处做时序融合&#xff0c;也可以是用当前帧query去cross-attn历史帧信息&#xff0c;则更多的时候是将之前帧信息与当前做融合或者cross-attn实现信息传…

SQL注入sqli_labs靶场第三题

?id1and 11 and 11和?id1and 11 and 11进行测试如果11页面显示正常和原页面一样&#xff0c;并且12页面报错或者页面部分数据显示不正常&#xff0c;那么可以确定此处为字符型注入。 根据报错信息判断为单引号带括号注入 联合查询&#xff1a; 猜解列名 ?id1) order by 3-…

SIC知识--(1):来龙去脉

一、碳化硅的起源 1891年&#xff0c;当时爱德华古德里奇艾奇逊在尝试制造人造金刚石的过程中意外发现了这一材料。艾奇逊将黏土&#xff08;铝硅酸盐&#xff09;与粉状焦炭&#xff08;碳&#xff09;混合后在电炉中加热&#xff0c;预期得到金刚石&#xff0c;却意外获得了一…