MySQL | 知识 | 从底层看清 InnoDB 数据结构

文章目录

  • 一、InnoDB 简介
    • InnoDB 行格式
      • COMPACT 行格式
      • CHAR(M) 列的存储格式
      • VARCHAR(M) 最多能存储的数据
      • 记录中的数据太多产生的溢出
      • 行溢出的临界点
  • 二、表空间文件的结构
  • 三、InnoDB 数据页结构
    • 页的概览
    • Infimum 和 Supremum
    • 使用Page Directory
    • 页的真实面貌
  • 四、B+ 树是如何进行查询的
    • 查询方式
    • 聚簇索引和二级索引
  • 五、总结

InnoDB 一个支持事务安全的存储引擎,同时也是 mysql 的默认存储引擎。本文主要从数据结构的角度,详细介绍 InnoDB 行记录格式和数据页的实现原理,从底层看清 InnoDB 存储引擎。从存储原理和读取原理分析结构。

一、InnoDB 简介

大家都知道 mysql 中数据是存储在物理磁盘上的,而真正的数据处理又是在内存中执行的。由于磁盘的读写速度非常慢,如果每次操作都对磁盘进行频繁读写的话,那么性能一定非常差。为了上述问题,InnoDB 将数据划分为若干页,以页作为磁盘与内存交互的基本单位,一般页的大小为 16KB。这样的话,一次性至少读取 1 页数据到内存中或者将 1 页数据写入磁盘。通过减少内存与磁盘的交互次数,从而提升性能。

其实,这本质上就是一种典型的缓存设计思想,一般缓存的设计基本都是从时间维度或者空间维度进行考量的:

  1. 时间维度:如果一条数据正在在被使用,那么在接下来一段时间内大概率还会再被使用。可以认为热点数据缓存都属于这种思路的实现。
  2. 空间维度:如果一条数据正在在被使用,那么存储在它附近的数据大概率也会很快被使用。InnoDB 的数据页和操作系统的页缓存则是这种思路的体现。

InnoDB 行格式

mysql 是以记录 (一行数据) 为单位向数据表中插入数据的,这些记录在磁盘上的存放方式称为行格式。mysql 支持 4 种不同类型的行格式:CompactRedundant(比较老,本文就不具体介绍了)、DynamicCompressed。我们可以在创建或修改表的语句中指定行格式:

CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称

比如,我们要创建一个行格式为 Compact,字符集为 ascii 的数据表 record_format_demo,sql 如下:

mysql> CREATE TABLE record_format_demo (c1 VARCHAR(10),c2 VARCHAR(10) NOT NULL,c3 CHAR(10),c4 VARCHAR(10)) CHARSET=ascii ROW_FORMAT=COMPACT;
Query OK, 0 rows affected (0.03 sec)

假设我们向 record_format_demo 表中插入了 2 行数据:

mysql> SELECT * FROM record_format_demo;
+------+-----+------+------+
| c1   | c2  | c3   | c4   |
+------+-----+------+------+
| aaaa | bbb | cc   | d    |
| eeee | fff | NULL | NULL |
+------+-----+------+------+
2 rows in set (0.00 sec)

COMPACT 行格式

在这里插入图片描述
记录的额外信息
记录的额外信息主要包含 3 类:变长字段长度列表、NULL 值列表和记录头信息。

变长字段长度列表
mysql 中支持一些变长数据类型(比如 VARCHAR(M)、TEXT 等),它们存储数据占用的存储空间不是固定的,而是会随着存储内容的变化而变化。为了准确描述这种数据,这种变长字段占用的存储空间要同时包含:

  1. 真正的数据内容
  2. 占用的字节数
    在 Compact 行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表,各变长字段数据占用的字节数按照列的顺序逆序存放
    我们以 record_format_demo 第一行数据为例。由于 c1、c2 和 c4 都是变成数据类型 (VARCHAR(10)), 因此要将这 3 列值得长度保存在记录的开头处。
    在这里插入图片描述

记录头信息
记录头信息是由固定的 5 个字节 (40 位) 组成, 不同的位代表不同的含义:
在这里插入图片描述

暂时不详细展开。

