实现自己的数据库三

news2024/12/25 15:19:59

一 前言

上篇实现了数据库的持久化,就是一个质的飞跃,虽然代码不复杂,但是对没有这方面经验者来说,还是意思的,下一步就是要完成另外一个飞跃,将存储的数据结构采用B+树的形式来保存。在改造之前,还有些准备工作,一是将代码改动下,引入游标这个概念,二是对B+树的结构和原理再做一次梳理,然后才能进入代码开发阶段。

二 游标cursor

游标这个概念,再这里面可以抽象认为为指向一行的指针,通过这个指针我们可以做插入或查询、以及移动到下一行row等。 定义结构如下:

typedef struct {
Table* table;
uint32_t row_num;
bool end_of_table;  // Indicates a position one past the last element
} Cursor;

定义内容比较简单,游标归属的表,归属的行号、是否为表的结尾,遍历时候使用,表结束后退出查询等。

定义好游标,第一个想法可能是如何创建游标,由于游标可以用来遍历之用,我们在一些语言的集合遍历的时候也有定义类似Iter 之类的,和游标类似,一般通过集合的start()行数创建,创建指向第一个元素,这里面的游标也是类似,start创建即指向第一个游标,end指向最后一个元素。

Cursor* table_start(Table* table) {
  Cursor* cursor = (   Cursor* )malloc(sizeof(Cursor));
  cursor->table = table;
  cursor->row_num = 0;
  cursor->end_of_table = (table->num_rows == 0);
  return cursor;
}

Cursor* table_end(Table* table) {
   Cursor* cursor =(   Cursor* ) malloc(sizeof(Cursor));
   cursor->table = table;
   cursor->row_num = table->num_rows;
   cursor->end_of_table = true;
   return cursor;
}

有了这个定义,我们就可以简化定位一行的操作,原来的核心行数:

void* row_slot(Table* table, uint32_t row_num)

可以改成才有游标的形式:

void* cursor_value(Cursor* cursor)

里面的内容也需要按照数据结构做调整:

void *row_slot(Cursor* cursor)
{
 // 行号通过游标获取
        uint32_t row_num = cursor->row_num;
        uint32_t page_num = row_num / ROWS_PER_PAGE;
        // 页面参数通过游标获取到table后再获取pager
        void *page = get_page(cursor->table->pager, page_num);
        uint32_t row_offset  = row_num % ROWS_PER_PAGE;
        uint32_t byte_offset = row_offset * ROW_SIZE;
        return (char *)page + byte_offset;
}

对于游标的递增,可以通过下面行数实现:

void cursor_advance(Cursor* cursor) {
    cursor->row_num += 1;
   // 如果行数达到了表的最大行数,设置游标的表结束标志
   // 循环时候可以根据这个判断来决定是否结束
   if (cursor->row_num >= cursor->table->num_rows) {
      cursor->end_of_table = true;
   }
}

对于我们原来的核心代码中的执行逻辑也要进行改动了,查询循环原来是按照行数遍历现在改成游标的方式,只所以用游标抽象一层,是因为我们通过游标可以封装后面的B+树的遍历,而我们原来的遍历是和表的行紧密绑定的,这里面又包含了将容易改变的部分要抽离出来,整个架构依赖于抽象实现,抽象的具体实现,就可以根据需要灵活改动,而上层架构不受到影响,秒啊!

ExecuteResult execute_select(Statement *statement, Table *table)
{
 Row row;
 Cursor *cursor = table_start(table);
 // for (uint32_t i = 0; i < table->num_rows; i++) {
 //         deserialize_row(row_slot(table, i), &row);
 //         print_row(&row);
 // }
        // 直到游标是最后标记则结束
 while (!(cursor->end_of_table)) {
  deserialize_row(cursor_value(cursor), &row);
  print_row(&row);
           // 游标向下移动一行
  cursor_advance(cursor);
 }
 return EXECUTE_SUCCESS;
}

同样插入操作也要改动,首先我们获取需要插入的行,然后定义一个指向表结尾的游标,为什么要指向表结尾的游标那,因为我们的插入操作其实是一种追加,直接追加到最后一个,然后通过cursor_value 获取游标处的内存页,将表的行持久化到游标处的内存页中。

