【CMU15-445数据库】bustub Project #2:B+ Tree(上)

news2024/11/18 9:34:32

(最近两个月学校项目有亿点忙,鸽得有点久,先来把 Project 2 补上)

本节实验文档地址:Project #2 - B+Tree

Project 2 要实现的是数据结构课上都会讲的一个经典结构 B+ 树,但是相信大多数的同学(包括博主)当时都没有自己动手实现过它,本节就是一个很好的锻炼机会。

本节内容会大量使用到 Project 1 实现的 BufferPoolManager(当然也包含了其内部用到的 ExtendibleHashTable 和 LRUKReplacer),所以需要完成前置内容(博主也比较建议这样做,否则直接上手本节可能不好理解对 Page 的 Fetch 和 Unpin 操作)。

由于代码量较多,打算拆成上下两篇写完,本篇介绍用到的数据结构和 B+ 树的查找和插入实现,下一篇讲迭代器,删除和并发控制。

关于 B+ 树的文字介绍就不赘述了,查阅资料过程中发现维基百科的 B+ 树词条的算法描述不够具体,推荐一个有比较具体的例子的博客:
B树和B+树的插入、删除图文详解
(同时不建议参考那些插入和删除分 N 多种具体情况讨论的介绍)

数据结构

B+ 树中有内部节点和叶节点两种结构,它们存储的数据格式和内容不同。bustub 为我们设计好了下面这三个类:

  • 节点基类 BPlusTreePage
    在这里插入图片描述
    内部节点和叶节点的基类,包含了节点类型、当前容量、最大/最小容量、ID、父节点 ID 信息,从类结构上可以看做是两种节点的头信息。按照函数字面意思将其实现即可。可以规定 parent_page_id_INVALID_PAGE_ID 表示根节点

  • 内部节点 BPlusTreeInternalPage<KeyType, ValueType, KeyComparator>

在这里插入图片描述
在这里插入图片描述
内部节点,首先看用到的三个泛型 KeyType, ValueType, KeyComparatorKeyType 不一定直接可用大于小于号比较,所以引入了 KeyComparator,从 cpp 文件中的实例化可以看出用的是 GenericKeyGenericComparator,查看二者源码可以得到以下信息:

  • GenericKey 可以调用 ToString() 函数得到其 int64 表示,然后用 %ld 格式符打印。这对我们后面调试时非常重要。
  • GenericComparator 的比较规则是:左边小于右边时,返回 -1;左边大于右边时,返回 1;相等返回 0。

ValueType 代表的是指向子页面的指针,从实例化可以看出实际只用了 page_id_t,也就是 int。

数据存储上,其理论结构应为 <指针,键,指针,键…,键,指针>,为方便存储,实际上在头部多补了一个无效键,从而可以用一个 pair 的数组存储:

#define MappingType std::pair<KeyType, ValueType>
...
class BPlusTreeInternalPage : public BPlusTreePage {
...
private:
  // Flexible array member for page data.
  MappingType array_[1];
}

array_[1] 等价于一个指针,按照一般习惯应该在构造函数中为其 new 出一片大小为 max_size_ 的空间,但实际上不需要这样做,因为:

Each B+Tree leaf/internal page corresponds to the content (i.e., the data_ part) of a memory page fetched by buffer pool. So every time you try to read or write a leaf/internal page, you need to first fetch the page from buffer pool using its unique page_id, then reinterpret cast to either a leaf or an internal page, and unpin the page after any writing or reading operations.