真实的数据
记录的真实数据除了包含各列具体的数据外,还会自动添加一些隐藏列数据。
列名是否必须占用空间描述 row_id 否 6 字节行 ID,唯一标识一条记录 transaction_id 是 6 字节事务 IDroll_pointer 是 7 字节回滚指针
实际上这几个列的真正名称其实是:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR,为了美观才写成了 row_id、transaction_id 和 roll_pointer。
只有当数据库没有定义主键或者唯一键时,隐藏列 row_id 才会存在,并且将其作为数据表主键。因为表 record_format_demo 并没有定义主键,所以 MySQL 服务器会为每条记录增加上述的 3 个列。现在看一下加上记录的真实数据的两个记录的数据结构:
在这里插入图片描述

CHAR(M) 列的存储格式

对于 CHAR(M) 类型的列来说,当列采用的是定长字符集时,该列占用的字节数不会被加到变长字段长度列表,而如果采用变长字符集时,该列占用的字节数也会被加到变长字段长度列表。另外有一点还需要注意,变长字符集的 CHAR(M) 类型的列要求至少占用 M 个字节,而 VARCHAR(M) 却没有这个要求。比方说对于使用 utf8 字符集的 CHAR(10) 的列来说,该列存储的数据字节长度的范围是 10~30 个字节,即使我们向该列中存储一个空字符串也会占用 10 个字节。

VARCHAR(M) 最多能存储的数据

MySQL 对一条记录占用的最大存储空间是有限制的,除了 BLOB 或者 TEXT 类型的列之外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过 65535 个字节。可以不严谨的认为,mysql 一行记录占用的存储空间不能超过 65535 个字节。这个 65535 个字节除了列本身的数据之外,还包括一些其他的数据(storage overhead),比如说我们为了存储一个 VARCHAR(M) 类型的列,其实需要占用 3 部分存储空间:

  1. 真实数据
  2. 真实数据占用字节的长度
  3. NULL 值标识,如果该列有 NOT NULL 属性则可以没有这部分存储空间

假设 varchar_size_demo 只有一个 VARCHAR 类型的字段,那么该字段最大占用的 65532 个字节。因为真实数据的长度可能占用 2 个字节,NULL 值标识需要占用 1 个字节。如果该 VARCHAR 类型的列没有 NOT NULL 属性,那最多只能存储 65532 个字节的数据。如果该列是 ascii 字符集,对应的最大字符数最大为 65532;如果是 utf8 字符集,则对应的最大字符数为 21844。

记录中的数据太多产生的溢出

我们以 ascii 字符集下的 varchar_size_demo 表为例,插入一条记录:

mysql> CREATE TABLE varchar_size_demo(c VARCHAR(65532)) CHARSET=ascii ROW_FORMAT=Compact;
Query OK, 0 rows affected (0.01 sec)mysql> INSERT INTO varchar_size_demo(c) VALUES(REPEAT('a', 65532));
Query OK, 1 row affected (0.00 sec)

mysql 中磁盘与内存交互的基本单位是页,一般为 16KB,16384 个字节,而一行记录最大可以占用 65535 个字节,这就造成了一页存不下一行数据的情况。在 Compact 和 Redundant 行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的一部分数据,把剩余的数据分散存储在几个其他的页中,然后记录的真实数据处用 20 个字节存储指向这些页的地址,从而可以找到剩余数据所在的页,如图所示:
在这里插入图片描述
这种在本记录的真实数据处只会存储该列的前 768 个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页中的情况就叫做行溢出,存储超出 768 字节的那些页面也被称为溢出页。

行溢出的临界点

MySQL 中规定一个页中至少存放两行记录。以上边的 varchar_size_demo 表为例,它只有一个列 c,我们往这个表中插入两条记录,每条记录最少插入多少字节的数据才会行溢出的现象呢?这得分析一下页中的空间都是如何利用的。

  1. 每个页除了存放我们的记录以外,也需要存储一些额外的信息,大概 132 个字节。
  2. 每个记录需要的额外信息是 27 字节。
    假设一个列中存储的数据字节数为 n,如要要保证该列不发生溢出,则需要满足:
    132 + 2×(27 + n) < 16384
    结果是 n < 8099。也就是说如果一个列中存储的数据小于 8099 个字节,那么该列就不会成为溢出列。如果表中有多个列,那么这个值更小。

二、表空间文件的结构