ExecuteResult execute_insert(Statement *statement, Table *table)
{
 if (table->num_rows >= TABLE_MAX_ROWS) {
  return EXECUTE_TABLE_FULL;
 }
 Row *row_to_insert = &(statement->row_to_insert);
 Cursor *cursor = table_end(table);
 serialize_row(row_to_insert, cursor_value(cursor));
 table->num_rows += 1;

 free(cursor);
 return EXECUTE_SUCCESS;
}

获取游标处的内存操作如下,和原来类似,通过行号定位到属于哪个页面,通过行号定位到偏移量,如果仔细看的同学可能会发现,这里面的定位的时候是按照最后一行定位的,不应该是最后一行的下一行那,这里面实际没问题的,因为行的偏移量从0开始的,比如如果表里面只有一行数据,那表的row_num为1,则row_num为1的偏移量其实是没数据的,将数据插入到这个空闲位置没问题。

void *cursor_value(Cursor *cursor)
{
 uint32_t row_num = cursor->row_num;
 uint32_t page_num = row_num / ROWS_PER_PAGE;
 void *page = get_page(cursor->table->pager, page_num);
 uint32_t row_offset  = row_num % ROWS_PER_PAGE;
 uint32_t byte_offset = row_offset * ROW_SIZE;
 return (char *)page + byte_offset;
}

游标抽象化之后测试:

[root@localhost microdb]# ./a.out db.mb 
microdb > select
(1,a,a@qq.com)
(2,b,b@qq.com)
(1,a,a@qq.com)
(4,rrr,rrr@qq.com)
(5,ttt,ttt@qq.com)
(6,d,d@qq.com)
(7,f,f@qq.com)
(8,g,g@qq.com)
(10,q,q@163.com)
Executed.
microdb > insert 11 dd dd@qq.com
Executed.
microdb > select
(1,a,a@qq.com)
(2,b,b@qq.com)
(1,a,a@qq.com)
(4,rrr,rrr@qq.com)
(5,ttt,ttt@qq.com)
(6,d,d@qq.com)
(7,f,f@qq.com)
(8,g,g@qq.com)
(10,q,q@163.com)
(11,dd,dd@qq.com)
Executed.
microdb > .exit
[root@localhost microdb]# ./a.out db.mb 
microdb > select
(1,a,a@qq.com)
(2,b,b@qq.com)
(1,a,a@qq.com)
(4,rrr,rrr@qq.com)
(5,ttt,ttt@qq.com)
(6,d,d@qq.com)
(7,f,f@qq.com)
(8,g,g@qq.com)
(10,q,q@163.com)
(11,dd,dd@qq.com)
Executed.
microdb > quit
Unrecognized keyword at start of 'quit'.
microdb > .exit

三 B+树

3.1 搜索

我理解的搜索是从大量数据中提取所需要的数据,比如图书馆去找一本书,比如在一本书里面搜寻特定的关键词,那如何能快速搜索那,核心在于每步搜索都能快速缩减数据集合,仔细体会下,如果我们每步查询都可以将搜索的集合减少一半,那就是典型的二分法的搜索,那给我们一堆数据,比如以上设计的数据库,去搜索的时候,由于数据之间和位置之间是没有任何关系的,我们只能一行行遍历查询比较。但是这样的性能是和行数成正比的,即行数越多,我们需要查询的平均时间越长。

有什么办法解决这个问题那,然我们用一组数据在内存中简单模拟下我们的表数据情况,如果要快速搜索这组数据,数据又很大的话,那么如果数据是排序的数组组成,我们可以通过二分法快速的找到所要的数据。但是如果用数组存储这组数据,在插入数据的时候,就需要将插入位置的元素都向后移动,导致插入的时候后面的元素都要向后移动一位。同样如果我们做删除操作,需要将删除位置的后面的数据向前移动,补上删除的空洞。

从上面描述来看,用数组保存排序的数据,查询速度快,但是插入和删除太慢了。而想插入和删除的速度快,我们很容易想到的数据结构是链表,链表插入和删除都是更改指针的事情,比较简单,但是遍历的时候,我们只能沿着链表一个个去查,性能又和数据集合大小相关了。