简单翻译一下就是 内部节点和叶节点对象都不是直接创建出来,而是由一个 Buffer Pool 管理的 Page 的 data 部分类型转化而来(所以要用到很少用很暴力的 reinterpret_cast。所以,节点对象使用的是预先分配好的固定空间,array_ 可以控制从该位置开始到 Page 的 data 结束为止的这一段空间。因此,节点对象的生命周期也不是由 new 和 delete,而是由我们上节实现的 BufferPoolManager 管理:取一个页面,用 FetchPage;使用结束归还一个页面,用 UnpinPage。同时也就能理解 BPlusTreePagepage_id_ 成员的另一个含义:它不仅是 B+ 树中节点的编号,同时也是这个节点使用的 Page 在 BufferPool 中的编号

  • 叶节点 BPlusTreeLeafPage<KeyType, ValueType, KeyComparator>

在这里插入图片描述
数据存储上,叶节点也是一个 键+值 的数组,但不像内部节点那样第一个键无效。值的类型实际用的也只有一种:RID。这个和我们本节的内容关系不大,大致知道它是代表数据实际存放的位置即可。

BPlusTree 类代表整个 B+ 树:

在这里插入图片描述
其主要成员有:buffer_pool_manager_,由外部传入;root_page_id,表示根节点 ID;comparator_KeyComparator 类型的对象,用于键的大小比较;leaf_max_size_internal_max_size_,表示叶节点和内部节点的最大容量。我们需要实现 B+ 树的四个功能:查找,插入,删除和迭代器。

Checkpoint 1:查找,插入和删除

实验非常贴心地将所有内容分为了两个 checkpoint,其中 checkpoint 1 要实现查找,插入和删除功能,checkpoint 2 要实现迭代器和并发控制,Autograder 上也对应有两个提交位置。下面放出的代码都只通过 checkpoint 1,没有考虑加锁,这样能更专注于讲解其本身的逻辑。本篇先讲查找和插入。

查找(GetValue)

给定一个键 x x x,查找其是否在 B+ 树中存在。实现逻辑是先找到键可能在的叶节点,然后扫描一遍叶节点的内容确定是否存在,其中重点是前者。编写一个函数 GetLeafPage,根据 B+ 树的规则,应该从根节点开始,每次在内部节点中找到 k i < x < k i + 1 k_i < x < k_i+1 ki<x<ki+1 的位置,然后沿着 v i v_i vi 指针继续向下,直到达到叶节点。函数实现如下:

在这里插入图片描述

Tips:循环时找内部节点中第一个比 x x x 大的键,取其左侧的值即可( k [ 0 ] k[0] k[0]无效),而这样不能探测到 x x x 比所有 k k k 都大的情况,所以要将 next_page_id 初始化为最右侧的键

在此基础上,GetValue 的实现就很简单了:

在这里插入图片描述

插入(Insert)

热身完毕,下面进入本节第一个难点,插入的实现。B+ 树的插入流程为:

  1. 如果是空树,创建一个叶节点作为根。注意涉及 root_page_id_ 更新时都要调用一次 UpdateRootPageId,如果是第一次创建传 1 作为参数,更新不用,以下不再复述
  2. 从根节点向下查找到键值应该所在的叶节点。文档说明了不支持重复键,所以先扫描一遍叶节点,如果发现键存在则直接返回 false
  3. 如果叶节点 插入后 达到了 max_size,则要进行分裂(split),创建一个新的叶节点,将原节点的一半内容拷贝到新节点,分裂点的键插入父节点,该键对应的值指向新的叶节点。(如果父节点不存在,说明是第一个叶节点兼根节点,需要创建一个新的根,这种情况和 4 的建根可以合并处理)
  4. 如果父节点(内部节点)插入前 达到了 max_size,也要递归进行分裂并向上插入,此时还要调整原节点的一半子节点的 parent_id_ 指针指向新的内部节点。如果根节点满了,则要创建一个新的根节点,使得 B+ 树长高一层。

Tips:特别注意这里叶节点和内部节点的判断条件是不同的,摘一段文档原文:
You should correctly perform splits if insertion triggers the splitting condition (number of key/value pairs AFTER insertion equals to max_size for leaf nodes, number of children BEFORE insertion equals to max_size for internal nodes).

第 1、2 步代码:

在这里插入图片描述
第 3 步,未溢出情况,插入的具体逻辑可以放到 LeafPage 类中做,所以添加一个 Insert 函数,找到插入位置,将所有后面的键值对后移一位,再设置。由于 array_ 是有序的,如果还想提高效率,可以把找插入位置用二分搜索实现。

在这里插入图片描述

Tips:comparator_ 也要作为参数传入 Insert,否则 LeafPage 中无法进行键的比较,也就无法查找

在这里插入图片描述
叶节点溢出情况,注意处理好 next_page_id_。移动一半数据的逻辑也可以放到 LeafPage 类中,添加一个 MoveDataTo 函数:

在这里插入图片描述
在这里插入图片描述

Tips:MoveDataTo 不用真的对原叶节点后一半数据进行“抹除”,修改 size 即可,以后的新数据自然会覆盖掉这些数据。

真正的难点来了:如何处理向父节点插入、同时处理父节点可能继续分裂的递归逻辑。需要想清楚的是:在两次递归之间,需要传递的数据是什么?我的设计是,传递两个子节点对象和分裂点的键。前者是为了获取到其父节点,也可以对其本身的父节点指针进行更新,后者是要插入父节点的键。进一步思考,在第一轮,传递的子节点对象是叶节点,而后面每轮是内部节点,看起来不统一,但实际上我们需要这两个子节点只涉及到 page_id 的父子指针的更改,所以,传递的形式应设计为基类指针 BPlusTreePage *,就可以兼顾这两种情况。

这里我用一个 while(true) 循环实现,写成函数递归调用当然也可以。三个传递数据分别命名为 old_tree_pagenew_tree_pagesplit_key

第一轮初始化和到达根节点的处理。正因为用的是 BPlusTreePage *,所以可以兼顾 3 和 4,即上一层是叶节点和内部节点两种情况的建根。

在这里插入图片描述
未到达根节点,则在父节点进行插入。这里类似地我在 InternalPage 中也添加了一个 Insert 函数,但要注意逻辑上有一丁点不同,就是查找插入位置要从 1 开始

在这里插入图片描述
如果父节点也溢出,创建新的内部节点并移动一半数据。这里涉及到子节点的指针修改,所以直接把逻辑写在这里了。最后将三个传递数据更新,准备做下一轮处理。

在这里插入图片描述
细心的读者可能注意到上面达到跳出循环条件后没有 return true 而是写了 break,这是因为在最后一轮循环结束后还要统一做一件事情:释放最后两个页面。

在这里插入图片描述
如果你做完后本地测试和 AutoGrader 其它测试都能通过,只有 ScaleTest 报错 SIGSEGV,InternalPage 或 LeafPage 的函数(比如 GetSize())访问了空地址,则很可能是 Insert 函数中没有把所有 Fetch 的 Page 最后 Unpin 掉,导致其一直占着 BufferPoolManager 的空间,最终空间耗尽无法取到新页面,FetchPage 返回 nullptr。检查也很简单,改一下 BufferPoolManagerInstance 的代码,例如每次 Fetch 和 Unpin 时打印一个信息,看一下是不是所有的页面都被释放了(0 号页面不被释放是正常的)。

Debug 方法

这里我要吹爆 bustub 的开发组,他们提供了一个非常好用的工具 b_plus_tree_printer,可视化展现树的结构,帮助检查你的实现效果是否正确。

在这里插入图片描述
更感人的是他们还提供了一个打印正确实现的 B+ 树的在线版本,可以与自己本地的效果对比(泪目)

在这里插入图片描述
本篇内容到此结束,下一篇继续讲迭代器,删除和并发控制的实现

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

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

相关文章

vue中,给一个URL地址,利用FileSaver.js插件下载文件到本地

①首先下载 FileSaver.js 插件 npm install file-saver --save ②在需要的.vue页面引入 import { saveAs } from file-saver 在HTML中引入 <script src"https://cdn.bootcdn.net/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script> //Fil…

k8s 安装dashboard

前言 上一篇中将k8s简单部署安装上了&#xff0c;这篇接着安装下dashboard。 具体步骤 下载yaml文件 wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.5.0/aio/deploy/recommended.yaml注意&#xff1a;这里使用的版本是v2.5.0&#xff0c;这个要和k8s的版…

褪去大厂光环下的功能测试,出去面试自动化居然一问三不知......不淘汰你淘汰谁呢

在一家公司待久了技术能力反而变弱了&#xff0c;原来的许多知识都会慢慢遗忘&#xff0c;这种情况并不少见。 一个京东员工发帖吐槽&#xff1a;感觉在大厂快待废了&#xff0c;出去面试问自己接口环境搭建、pytest测试框架&#xff0c;自己做点工太久都忘记了。平时用的时候…

CCF-CSP真题《202212-2 训练计划》思路+python题解

想查看其他题的真题及题解的同学可以前往查看&#xff1a;CCF-CSP真题附题解大全 试题编号&#xff1a;202212-2试题名称&#xff1a;训练计划时间限制&#xff1a;1.0s内存限制&#xff1a;512.0MB问题描述&#xff1a; 问题背景 西西艾弗岛荒野求生大赛还有 n 天开幕&#xf…

总结Anisble中的任务执行控制并练习

文章目录一、循环1.简单循环2.循环散列或字典列表二、条件三、触发器四、处理失败任务1.ignore_errors2.force_handlers3.changed_when4.failed_when5.block五、 练习建立大小为1500M名为/dev/sdb1的设备利用ansible循环安装且开启vsftpd&#xff0c;apache&#xff0c;dns&…

java8新特性【2023】

Lambda表达式 新的一套语法规则 是一个匿名函数 Testpublic void test1(){Runnable r1 new Runnable(){Overridepublic void run() {System.out.println("线程A");}};r1.run();System.out.println("");Runnable r2 () -> System.out.println("…

linux 安装rabbitmq 文档

1、下载rabbitMQ https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.9.15对应的erlang https://packagecloud.io/app/rabbitmq/erlang/search?distel%2F7https://packagecloud.io/rabbitmq/erlang/packages/el/7/erlang-23.3.4.11-1.el7.x86_64.rpm?distro_ver…

Mysql | Error Code: 1153 - Got a packet bigger than ‘max_allowed_packet‘ bytes

描述 在执行sql语句插入的时候&#xff0c;报出了Error Code: 1153 - Got a packet bigger than ‘max_allowed_packet’ bytes 错误 解决方法 &#x1f6a9; 临时调整&#xff0c;重启后失效 1️⃣ 查询默认的max_allowed_packet值大小 执行语句&#xff1a; SHOW VARIABLE…

[Android Studio] Android Studio Virtual Device虚拟机的功能试用

&#x1f7e7;&#x1f7e8;&#x1f7e9;&#x1f7e6;&#x1f7ea; Android Debug&#x1f7e7;&#x1f7e8;&#x1f7e9;&#x1f7e6;&#x1f7ea; Topic 发布安卓学习过程中遇到问题解决过程&#xff0c;希望我的解决方案可以对小伙伴们有帮助。 &#x1f680;write…

JS:构造函数和原型

目录 1.构造函数和原型 1.1 概述 1.2 构造函数 1.2.1创建 1.2.2 静态成员和实例成员 1.3 构造函数原型 prototype 1.4 对象原型_proto_ 1.5 constructor 构造函数 1.6 构造函数、实例、原型对象的关系 1.7 原型链 2. 继承 2.1 call() 1.构造函数和原型 1.1 概述 在…

OpenStack

名词解释RAM 身份验证管理SDN 软件定义网络虚拟化为什么openstack清一色的KVM&#xff1f;KVM属于redhat&#xff0c;开源可定制nentron实现SDNSDN实现了东西向流量管理&#xff08;VPC与VPC的流量管理&#xff09;SDNVXLan实现隧道网络&#xff08;对等连接&#xff09;SDN在公…

【郭东白架构课 模块一:生存法则】04|法则二:架构师为什么要学习马斯洛的需求理论?

你好&#xff0c;我是郭东白&#xff0c;今天我们来聊聊架构师的第二个生存法则&#xff1a;架构活动需要尊重和顺应人性。 自从学习计算机专业的那一天起&#xff0c;我们似乎就走入了一个简单直接的机器世界&#xff0c;一个完全靠逻辑和数字主宰的世界。于是我们总不自觉地认…

如何将数据库结构导入到word

在navicat执行查询语句 SELECT COLUMN_NAME 备注, COLUMN_COMMENT 名称, COLUMN_TYPE 数据类型, false as 是键 FROM INFORMATION_SCHEMA.COLUMNS where -- wx 为数据库名称&#xff0c;到时候只需要修改成你要导出表结构的数据库即可 table_schema yuncourt_ai AND -- articl…

蓝库云|制造业数字化转型为何转不动?资料处理很重要

数字化转型已经成为每个产业势在必行的课题&#xff0c;没有人会怀疑数字化技术与科技能解放的生产力能量&#xff0c;但为什么看似美好的愿景&#xff0c;实行起来却如此缓慢&#xff1f;蓝库云认为这是因为没有盖好「资料治理」的地基。 面对不断变化的法令规范要求&#xf…

<<Java开发环境配置>>7-Apache Tomcat安装教程环境变量配置IDEA配置

一.Apache Tomcat简介: Apache是普通服务器&#xff0c;本身只支持html即普通网页。不仅可以通过插件支持php,还可以与Tomcat连通(单向Apache连接Tomcat,就是说通过Apache可以访问Tomcat资源。反之不然)。Apache只支持静态网页&#xff0c;但像php,cgi,jsp等动态网页就需要Tomc…

无需端口映射,快解析外网访问U8C

U8C是用友针对成长型、创新型企业&#xff0c;提供企业级ERP整体解决方案&#xff0c;支持多组织业务协同、营销创新、智能财务、人力服务&#xff0c;构建产业链制造平台&#xff0c;实现企业互联网资源连接、共享、协同。 在系统实施过程中&#xff0c;多数企业想要自主掌握企…

分支管理方案

背景 在工作的过程中&#xff0c;git管理方式已经成为每一个项目开发的基础&#xff0c;每个项目的开发都离不开git管理方式。 但是在使用的过程中&#xff0c;由于对git分支管理方案的了解不深&#xff0c;导致会出现分支管理不明确的情况。 本文主要是做科普作用&#xff…

c++类与对象

1.类的定义 2.类的构造函数 1.函数名与类名相同 2.无返回值 3.对象实例化时编译器会自己调用构造函数 4.构造函数可以重载 5.没有显示定义时&#xff0c;编译器会自动调用一个无参的默认构造函数&#xff0c;一旦写显示则不生成 6.对于内置类型构造函数不做处理&#xff0c;对于…

麒麟 arm架构安装nginx

目录 1、下载nginx安装包并解压 在线安装&#xff1a; 离线安装&#xff1a; 上传nginx安装包&#xff08;下载地址&#xff1a;https://nginx.org/download/nginx-1.20.2.tar.gz&#xff09;到指定目录 2、安装系统相关依赖软件、组件包 1、上传或者下载对应的组件包 2、安…

SQL 中的窗口函数

SQL 中的窗口函数&#xff08;Window Functions&#xff09;是一种特殊的函数&#xff0c;它可以在查询结果的某个区间内执行计算&#xff0c;而不仅仅是对单个行进行计算。 以下是一些常用的窗口函数&#xff1a; 1、ROW_NUMBER() ROW_NUMBER() 函数可以给查询结果的每一行…