表空间由段(segment)、区(extent)、页(page)、行(row)组成,InnoDB存储引擎的逻辑存储结构大致如下图:
在这里插入图片描述
下面我们从下往上一个个看看。
1、行(row)
数据库表中的记录都是按行(row)进行存放的,每行记录根据不同的行格式,有不同的存储结构。
后面我们详细介绍 InnoDB 存储引擎的行格式,也是本文重点介绍的内容。
2、页(page)
记录是按照行来存储的,但是数据库的读取并不以「行」为单位,否则一次读取(也就是一次 I/O 操作)只能处理一行数据,效率会非常低。
因此,InnoDB 的数据是按「页」为单位来读写的,也就是说,当需要读一条记录的时候,并不是将这个行记录从磁盘读出来,而是以页为单位,将其整体读入内存。
默认每个页的大小为 16KB,也就是最多能保证 16KB 的连续存储空间。
页是 InnoDB 存储引擎磁盘管理的最小单元,意味着数据库每次读写都是以 16KB 为单位的,一次最少从磁盘中读取 16K 的内容到内存中,一次最少把内存中的 16K 内容刷新到磁盘中。
页的类型有很多,常见的有数据页、undo 日志页、溢出页等等。数据表中的行记录是用「数据页」来管理的,数据页的结构这里我就不讲细说了,之前文章有说过,感兴趣的可以去看这篇文章:
总之知道表中的记录存储在「数据页」里面就行。
3、区(extent)
我们知道 InnoDB 存储引擎是用 B+ 树来组织数据的。
B+ 树中每一层都是通过双向链表连接起来的,如果是以页为单位来分配存储空间,那么链表中相邻的两个页之间的物理位置并不是连续的,可能离得非常远,那么磁盘查询时就会有大量的随机I/O,随机 I/O 是非常慢的。
解决这个问题也很简单,就是让链表中相邻的页的物理位置也相邻,这样就可以使用顺序 I/O 了,那么在范围查询(扫描叶子节点)的时候性能就会很高。
那具体怎么解决呢?
在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区(extent)为单位分配。每个区的大小为 1MB,对于 16KB 的页来说,连续的 64 个页会被划为一个区,这样就使得链表中相邻的页的物理位置也相邻,就能使用顺序 I/O 了。
4、段(segment)
表空间是由各个段(segment)组成的,段是由多个区(extent)组成的。段一般分为数据段、索引段和回滚段等。

  • 索引段:存放 B + 树的非叶子节点的区的集合;
  • 数据段:存放 B + 树的叶子节点的区的集合;
  • 回滚段:存放的是回滚数据的区的集合,之前讲的时候就介绍到了 MVCC 利用了回滚段实现了多版本查询数据。

三、InnoDB 数据页结构

我们已经知道页是 InnoDB 管理存储空间的基本单位,一个页的大小一般是 16KB。InnoDB 为了不同的目的设计了许多不同类型的页,我们这里主要关注存储数据记录的页,官方称为索引页。由于还没介绍索引,暂且我们先称为数据页吧。

首先,我们需要知道,页(Pages)是 InnoDB 中管理数据的最小单元。Buffer Pool 中存的就是一页一页的数据。再比如,当我们要查询的数据不在 Buffer Pool 中时,InnoDB 会将记录所在的页整个加载到 Buffer Pool 中去;同样的,将 Buffer Pool 中的脏页刷入磁盘时,也是按照页为单位刷入磁盘的。

不了解 Buffer Pool 的、或者感兴趣的可以去搜索下;

页的概览

我们往 MySQL 插入的数据最终都是存在页中的。在 InnoDB 中的设计中,页与页之间是通过一个双向链表连接起来。
在这里插入图片描述
而存储在页中的一行一行的数据则是通过单链表连接起来的。
在这里插入图片描述
上图中的 User Records 的区域就是用来存储行数据的。那 InnoDB 为什么要这么设计?假设我们没有页这个概念,那么当我们查询时,成千上万的数据要如何做到快速的查询出结果?众所周知,MySQL 的性能是不错的,而如果没有页,我们剩下的只能是逐条逐条的遍历数据了。

那页是如何做到快速查询的呢?在当前页中,可以通过 User Records 中的连接每条记录的单链表来进行遍历,如果在当前页中没有找到,则可以通过下一页指针快速的跳到下一页进行查询。