好吧,再思考下排序数组按照二分法来查找数据的场景,每次都可以将要搜索的数据集合缩小一半,那么我们是不是可以把单链表改造下,将排序链表从中间值提溜起来, 中间值作为root节点,比root节点保存值小的,放在左边的链表中,比root值大的放在右边链表中,即变成一个二叉树,这样就变成了一个完全二叉树,插入和删除性能很高,查询的时候性能仍然很高(完全二叉树插入需要平衡,这个也影响了性能,所以设计了红黑树等取插入、删除、查询都均衡的数据结构)。

二叉树的数据结构在内存中保存数据挺好,但是如果作为存在磁盘上的数据就有很多麻烦,二叉树每个节点最多有两个子树,如果数量大,导致整个树的高度比较高,树高了之后那,那需要指针就比较多,每次指针指向一个页面的话,导致需要从磁盘中读取N多个页面,这对我们搜索数据很不利。

3.2 B树和B+树

82a8bafb351f311a6ada02f913d71136.png
B树结构

B树结构可以看作是二叉树的进化,我们的指针每次指向一个页面,因为一个磁盘的页面现在一般都是4KB大小,这个页面可以保存很多行数据,那我们的B树结构就可以在一个页面有多个分叉,一个页面又可以保存很多数据。

数据的数据存储有的是B+树,有的是B树这两者有何区别那,如下表:101a63c25f08492dec52b3525dc60b89.png

  1. B+树用来存储表数据的,Mysql的InnoDB引擎采用B+树存储,多少个索引就多少个B+树,数据存储在主键的B+树上的,B+树的内部节点只保存key,叶子节点是全部行数据,因为内部节点只保存key,所以一个节点可以保存更多的数据,而B树的节点保存不管是key还包含value,所以一个页面能存储的数量更少。

  2. B树的内部节点和叶子节点有相同的数据结构,而B+树是不同的,B+树内部节点保存的是key和指针数据,叶子节点是完整的多行数据。

  3. 还要注意一点B+树和B树的节点一般对应一个磁盘页面,页面内的数据是有序的。 查找时候可以按照二分法进行查找。

3.3 m阶有序树

我们刚才聊到B树或B+树的可以有N多个子节点,节点内的数据又是有序的,我们可以叫做N阶排序树。这种树的内部节点和叶子节点是不同的,具体区别如下:b860a025ea86571fc1e547eaf8822999.png

  1. 内部节点存储的是key和指向子节点的指针,叶子节点保存的是key和value

  2. 内部节点的保存key的数量最大为m-1个,而叶子节点能保存多少保存多少

  3. 内部节点保存指针的数量为key的数量+1个,而叶子节点不保存指针。

  4. 内部节点保存key的目的是为了路由,叶子节点保存key的目的是为了和value关联。

  5. 内不能节点不保存value,而叶子节点保存value数据。

3.4 3阶排序树的插入过程

按照上述定义,我们来画图模拟三阶排序树的插入过程,三阶排序树有以下约束:

  1. 每个内部节点最多保存三个子节点。

  2. 每个内部节点最多保存二个key。

  3. 每个内部节点至少有两个子节点。

  4. 每个内不能节点至少有一个key。

  5. 初始是一颗空树:786b41e3bddcb05c1dde668ff41e4673.png

这个空树直接作为没存任何数据的叶子节点。

  1. 我们插入两个数据,如下:1b357909900e9ed3889f7ece8076a23b.png数据没超出三个数据的范围,我们将两条数据直接保存在这个节点上。

  2. 假如一个节点只能保存2个数据,那么上述数据结构再插入一个数据后,会怎么办,只能分裂:fe038a86072d85dc3813fb20044ab6fc.png我们插入数据后,由于不够保存,只能拆分上面的叶子节点,分成一个根节点和两个叶子节点,数据分到两个叶子节点上,根节点保存两个指针和一个key。 查询的时候,找小于等于5的走左边的子节点,查大于等于5的,走右边的子节点。

  3. 插入key2 后,按照查找路由,找到叶子节点,但是叶子节点满了,只能分裂叶子节点,将原来的key5 分裂到新的叶子节点上去,并对key 2创建新的条目。

16d7f590fc52b114b0e60e547abd8754.png
插入key2 后
  1. 继续插入18和21两条数据,树的结构变化如下:854692636fef40bcd774ce330a7ba1f0.png当插入21 的时候,右边的叶子节点不够存储,如果再次创建叶子节点,则根节点要增加个指针,但是根节点也满了,需要将根节点再拆分,这样就增加了B+树的高度。df6a88bba81668a6efbfa6b8e087b747.png

