【MySQL】InnoDB存储引擎篇(二)索引结构

数据库的索引,相信大家都不陌生了。这篇文档我会从头到尾的把索引讲清楚,而不是死记硬背的八股模式。死记硬背固然可以应付面试,但其实不会对索引的理解很深刻。

我也是在深入学完索引的时候才感慨,还是加深对技术的理解,面试的时候才可以侃侃而谈,答出其他竞争者答不到的点,从而让面试官对你高看一眼(实话)。

下面我会从 B+树结构,聚簇索引,二级索引,联合索引的顺序展开。

1. B+树

我们先不直接讲 B+树 的定义,而是通过讲 InnoDB 的存储方式上理解其结构。

前文我们说 InnoDB 引擎存储数据的基本单位是 ,我们存储的一条一条记录都是存放到数据页里的。

那么 B+树 上的每个节点其实就是页,是一个一个数据页组成的一棵树。但不是所有的页都存放用户记录。

我们简单的建一个表,先不建索引。

CREATE TABLE cs_user (
  id INT PRIMARY KEY,
  age INT,
  name VARCHAR(50),
);

INSERT INTO cs_user (`id`, `age`, `name`) VALUES (1, 22, '张三');
INSERT INTO cs_user (`id`, `age`, `name`) VALUES (2, 25, '李四');

SELECT * FROM cs_user;
SELECT * FROM cs_user WHERE id = 1;

先抛出两个问题:

  • 创建了一张表并插入了两条记录,那么在我们不创建索引的情况下,进行查询操作的时候,数据库是如何快速找到指定的记录的呢?
  • InnoDB 引擎自动帮我们创建了一个聚簇索引(一会详细说),然后根据这个索引快速的定位。
  • 我们知道在每个页中,有页目录可以通过二分法快速定位到要找的那条记录。但是每个页固定为 16KB,数据库很庞大的话,那么必然会有很多个页,这样又该如何快速找到指定的页呢?
  • 这就要从 B+树 的结构说起了,B+树是一个多叉树,在 InnoDB 引擎中,只有最后一层也就是叶子节点才存放用户记录,上面的那些层包括根节点其实都相当于是一个目录页,只不过越靠近根节点,囊括的范围越广。就比如说我想查找一个学生的班级,那么我肯定先查他是哪个学校的,再查他是哪个年级的,最后再查他是哪个班级的,如下图所示:只有叶子节点才存储数据(即班级)

2. 聚簇索引

同理,B+树前几层都是目录页,用于更快速的定位查询记录所在的页,然后一层一层向下查询,直到找到记录所在的数据页,再根据上一篇文档讲的页目录,查询到具体的记录。

下面这个是一个聚簇索引构成的 B+树,叶子节点存的是完整的记录

这里引用下《MySQL是怎样运行的》图示,不知道 Infimum 和 Supremum 是啥的请看上一篇文档。

通过这张图片,可以看到:

  • 不同页之间按照主键的大小顺序排成一个双向链表
  • 不同记录之间按照主键的大小顺序排成一个单向链表
  • 非叶子节点(也就是目录页),第二行的值是这个目录项记录指向的那个页第一条记录的主键值。比如第二层的页30,1 -> 5 -> 12 -> 200,1是页10的第一个主键值,5是页28的主键值...,以此类推。
  • 非叶子节点,第三行的值是这个目录项记录指向的那个页的页号,图中显而易见。
  • 叶子节点(也就是数据页),第一行是主键值(id),第二行是年龄(age),第三行是名字(name),也就是说,聚簇索引的叶子节点包含一条记录完整的字段。

比如我执行这条语句:SELECT * FROM cs_user WHERE id = 8;

  • 首先从根节点开始,由于 1 < 8 < 320,所以去页30查询
  • 再查第二层目录,由于5 < 8 < 12,所以去页28查询
  • 最后查叶子节点,从页28 id = 5 的记录向后查,查到下一个就是 id = 8 的记录

3. 二级索引

上面说的是我们通过主键 id 来查询记录,现在假如我们多了一个需求,需要频繁通过 age 来筛选用户。

如果我们没有对 age 设置索引,那么数据库查询的时候,就会进行全表扫描,从第一个记录开始一个一个遍历,这种方式当数据量很大的时候是很费性能的。所以应对这种需求,我们一般会为 age 添加一个索引(二级索引)这样再通过 age 查询用户的时候,就可以用这个索引来查询用户记录,效率直接翻xxx倍。