Infimum 和 Supremum

有人可能会说了,你在 User Records 中还不是通过遍历来解决的,你就是简单的把数据分了个组而已。如果我的数据根本不在当前这个页中,那我难道还是得把之前的页中的每一条数据全部遍历完?这效率也太低了

当然,MySQL 也考虑到了这个问题,所以实际上在页中还存在一块区域叫做 The Infimum and Supremum Records ,代表了当前页中最大和最小的记录。
在这里插入图片描述
有了 Infimum Record 和 Supremum Record ,现在查询不需要将某一页的 User Records 全部遍历完,只需要将这两个记录和待查询的目标记录进行比较。比如我要查询的数据 id = 101 ,那很明显不在当前页。接下来就可以通过下一页指针跳到下页进行检索。

使用Page Directory

可能有人又会说了,你这 User Records 里不也全是单链表吗?即使我知道我要找的数据在当前页,那最坏的情况下,不还是得挨个挨个的遍历100次才能找到我要找的数据?你管这也叫效率高?

不得不说,这的确是个问题,不过是一个 MySQL 已经考虑到的问题。不错,挨个遍历确实效率很低。为了解决这个问题,MySQL 又在页中加入了另一个区域 Page Directory 。
在这里插入图片描述
顾名思义,Page Directory 是个目录,里面有很多个槽位(Slots),每一个槽位都指向了一条 User Records 中的记录。大家可以看到,每隔几条数据,就会创建一个槽位。其实我图中给出的数据是非常严格按照其设定来的,在一个完整的页中,每隔6条数据就会有一个 Slot。

Page Directory 的设计不知道有没有让你想起另一个数据结构——跳表,只不过这里只抽象了一层索引
MySQL 会在新增数据的时候就将对应的 Slot 创建好,有了 Page Directory ,就可以对一张页的数据进行粗略的二分查找。至于为什么是粗略,毕竟 Page Directory 中不是完整的数据,二分查找出来的结果只能是个大概的位置,找到了这个大概的位置之后,还需要回到 User Records 中继续的进行挨个遍历匹配。

不过这样的效率已经比我们刚开始聊的原始版本高了很多了。
在这里插入图片描述

页的真实面貌

如果我开篇就把页的各种组成部分,各种概念直接抛出来,首先我自己接受不了,这样显得很僵硬。其次,对页不熟悉的人应该是不太能理解页为什么要这么设计的。所以我按照查询一条数据的一套思路,把页的大致的面貌呈现给了大家。

实际上,页上还存储了很多其他的字段,也还有其他的区域,但是这些都不会影响到我们对页的理解。所以,在对页有了一个较为清晰的认知之后,我们就可以来看看真实的页到底长啥样了。
在这里插入图片描述
上图就是页的实际全部组成,除了我们之前提到过的,还多了一些之前没有聊过的,例如 File Header、Page Header、Free Space、File Tailer 。我们一个一个来看。

File Header
其实File Header 在上文已经聊过了,只是不叫这个名字。上面提到的上一页指针和下一页指针其实就是属于File Header的,除此之外还有很多其他的数据。
在这里插入图片描述
其实我比较抗拒把一堆参数列出来,告诉你这个大小多少,那个用来干嘛。对于我们需要详细了解页来说,其实暂时只需要知道两个就足够了,分别是:

  • FIL_PAGE_PREV
  • FIL_PAGE_NEXT

这两个变量就是上文提到过的上一页指针和下一页指针,说是指针,是为了方便大家理解,实际上是页在磁盘上的偏移量。

Page Directory(页目录)
我们已经知道,记录在页中按照主键大小正序串联成了一个单链表。如果我们要根据主键查找具体的某条记录应该怎么办,简单的方式是根据链表进行遍历。但是在数据量比较大的情况下,这种方式显然效率太差了。因此 mysql 使用了 Page Directory(页目录)来解决这个问题。Page Directory(页目录)大致的原理如下:

  1. 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。怎么划分先不关注。
  2. 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的 n_owned 属性表示该组内共有几条记录。
  3. 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页尾部的地方,这个地方就是所谓的 Page Directory。