后续的B+树的数据结构实现下篇幅再来聊聊。

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

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

相关文章

为什么要设计非公平锁?

背景 公平&#xff1a;排队 非公平&#xff1a;在合适时机插队 非公平还是 ReentrantLock 的默认策略&#xff0c;排队时间不浪费了&#xff1f; 场景 来看这种场景 假如A持有一把锁&#xff0c;B请求这把锁&#xff0c;这时候B被挂起进入阻塞&#xff0c;A释放锁的时候&a…

点与线段的关系

点与线段的关系 对于向量a(x1,y1)和b(x2,y2) 点乘的数学意义&#xff1a;a * b x1x2 y1y2 点乘的几何意义&#xff1a;a * b |a||b|cosQ 这个可以看成是投影关系表达式&#xff1a;cosQ a * b / ( |a||b|) 令r cosQ&#xff1b; 求p点和线段AB的位置关系&#xff0c;可以…

将字符串根据指定的分隔符拆分为元组str.partition()

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 将字符串根据指定的分隔符拆分为元组 str.partition() 选择题 对于以下python代码最后输出的结果是? string "I Love Python" print("【显示】string.partition(Love)"…

Unity3d 微信小程序(小游戏)项目实现流量主接入功能(含源码)

前言 很早之前编写了Unity导出微信小游戏的博客&#xff0c;也尝试自己做了个Demo上线了&#xff0c;基本没更新过&#xff0c;不过几个月的时间&#xff0c;用户超过了一千&#xff0c;可以开通流量主了&#xff0c;大概率是因为上篇的帖子浏览量大了&#xff0c;扫码体验的人…

Aurora、Chip2chip、Ethernet(二)

摘要&#xff1a;Aurora、Chip2chip、Ethernet共用一个gt时钟的正确的解决方案以及在实际实现以及在实现过程中遇到的其它的问题。 我在实际中遇到的困难如下&#xff1a; 现在需要将三个ip共用一对GT时钟&#xff0c;一个Ethernet IP&#xff0c;一个Ethernet IP&#xff08…

每日学术速递1.29

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.CV 1. Compact Transformer Tracker with Correlative Masked Modeling 标题&#xff1a;带有相关掩码建模的紧凑型变压器跟踪器 作者&#xff1a; Zikai Song, Run Luo, Junqing Yu, Y…

GLM国产大模型训练加速:性能最高提升3倍,显存节省1/3,低成本上手

作者&#xff5c;BBuf、谢子鹏、冯文 2017 年&#xff0c;Google 提出了 Transformer 架构&#xff0c;随后 BERT 、GPT、T5等预训练模型不断涌现&#xff0c;并在各项任务中都不断刷新 SOTA 纪录。去年&#xff0c;清华提出了 GLM 模型&#xff08;https://github.com/THUDM…

一句话说明线程和进程

知识点&#xff1a; 1、一句话说明线程和进程 2、操作系统为什么需要进程 3、为什么要引入线程 4、一图说明线程和进程的关系 一、一句话说明线程和进程 进程&#xff1a;是指⼀个内存中运⾏的应⽤程序&#xff0c;比如QQ、微信、浏览器等&#xff1b;⼀个应⽤程序可以同时运⾏…

CAS 和 Synchronized优化过程以及常见的锁策略

目录 &#x1f411;今日良言:追星赶月莫停留,平芜尽处是春山 &#x1f402;一、锁策略 &#x1f43c;二、CAS &#x1f42d;三、Synchronized &#x1f411;今日良言:追星赶月莫停留,平芜尽处是春山 &#x1f402;一、锁策略 锁策略是实现锁的时候,考虑出现锁竞争了该怎么…

电驱系统电磁兼容基础知识及测试方法

电驱系统电磁兼容基础知识及测试方法 学习参考&#xff1a;驱动视界公众号、百度百科、《电动汽车电机驱动系统EMC研究综述》 1.背景 2.电磁干扰三要素 3.电波暗室与屏蔽室的原理 4.测试方法 5.如何看测试数据 6.工作中需要注意的EMC的几点问题 7.案例 1.背景 汽车工业发展…

docker安装db2

第一步&#xff1a;下载镜像 docker pull ibmcom/db2express-c:latest备注&#xff1a;docker images -a 可以查看已安装镜像&#xff1b; 第二步&#xff1a;启动镜像 docker run -d --name db2 -p 50000:50000 -e DB2INST1_PASSWORD[数据库密码] -e LICENSEaccept ibmcom…

玩转PPT 第1节 PPT制作理念学习笔记

ppt神器islide 第1节 初步接触强大的工具1 PPT大神的课程总结1.1 骨架篇1.2 色彩篇1.3 对齐篇1.4 对比篇1.5 修饰篇1.6 字体篇1.7 素材篇1.8 线条篇1.8.1 可以随意画线条&#xff0c;填充空白1.8.2 在字体上画线条&#xff0c;做成艺术字1.8.3 做对称线条&#xff0c;比如递进三…

Bluesky勒索软件深度技术分析

0 1、 背景介绍 勒索软件作为一种流行的木马&#xff0c;近年来已成为最为常见的安全威胁之一。与其他威胁不同&#xff0c;勒索软件先向受害者表明自己的身份&#xff0c;再通过加密的方式使用户数据资产或计算资源无法正常使用&#xff0c;而恢复数据资产的唯一方法通常是支…

收藏破万,谷歌联手哈佛发布首个炼丹指南:教你科学化「调参」

调参不能只靠直觉&#xff0c;也是一门大学问&#xff01; 虽然算法工程师往往调侃自己是「调参侠」&#xff0c;但调参这件事可能真没想象中那么简单。 比如&#xff0c;你是不是经常会有疑惑到底该选择哪个优化器&#xff1f;一个batch放多少数据&#xff1f;超参数如何设置…

人工智能英文缩写怎么读,人工智能英文缩写大全

1、人工智能英文缩写是什么&#xff1f; AI。 人工智能&#xff0c;英文缩写为AI。它是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术以及应用系统的一门技术科学。“十三五”以来&#xff0c;我国高度重视人工智能的发展&#xff0c;国内科研机构也围绕人工智能…

【Pytorch项目实战】之机器翻译:编码器-解码器、注意力机制AM

文章目录机器翻译 - 中英文翻译算法一&#xff1a;编码器-解码器&#xff08;Encoder - Decoder&#xff09;算法二&#xff1a;注意力机制&#xff08;Attention Model&#xff0c;AM&#xff09;2.1为何要引入注意力机制&#xff1f;2.2注意力机制的语义向量表示C计算2.3每个…

2023年首更,警惕6本SCISSCI期刊被剔除

2023年1月17日&#xff0c;Web of Science核心期刊目录首次更新。此次SCIE & SSCI期刊目录更新&#xff0c;与上次更新&#xff08;2022年12月&#xff09;相比&#xff0c;共有6本期刊被剔除出SCIE & SSCI期刊目录&#xff0c;详情如下&#xff1a; 公众号“Unionpub…

Redis实战9-全局唯一ID

发布优惠券的时候&#xff0c;每个店铺都可以发布优惠券&#xff0c;当用户抢购的时候,优惠券表中的id如果使用数据库的自增长ID会存在以下问题&#xff1a; 1&#xff1a;id的规律太明显&#xff0c;容易被刷 2&#xff1a;当数据量很大的时候&#xff0c;会受到单表数据的限…

三、分布式id,lua脚本,分布式锁,消息队列

文章目录优惠卷秒杀1.redis实现分布式ID2.优惠券秒杀下单3.超卖问题4.lua脚本5.分布式锁6.redis stream消息队列实现异步秒杀7.redis消息队列list实现消息队列PubSub实现消息队列stream实现消息队列stream的消息队列-消费者组学习黑马点评项目整理总结:https://www.bilibili.co…

5 个用于自动化的杀手级 Python 脚本

Python 是一种功能强大的语言&#xff0c;广泛用于自动执行各种任务。无论您是开发人员、系统管理员&#xff0c;还是只是想通过自动化日常任务来节省时间的人&#xff0c;Python 都能满足您的需求。 这里有 5 个 Python 脚本&#xff0c;可以帮助您自动执行各种任务 文章目录…