还是上面那张表,这次我们为 年龄age 加一个索引

CREATE TABLE cs_user (
  id INT PRIMARY KEY,
  age INT,
  name VARCHAR(50)
  INDEX idx_age age;
);

INSERT INTO cs_user (`id`, `age`, `name`) VALUES (1, 22, '张三');
INSERT INTO cs_user (`id`, `age`, `name`) VALUES (2, 25, '李四');

SELECT * FROM cs_user;
SELECT * FROM cs_user WHERE id = 1;
SELECT * FROM cs_user WHERE age = 6;

二级索引整体结构上,和聚簇索引差不多,区别在于叶子节点上(数据页)。

  1. 原来聚簇索引中记录的所有字段(id, age, name),现在只有两个字段了(age, id)
  2. 第一行变成了 年龄age,而主键退居后方到了第二行。

也就是说,二级索引会针对定义了索引的这个字段,构造一棵新的 B+树,这棵树的叶子节点,其每个记录的第一行都是定义索引的这个字段二分法会根据这个索引字段的大小进行排序和查找。但是这个新的 B+树还是会携带上主键,这个主键的作用是为了回表(一会说)

比如我执行这条语句:SELECT * FROM cs_user WHERE age = 6;

  • 首先从根节点开始,由于 2 < 6 < 9,所以去页42查询
  • 再查第二层目录,由于5 < 6 < 7,所以去页36查询
  • 最后查叶子节点,从页36 age = 5 的记录向后查,查到下一个就是 age = 6 的记录

记录排列顺序

读者请注意看,叶子节点的 页35,当索引字段 age 相同时(都等于4),又该怎么排序呢?

当然不是随便排,第一行相同,那就比第二行主键值的大小,发现 4 < 10,那么 id = 10 的就排在 id = 4 的后面

  • 这样在我们插入 age = 4 时,主键值也是有序的,也是一种规范。不然会造成一种混乱,比如插入相同记录的时候,是该将 age = 4 的记录插入页34呢还是页35呢。
  • 除此之外,这样按一层一层的大小顺序排布,查询的时候也是有讲究的,下一章会具体讲解。

回表

我们现在知道,二级索引中是不存储完整记录的。那当我们想查询这个年龄对应用户的完整记录,又该如何操作呢。答案是从二级索引的B+树中拿到主键值,再通过这个主键值去聚簇索引中查询完整记录。

也就是说,通过二级索引查询完整记录,我们需要走两次B+树的查询流程。

  • 第一次走二级索引,查到 age = 6 对应的主键 id = 220
  • 第二次走聚簇索引,查到 id = 220 对应的完整记录 cs_user(220, 6, 'i')

当然,如果我们只需要查询 age (或者同时查询 主键值id 和 age)的话,那就不需要回表操作,因为二级索引的这棵B+树就包含了这两个字段。

4. 联合索引

假如傻x产品经理现在又多了一个需求,在筛选用户时,要通过用户的 name 和 age 一起查询。

我们现有的索引,聚簇索引(主键索引),age 的二级索引,都无法快速拿到这两个值。

  • 聚簇索引的叶子节点(数据页),是以主键 id 大小排序的,二分法也是用 id 值快速查询,无法用二分法快速通过 age 和 name 查询到指定记录。
  • age 的二级索引里面,又没有 name 字段,查询还得通过回表再查一次聚簇索引。

所以,我们可以根据 age 和 name 建立一个联合索引

CREATE TABLE cs_user (
  id INT PRIMARY KEY,
  age INT,
  name VARCHAR(50)
  INDEX idx_age age;
  INDEX idx_age_name (age, name);
);

INSERT INTO cs_user (`id`, `age`, `name`) VALUES (1, 22, '张三');
INSERT INTO cs_user (`id`, `age`, `name`) VALUES (2, 25, '李四');

SELECT * FROM cs_user;
SELECT * FROM cs_user WHERE id = 1;
SELECT * FROM cs_user WHERE age = 6;
SELECT * FROM cs_user WHERE age = 4 AND name = 'o';

根据这两个字段建立联合索引后,InnoDB 引擎会再建一棵 B+树,叶子节点的顺序为 age - name - id

也就是先按 age 的大小排序,再按照 name 的大小排序,最后按照 id 的大小排序。