mysql 规定对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1-8 条之间,剩下的分组中记录的条数范围只能在是 4-8 条之间。比方说现在的 page_demo 表中正常的记录共有 18 条,InnoDB 会把它们分成 5 组,第一组中只有一个最小记录,如下所示:
在这里插入图片描述
页目录创建的过程如下:

  1. 将所有的记录划分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录;
  2. 每个记录组的最后一条记录就是组内最大的那条记录,并且最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段(上图中粉红色字段)
  3. 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录。

从图可以看到,页目录就是由多个槽组成的,槽相当于分组记录的索引。然后,因为记录是按照「主键值」从小到大排序的,所以我们通过槽查找记录时,可以使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到对应的记录,无需从最小记录开始遍历整个页中的记录链表。
以上面那张图举个例子,5 个槽的编号分别为 0,1,2,3,4,我想查找主键为 11 的用户记录:

  • 先二分得出槽中间位是 (0+4)/2=2 ,2号槽里最大的记录为 8。因为 11 > 8,所以需要从 2 号槽后继续搜索记录;
  • 再使用二分搜索出 2 号和 4 槽的中间位是 (2+4)/2= 3,3 号槽里最大的记录为 12。因为 11 < 12,所以主键为 11 的记录在 3 号槽里;
  • 这里有个问题,「**槽对应的值都是这个组的主键最大的记录,如何找到组里最小的记录」?**比如槽 3 对应最大主键是 12 的记录,那如何找到最小记录 9。解决办法是:通过槽 3 找到 槽 2 对应的记录,也就是主键为 8 的记录。主键为 8 的记录的下一条记录就是槽 3 当中主键最小的 9 记录,然后开始向下搜索 2 次,定位到主键为 11 的记录,取出该条记录的信息即为我们想要查找的内容。

看到第三步的时候,可能有的同学会疑问,如果某个槽内的记录很多,然后因为记录都是单向链表串起来的,那这样在槽内查找某个记录的时间复杂度不就是 O(n) 了吗?
这点不用担心,InnoDB 对每个分组中的记录条数都是有规定的,槽内的记录就只有几条:

  • 第一个分组中的记录只能有 1 条记录;
  • 最后一个分组中的记录条数范围只能在 1-8 条之间;
  • 剩下的分组中记录条数范围只能在 4-8 条之间。

通过 Page Directory 在一个数据页中查找指定主键值的记录的过程分为两步:

  1. 通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。
  2. 通过记录的 next_record 属性遍历该槽所在的组中的各个记录。

Page Header(页面头部)
Page Header 专门用来存储数据页相关的各种状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等。固定占用 56 个字节,各部分字节属性含义如下:
在这里插入图片描述
这里全列出来是因为了解这些参数的含义和为什么要设置参数,能够更好的帮助我们了解页的原理和构造,具体的看图说话就行。

这里也很想吐槽,太多博客都写的太僵硬,比如参数 PAGE_HEAP_TOP ,这里的 HEAP 很多博客都直接叫堆。这就跟你给Init写注释叫初始化一样,还不如不写。实际上你去研究一下就会知道,这里的堆实际上就是指User Records。

里面有个两个参数可能会有点混淆,分别是PAGE_N_HEAP和PAGE_N_RECS ,都是当前 User Records 中记录的数量,唯一的不同在于,PAGE_N_HEAP 中是包含了被标记为删除的记录的, 而 PAGE_N_RECS 中就是实际上我们能够查询到的所有数据。

Infimum & Supremum Records
上文中提到,Infimum & Supremum Records会记录当前页最大最小记录。实际上不准确,更准确的描述是最小记录和最大纪录的开区间。因为实际上 Infimum Records 会比当前页中的最小值还要小,而 Supremum Records 会比当前页中的最大值要大。

User Records
User Records 可以说是我们平时接触的最多的部分了,毕竟我们的数据最终都在这。页被初始化之后,User Records 中是没有数据的,随着系统运行,数据产生,User Records 中的数据会不断的膨胀,相应的 Free Space 空间会慢慢的变小。

关于 User Records 中的概念,之前已经聊过了。这里只聊我认为很关键的一点,那就是顺序。

我们知道,在聚簇索引中,Key 实际上会按照 Primary Key 的顺序来进行排列。那在 User Records 中也会这样吗?我们插入一条新的数据到 User Records 中时,是否也会按照 Primary Key 的顺序来对已有的数据重排序?

答案是不会,因为这样会拉低 MySQL 处理的效率。

User Records 中的数据是由单链表指针的指向来保证的,也就是说,行数据实际在磁盘上的表现,是按照插入顺序来排队的,先到的数据在前面,后来的数据在后面。只不过通过 User Records 中的行数据之间的单链表形成了一个按照 Primary Key排列的顺序。

用图来表示,大概如下:
在这里插入图片描述
Free Space
这块其实变相的在其他的模块中讨论了,最初 User Records 是完全空的,当有新数据进来时,会来 Free Space 中申请空间,当 Free Space 没空间了,则说明需要申请新的页了,其他没什么特别之处。

Page Directory
这跟上文讨论的没什么出入,就直接跳过了。

File Trailer
这块主要是为了防止页在刷入磁盘的过程中,由于极端的意外情况(网络问题、火灾、自然灾害)导致失败,而造成数据不一致的情况,也就是说形成了脏页。

四、B+ 树是如何进行查询的

查询方式

上面我们都是在说一个数据页中的记录检索,因为一个数据页中的记录是有限的,且主键值是有序的,所以通过对所有记录进行分组,然后将组号(槽号)存储到页目录,使其起到索引作用,通过二分查找的方法快速检索到记录在哪个分组,来降低检索的时间复杂度。
但是,当我们需要存储大量的记录时,就需要多个数据页,这时我们就需要考虑如何建立合适的索引,才能方便定位记录所在的页。
为了解决这个问题,InnoDB 采用了 B+ 树作为索引。磁盘的 I/O 操作次数对索引的使用效率至关重要,因此在构造索引的时候,我们更倾向于采用“矮胖”的 B+ 树数据结构,这样所需要进行的磁盘 I/O 次数更少,而且 B+ 树 更适合进行关键字的范围查询。
InnoDB 里的 B+ 树中的每个节点都是一个数据页,结构示意图如下:
在这里插入图片描述
通过上图,我们看出 B+ 树的特点:

  • 只有叶子节点(最底层的节点)才存放了数据,非叶子节点(其他上层节)仅用来存放目录项作为索引。
  • 非叶子节点分为不同层次,通过分层来降低每一层的搜索量;
  • 所有节点按照索引键大小排序,构成一个双向链表,便于范围查询;

我们再看看 B+ 树如何实现快速查找主键为 6 的记录,以上图为例子:

  • 从根节点开始,通过二分法快速定位到符合页内范围包含查询值的页,因为查询的主键值为 6,在[1, 7)范围之间,所以到页 30 中查找更详细的目录项;
  • 在非叶子节点(页30)中,继续通过二分法快速定位到符合页内范围包含查询值的页,主键值大于 5,所以就到叶子节点(页16)查找记录;
  • 接着,在叶子节点(页16)中,通过槽查找记录时,使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到主键为 6 的记录。

可以看到,在定位记录所在哪一个页时,也是通过二分法快速定位到包含该记录的页。定位到该页后,又会在该页内进行二分法快速定位记录所在的分组(槽号),最后在分组内进行遍历查找。

聚簇索引和二级索引

另外,索引又可以分成聚簇索引和非聚簇索引(二级索引),它们区别就在于叶子节点存放的是什么数据:

  • 聚簇索引的叶子节点存放的是实际数据,所有完整的用户记录都存放在聚簇索引的叶子节点;
  • 二级索引的叶子节点存放的是主键值,而不是实际数据。

因为表的数据都是存放在聚簇索引的叶子节点里,所以 InnoDB 存储引擎一定会为表创建一个聚簇索引,且由于数据在物理上只会保存一份,所以聚簇索引只能有一个。
InnoDB 在创建聚簇索引时,会根据不同的场景选择不同的列作为索引:

  • 如果有主键,默认会使用主键作为聚簇索引的索引键;
  • 如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键;
  • 在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键;