比如我执行这条语句:SELECT * FROM cs_user WHERE age = 4 AND name = 'o';

  • 首先从根节点开始,由于 2 < 4 < 9,所以去页59查询
  • 再查第二层目录,由于2 < 4 <= 4,所以去页50查询
  • 最后查叶子节点,从页50 age = 2 的记录向后查,查到后两个就是 age = 4 的记录,发现 name != 'o'
  • 再查页55,从第一个记录开始查,发现第一个符合,再查第二个 name = 'u',不符合,停止遍历。

可以发现,这样按照一层一层顺序排布的时候,查询效率是最高的,如果杂乱无章的排列,那么就得遍历所有 age = 4 的记录,如果 age = 4 的记录很多的话,就会浪费大量时间。

5. 面试官提问

1. 请你讲一下 B+ 树的结构(优点),为什么 InnoDB 引擎要用 B+ 树而不用 B树(红黑树)等等?

八股内容就不写了,大家可以自行查看 小林、JavaGuide 等或者直接询问 AI

2. 请你说一下索引的分类,都有哪些索引,分别介绍下

3. 数据库中的 B+树 一般建议几层,这样大概能存多少数据量?

4. 具体业务中,什么场景需要建索引?

5. 讲一下回表。

后端技术学习 文章被收录于专栏

这个专栏我将对后端技术进行总结和复盘,以在字节输出文档的态度来要求自己。包括但不限于八股、算法、架构、系统设计、场景、智力题等。

全部评论

相关推荐

从大一开始就开始学习Java,一路走来真的不算容易,每次面试都被压力,不过这次终于达成了自己的一大心愿!时间线和面经:8.17-投递9.1-一面实习+项目拷打看门狗机制讲一下redis加锁解锁的本身操作是什么Lua脚本是干什么的udp和tcp讲一下流量控制讲一下令牌桶算法说一下大端和小端是什么线程和协程有什么区别怎么切换协程切换的时候具体做了什么对于程序来说,你刚才提到的保存和恢复现场,这个现场有哪些信息udp优势现在有一个客户端和服务端,要实现TCP的通信,我们的代码要怎么写服务器怎么感知有新的连接怎么处理多个客户端的请求连接TCP怎么处理粘包和分包现在有两个文件,然后每个文件都有一亿条URL,每个的长度都很长,要怎么快速查找这两个文件共有的URLHashmap底层说一下怎么尽量提升插入和查询的效率如果要查找快,查询快,还有解决非空的问题,怎么做LoadingCache了解吗手撕:堆排序9.4-二面部门的leader,超级压力面拷打实习+项目,被喷完全没东西类的加载到垃圾回收整个底层原理讲一遍类加载谁来执行类加载器是什么东西,和进程的关系Java虚拟机是什么东西,和进程的关系如果我们要执行hello&nbsp;world,那虚拟机干了什么呢谁把字节码翻译成机器码,操作时机是什么Java虚拟机是一个执行单元吗Java虚拟机和操作系统的关系到底什么,假如我是个完全不懂技术的人,举例说明让我明白一个操作系统有两个Java程序的话,有几个虚拟机有没有单独的JVM进程存在启动一个hello&nbsp;world编译的时候,有几个进程JVM什么时候启动比如执行一条Java命令的时候对应一个进程,然后这个JVM虚拟机到底是不是在这个进程里面,还是说要先启动一个JVM虚拟机的进程垃圾回收机制的时机能手动触发垃圾回收吗垃圾回收会抢占业务代码的CPU吗垃圾回收算法简单说说垃圾回收机制的stop&nbsp;the&nbsp;world存在于哪些时机垃圾回收中的计算Region的时候怎么和业务代码并行执行假如只有一个线程,怎么实现并行Java为什么要这么实现Java效率比C++慢很多,那为什么还要这样实现Java虚拟机到底是什么形式存在的说一下Java和C++的区别还有你对Java设计理念的理解无手撕面试结束的时候,我真的汗流浃背了,面试官还和我道歉,说他是故意压力面想看看我的反应的,还对我给予了高度评价:我当面试官这么多年,你是我见过最好的一个9.9-三面临时通知的加面,就问了三十分钟项目9.11-hr面问过往经历,未来计划,想从腾讯实习中得到什么?当场告知leader十分满意我,所以直接ochr面完一分钟官网流程变成录用评估中,30分钟后mt加微信告知offer正在审批9.15-offer这一次腾讯面试体验真的不错,每个面试官能感觉到专业能力很强,反馈很足,比起隔壁某节真是好太多以后就是鹅孝子了
三本咋了:当面试官这么多年你是我见过的最好的一个
你面试被问到过哪些不会的...
点赞 评论 收藏
分享
评论
1
4
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务