一张表只能有一个聚簇索引,那为了实现非主键字段的快速搜索,就引出了二级索引(非聚簇索引/辅助索引),它也是利用了 B+ 树的数据结构,但是二级索引的叶子节点存放的是主键值,不是实际数据。
二级索引的 B+ 树如下图,数据部分为主键值:
在这里插入图片描述
因此,如果某个查询语句使用了二级索引,但是查询的数据不是主键值,这时在二级索引找到主键值后,需要去聚簇索引中获得数据行,这个过程就叫作「回表」,也就是说要查两个 B+ 树才能查到数据。不过,当查询的数据是主键值时,因为只在二级索引就能查询到,不用再去聚簇索引查,这个过程就叫作「索引覆盖」,也就是只需要查一个 B+ 树就能找到数据。

五、总结

InnoDB 的数据是按「数据页」为单位来读写的,默认数据页大小为 16 KB。每个数据页之间通过双向链表的形式组织起来,物理上不连续,但是逻辑上连续。
数据页内包含用户记录,每个记录之间用单向链表的方式组织起来,为了加快在数据页内高效查询记录,设计了一个页目录,页目录存储各个槽(分组),且主键值是有序的,于是可以通过二分查找法的方式进行检索从而提高效率。
为了高效查询记录所在的数据页,InnoDB 采用 b+ 树作为索引,每个节点都是一个数据页。
如果叶子节点存储的是实际数据的就是聚簇索引,一个表只能有一个聚簇索引;如果叶子节点存储的不是实际数据,而是主键值则就是二级索引,一个表中可以有多个二级索引。
在使用二级索引进行查找数据时,如果查询的数据能在二级索引找到,那么就是「索引覆盖」操作,如果查询的数据不在二级索引里,就需要先在二级索引找到主键值,需要去聚簇索引中获得数据行,这个过程就叫作「回表」。

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

基于SpringBoot+Vue的房屋租赁平台

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于JavaSpringBootVueMySQL的…

如何使用ssm实现企业人事管理系统+vue

TOC ssm628企业人事管理系统vue 研究背景 自计算机发展以来给人们的生活带来了改变。第一代计算机为1946年美国设计&#xff0c;最开始用于复杂的科学计算&#xff0c;占地面积、开机时间要求都非常高&#xff0c;经过数十几的改变计算机技术才发展到今天。现如今已是电子时…

echarts map地图动态下钻,自定义标注,自定义tooltip弹窗【完整demo版本】

在数据可视化中&#xff0c;地图是很重要的一个环节&#xff0c;很多时候需要展现的不仅是国家地图&#xff0c;还需要能从国家进入到省市。这个逐级进入的过程就是我们今天说的地图下钻。 地图下钻看起来很屌、很高大上&#xff0c;但是仔细琢磨一下&#xff0c;技术实现上真的…

爵士编曲:爵士鼓编写 爵士鼓笔记 底鼓和军鼓 闭镲和开镲 嗵鼓

底鼓和军鼓 底鼓通常是动的音色&#xff0c;军鼓通常是大的音色。 “动”和“大”构成基础节奏。“动大”听着不够有连接性&#xff0c;所以可以加入镲片&#xff01; 开镲 直接鼓棒敲击是开镲音色 闭镲 当脚踩下踏板&#xff0c;2个镲片合并&#xff0c;然后用鼓棒敲击&am…

R语言NHANES数据分析(2)

#交互分析 library(jstable) help(package"nhanesA") help(package"jstable") str(data_c) svy_design$variables$DMDCITZN<-as.factor(svy_design$variables$DMDCITZN) TableSubgroupMultiGLM(BPQ020~BMXBMI,datasvy_design,var_subgroups c("DM…

【AIGC】Kolors:快手开源的文生图大模型

GitHub&#xff1a;GitHub - Kwai-Kolors/Kolors: Kolors Team 论文&#xff1a;Kolors/imgs/Kolors_paper.pdf at master Kwai-Kolors/Kolors GitHub comfyui&#xff1a;GitHub - comfyanonymous/ComfyUI: The most powerful and modular diffusion model GUI, api and b…

【UEFI基础】BIOS下的启动项管理

启动管理 启动管理&#xff08;Boot Manager&#xff09;是UEFI BIOS中重要的一部分&#xff0c;它通过一系列的变量来确定启动策略&#xff0c;包括&#xff1a; 执行启动还是恢复操作启动顺序是如何 本文会介绍下面的内容&#xff1a; 与启动管理相关的变量启动或恢复的流…

关于STM32项目面试题02:ADC与DAC篇(输入部分NTC、AV:0-5V、AI:4-20mA和DAC的两个引脚)

博客的风格是&#xff1a;答案一定不能在问题的后面&#xff0c;要自己想、自己背&#xff1b;回答都是最精简、最精简、最精简&#xff0c;可能就几个字&#xff0c;你要自己自信的展开。 面试官01&#xff1a;什么是模数转换/ADC&#xff1f;说说模数转换的流程&#xff1f; …

『功能项目』靠近Npc显示可对话标识【60】

我们打开上一篇59窗口可拖拽脚本的项目&#xff0c; 本章要做的事情是在资源加载时加载Npc01并且实现主角靠近Npc01时&#xff0c;显示可对话标识&#xff0c;当按键盘G键时弹出对话内容 首先在资源场景中加载一个Npc模型 导入&#xff08;除脚本&#xff09;外的资源 在资源文…

PAT甲级-1055 The World‘s Richest

题目 题目大意 输入给出富人的总数以及富人的姓名、年龄、财富&#xff0c;接下来的k行给出需要排序的个数&#xff0c;每个排序要求输出m个富人&#xff0c;并且限制了年龄段&#xff0c;[Amin, Amax]。要求输出所有的排序。如果满足年龄段的人数为0&#xff0c;就输出None。…

Windows目录监控部署

1.前提 Cell_Directory_Monitoring.bat脚本用到的du命令,请协调Windows系统管理员提供。 下述du命令部署配置方式仅供参考,如要部署,请协调Windows系统管理员协助确认其不会对系统造成异常。 1.1.du.exe部署 1.将x32位du.exe文件放入如下目录 目录:C:\Windows\System3…

如何在win10Docker安装Mysql数据库?

1.拉取镜像 docker pull mysql 2.查看镜像 使用以下命令来查看是否已安装了 mysql镜像。 3.运行镜像 命令&#xff1a; docker run -p 3306:3306 --name mysql --restartalways --privilegedtrue \ -v /usr/local/mysql/log:/var/log/mysql \ -v /usr/local/mysql/data:/var…

机器学习-点击率预估-论文速读-20240916

1. [经典文章] 特征交叉: Factorization Machines, ICDM, 2010 分解机&#xff08;Factorization Machines&#xff09; 摘要 本文介绍了一种新的模型类——分解机&#xff08;FM&#xff09;&#xff0c;它结合了支持向量机&#xff08;SVM&#xff09;和分解模型的优点。与…

Linux下的CAN通讯

CAN总线 CAN总线简介 CAN&#xff08;Controller Area Network&#xff09;总线是一种多主从式 <font color red>异步半双工串行 </font> 通信总线&#xff0c;它最早由Bosch公司开发&#xff0c;用于汽车电子系统。CAN总线具有以下特点&#xff1a; 多主从式&a…

解决使用阿里云DataV Geo在线地图路径访问403问题

文章目录 1. DataV Geo在线地图路径访问403问题2. 解决方法3. 重启生效 1. DataV Geo在线地图路径访问403问题 最近在写一个省市下钻的demo&#xff0c;用到的是 阿里云DataV Geo在线地图 去动态获取GeoJSON 省市的数据&#xff0c;如下代码 axios.get("https://geo.dat…

Golang | Leetcode Golang题解之第414题第三大的数

题目&#xff1a; 题解&#xff1a; func thirdMax(nums []int) int {var a, b, c *intfor _, num : range nums {num : numif a nil || num > *a {a, b, c &num, a, b} else if *a > num && (b nil || num > *b) {b, c &num, b} else if b ! ni…

马匹行为识别系统源码分享

马匹行为识别检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…

C语言程序设计(进阶)

行到水穷处&#xff0c;坐看云起时。 中秋快乐呀&#xff01; 数据在内存中的存储 1.数据类型的介绍 &#xff08;1&#xff09;基本的内置类型&#xff1a; char //字符数据类型 short //短整型 int //整型 long //长整型 …

说一说Zookeeper的应用场景及其原理

一 ZooKeeper简介 ZooKeeper是一个分布式的&#xff0c;开放源码的分布式应用程序协调服务&#xff0c;是Google的Chubby一个开源的实现&#xff0c;是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件&#xff0c;提供的功能包括&#xff1a;配置维护、域名…