实现自己的数据库四

news2024/11/14 15:41:53

一前言

上一篇已经说明了B+树的一些原理,也讲到,我们目前采用的持久化数据的方式,而且我们是单独的插入数据,没有任何元数据信息,虽然插入的速度很快,因为是采用追加的方式。但是这种方式插入速度很快,像上次所说,查询和删除的速度会很慢。

df05c8fcc4633092ca39980d6a7725fa.png
数据结构性能对比图

我们以前用的就是非排序数组行,保存的是数据,没有其他信息。插入性能最好,但是删除和查找时间复杂度为O(n),排序数组查找很快,可以采用二分法查找,时间复杂度为O(log(n)),但是插入和删除时间复杂度为O(n),而采用B+树方式,且保存了元数据和主键的情况下,无论是查找、插入还是删除,性能都达到了均衡。

二 改造

2.1 元数据信息

采用树的方式来保存数据库的数据时候,就不能是简单的只记录原始信息,还需要记录诸如子节点指针信息, 为了方便遍历到兄弟节点,还需要保存这指向父节点的指针信息(这里面的指针类似c语言的指针,在磁盘保存时候,要看具体的实现,可能是个页号)。bf8150b05c75d8230e8fc220285f97ab.png同样我们为了区分子节点和叶子节点,需要保存节点的类别,以及是否为root节点这些信息。

用这种排序的树保存数据,还有个好处,就是遍历比较方便。

节点类型

typedef enum { NODE_INTERNAL, NODE_LEAF } NodeType;

root节点元数据

/*
 * Common Node Header Layout
 */
// 节点类型数据的大小,其实只有一个bit就可以区分叶子节点和根节点,这里面浪费了点
const uint32_t NODE_TYPE_SIZE = sizeof(uint8_t);
// 节点类型的偏移,放在页节点的开头
const uint32_t NODE_TYPE_OFFSET = 0;
// 是否为root的元数据大小
const uint32_t IS_ROOT_SIZE = sizeof(uint8_t);
// 是否为root的元数据的偏移量
const uint32_t IS_ROOT_OFFSET = NODE_TYPE_SIZE;
// 指向父指针的指针大小
const uint32_t PARENT_POINTER_SIZE = sizeof(uint32_t);
// 指向父指针的偏移量
const uint32_t PARENT_POINTER_OFFSET = IS_ROOT_OFFSET + IS_ROOT_SIZE;
// 整个Node节点的元数据整体尺寸
const uint8_t COMMON_NODE_HEADER_SIZE =
  NODE_TYPE_SIZE + IS_ROOT_SIZE + PARENT_POINTER_SIZE;

除了root节点外,就是叶子节点,叶子节点保存完整的数据信息,所以和root节点的内容有所不同。

/*
 * Leaf Node Header Layout
 */
// 叶子节点保存的cell数量,一个cell由key和value组成 可以看作一个key后面跟着持久化的行
const uint32_t LEAF_NODE_NUM_CELLS_SIZE = sizeof(uint32_t);
// 叶子节点的cell数量的偏移量
const uint32_t LEAF_NODE_NUM_CELLS_OFFSET = COMMON_NODE_HEADER_SIZE;
// 叶子节点的元数据大小
const uint32_t LEAF_NODE_HEADER_SIZE =
  COMMON_NODE_HEADER_SIZE + LEAF_NODE_NUM_CELLS_SIZE;

叶子节点保存的cell数量,一个cell由key和value组成 可以看作一个key后面跟着持久化的行。5d51d1bb96aff173e0434e4c6e4566e1.png

示意图很清楚说明了第一个字节是node_type,接着一个字节是is_root,后面四个字节是父节点的指针,再下面4个字节为cell的数量,这里面有个错误就是写了两个,剩下内容就是cell,即key+value,都是这样部署的,Node节点不够存cell的就浪费了。

访问叶子节点方法

// 叶子节点中cell数量地址获取
uint32_t* leaf_node_num_cells(void* node) {
return node + LEAF_NODE_NUM_CELLS_OFFSET;
}
// 叶子节点上第cell_num个cell的偏移量的地址
void* leaf_node_cell(void* node, uint32_t cell_num) {
 return node + LEAF_NODE_HEADER_SIZE + cell_num * LEAF_NODE_CELL_SIZE;
}
// 叶子节点上第cell_num个key的偏移量,因为cell的前面放的是key
uint32_t* leaf_node_key(void* node, uint32_t cell_num) {
  return leaf_node_cell(node, cell_num);
}
// 叶子节点上第cell_num个cell的value地址获取
void* leaf_node_value(void* node, uint32_t cell_num) {
 return leaf_node_cell(node, cell_num) + LEAF_NODE_KEY_SIZE;
}
// 初始化一个节点,将cell_num设置为0
void initialize_leaf_node(void* node) { *leaf_node_num_cells(node) = 0; }

2.2 Table和Pager的改动

首先整个设计考虑简单点,不支持部分页面的读取,每次读取是读取整个页面。

const uint32_t PAGE_SIZE = 4096;
 const uint32_t TABLE_MAX_PAGES = 100;
 
 
 typedef struct {
   int file_descriptor;
   uint32_t file_length;
+  uint32_t num_pages;
   void* pages[TABLE_MAX_PAGES];
 } Pager;
 
 typedef struct {
   Pager* pager;
-  uint32_t num_rows;
+  uint32_t root_page_num;
 } Table;

在新的定义中,我们固定了每个表的最大行数。另外我们将page的数量保存在Pager中。在Table中保存根页面的page_num,这样我们就可以通过表方便找到root页面了。

下面是一些关键行数的修改:

void* get_page(Pager* pager, uint32_t page_num) {
     pager->pages[page_num] = page;
    if (page_num >= pager->num_pages) {
     pager->num_pages = page_num + 1;
    }
   return pager->pages[page_num];
  }

///
Pager* pager_open(const char* filename) {
   Pager* pager = malloc(sizeof(Pager));
   pager->file_descriptor = fd;
   pager->file_length = file_length;
  pager->num_pages = (file_length / PAGE_SIZE);
 
  if (file_length % PAGE_SIZE != 0) {
     printf("Db file is not a whole number of pages. Corrupt file.\n");
    exit(EXIT_FAILURE);
   }

2.3 游标的更改

游标定位数据,以前通过行来定位,现在通过页面号和cell号来定位数据。

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

游标创建:

Cursor* table_start(Table* table) {
   Cursor* cursor = malloc(sizeof(Cursor));
   cursor->table = table;
-  cursor->row_num = 0;
-  cursor->end_of_table = (table->num_rows == 0);
+  cursor->page_num = table->root_page_num;
+  cursor->cell_num = 0;
+
+  void* root_node = get_page(table->pager, table->root_page_num);
+  uint32_t num_cells = *leaf_node_num_cells(root_node);
+  cursor->end_of_table = (num_cells == 0);
 
   return cursor;
 }

表开始的游标定位,游标的页面为表的根页面编号,通过leaf_node_num_cells行数获取root节点中cell的数量。

Cursor* table_end(Table* table) {
   Cursor* cursor = malloc(sizeof(Cursor));
   cursor->table = table;
-  cursor->row_num = table->num_rows;
+  cursor->page_num = table->root_page_num;
+
+  void* root_node = get_page(table->pager, table->root_page_num);
+  uint32_t num_cells = *leaf_node_num_cells(root_node);
+  cursor->cell_num = num_cells;
   cursor->end_of_table = true;
 
   return cursor;
 }

这个是表的结束页的游标,设置end_of_table的标志。

void* cursor_value(Cursor* cursor) {
-  uint32_t row_num = cursor->row_num;
-  uint32_t page_num = row_num / ROWS_PER_PAGE;
+  uint32_t page_num = cursor->page_num;
   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 page + byte_offset;
+  return leaf_node_value(page, cursor->cell_num);
 }

获取游标处的值,获取cell_num的value,由于每个cell的大小一样,所以也是类似的取值方法。游标的递增:

void cursor_advance(Cursor* cursor) {
-  cursor->row_num += 1;
-  if (cursor->row_num >= cursor->table->num_rows) {
+  uint32_t page_num = cursor->page_num;
+  void* node = get_page(cursor->table->pager, page_num);
+
+  cursor->cell_num += 1;
+  if (cursor->cell_num >= (*leaf_node_num_cells(node))) {
     cursor->end_of_table = true;
   }
 }

这里面每次递增,只增加cell的数量,也许你会说cell递增到最后一个cell怎么办,其实增加到最后到最后一个cell后,设置了表结束的标志,循环退出了。

数据库打开打开数据库,如果是一个新的数据库,初始化一个页面作为叶子节点。

Table* db_open(const char* filename) {
   Pager* pager = pager_open(filename);
   Table* table = malloc(sizeof(Table));
   table->pager = pager;
  table->root_page_num = 0;

  if (pager->num_pages == 0) {
    // New database file. Initialize page 0 as leaf node.
    void* root_node = get_page(pager, 0);
    initialize_leaf_node(root_node);
  }
 
   return table;
 }

下面是个关键行数,先叶子节点插入数据:

void leaf_node_insert(Cursor* cursor, uint32_t key, Row* value) {
 void* node = get_page(cursor->table->pager, cursor->page_num);

  uint32_t num_cells = *leaf_node_num_cells(node);
 
  if (num_cells >= LEAF_NODE_MAX_CELLS) {
    // 节点满了
    printf("Need to implement splitting a leaf node.\n");
   exit(EXIT_FAILURE);
  }

  if (cursor->cell_num < num_cells) {
    // 为一个cell腾出位置位置
  for (uint32_t i = num_cells; i > cursor->cell_num; i--) {
     memcpy(leaf_node_cell(node, i), leaf_node_cell(node, i - 1),
            LEAF_NODE_CELL_SIZE);
    }
  }
// 增加cell_num,设置key和持久化row。
  *(leaf_node_num_cells(node)) += 1;
  *(leaf_node_key(node, cursor->cell_num)) = key;
  serialize_row(value, leaf_node_value(node, cursor->cell_num));
}

这个函数假设树只有一个页面,目前版本先不支持多个页面。插入操作:

ExecuteResult execute_insert(Statement* statement, Table* table) {
  void* node = get_page(table->pager, table->root_page_num);
  if ((*leaf_node_num_cells(node) >= LEAF_NODE_MAX_CELLS)) {
     return EXECUTE_TABLE_FULL;
   }
 
   Row* row_to_insert = &(statement->row_to_insert);
   Cursor* cursor = table_end(table);
  leaf_node_insert(cursor, row_to_insert->id, row_to_insert);
   free(cursor);
}

三 打印命令

打印meta信息即元数据信息。

void print_constants() {
 printf("ROW_SIZE: %d\n", ROW_SIZE);
  printf("COMMON_NODE_HEADER_SIZE: %d\n", COMMON_NODE_HEADER_SIZE);
  printf("LEAF_NODE_HEADER_SIZE: %d\n", LEAF_NODE_HEADER_SIZE);
  printf("LEAF_NODE_CELL_SIZE: %d\n", LEAF_NODE_CELL_SIZE);
  printf("LEAF_NODE_SPACE_FOR_CELLS: %d\n", LEAF_NODE_SPACE_FOR_CELLS);
  printf("LEAF_NODE_MAX_CELLS: %d\n", LEAF_NODE_MAX_CELLS);
}

MetaCommandResult do_meta_command(InputBuffer* input_buffer, Table* table) {
   if (strcmp(input_buffer->buffer, ".exit") == 0) {
     db_close(table);
     exit(EXIT_SUCCESS);
  } else if (strcmp(input_buffer->buffer, ".constants") == 0) {
      printf("Constants:\n");
      print_constants();
     return META_COMMAND_SUCCESS;
   } else {
     return META_COMMAND_UNRECOGNIZED_COMMAND;
   }

没啥需要特别说明的,只是支持一个打印常量元数据的命令。

四 树的可视化

为了帮助调试,增加打印树的功能:

void print_leaf_node(void* node) {
  uint32_t num_cells = *leaf_node_num_cells(node);
  printf("leaf (size %d)\n", num_cells);
  for (uint32_t i = 0; i < num_cells; i++) {
    uint32_t key = *leaf_node_key(node, i);
   printf("  - %d : %d\n", i, key);
  }
}

遍历节点,打印cell信息。增加meta命令:

MetaCommandResult do_meta_command(InputBuffer* input_buffer, Table* table) {
   if (strcmp(input_buffer->buffer, ".exit") == 0) {
     db_close(table);
     exit(EXIT_SUCCESS);
  } else if (strcmp(input_buffer->buffer, ".btree") == 0) {
    printf("Tree:\n");
    print_leaf_node(get_page(table->pager, 0));
   return META_COMMAND_SUCCESS;
   } else if (strcmp(input_buffer->buffer, ".constants") == 0) {
     printf("Constants:\n");
     print_constants();
     return META_COMMAND_SUCCESS;
   } else {
     return META_COMMAND_UNRECOGNIZED_COMMAND;
   }

这次最大的改动是文件的存储结构改变,改成B+树方式,但是我们并没有对文件里面的cell按照key进行排序,而且我们只支持一个页面,不过仍然是重大的进步,慢慢来吧。

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

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

相关文章

Pd1 药物研发进展|销售数据|市场规模|竞争格局|前景分析

Programmed Death-1 (PD-1; CD279) 是一种在活化 T 细胞中诱导的抑制性受体&#xff0c;作为多种癌症的一线治疗药物。然而&#xff0c;严重的免疫相关不良反应限制了PD-1/PD-L1单克隆抗体的临床应用&#xff0c;尽管其疗效良好。 也迫切需要开发针对 PD-1/PD-L1 轴的新型抑制剂…

Torch 论文复现:Vision Transformer (ViT)

论文标题&#xff1a;An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale 从 TPUv3-core-days 可以看到&#xff0c;ViT 所需的训练时间比 ResNet 更短&#xff0c;同时 ViT 取得了更高的准确率 ViT 的基本思想是&#xff0c;把一张图片拆分成若干个…

Paddle入门实战系列(四):中文场景文字识别

✨写在前面&#xff1a;强烈推荐给大家一个优秀的人工智能学习网站&#xff0c;内容包括人工智能基础、机器学习、深度学习神经网络等&#xff0c;详细介绍各部分概念及实战教程&#xff0c;通俗易懂&#xff0c;非常适合人工智能领域初学者及研究者学习。➡️点击跳转到网站。…

每日学术速递1.31

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 今天带来的arXiv上最新发表的3篇NLP论文。 Subjects: cs.CL、cs.AI、cs.DB、cs.LG 1.Editing Language Model-based Knowledge Graph Embeddings 标题&#xff1a;编辑基于语言模型的知识图谱嵌入 作…

C语言求幂运算——奇特中文变量命名

写在前面 主要涉及C/C趣味编程应用及解析面向初学者撰写专栏&#xff0c;个人代码原创如有错误之处请各位读者指正&#xff0c;各位可以类比做自己的编程作业请读者评论回复、参与投票&#xff0c;反馈给作者&#xff0c;我会获得持续更新各类干货的动力。 致粉丝&#xff1a;…

【Rust】8. 包、Crate 和 模块管理(公有、私有特性)

8.1 包和 Crate 8.1.1 基本概念 crate 是 Rust 在编译时最小的代码单位&#xff1b;crate 有两种形式&#xff1a;二进制项&#xff08;可以被编译为可执行程序&#xff09;和库&#xff08;没有 main 函数&#xff0c;也不会编译为可执行程序&#xff0c;而是提供一些诸如函…

Selenium+Java+Maven(12):引入Allure作为报告生成器

一、前言 本篇作为SeleniumJava系列的补充&#xff0c;讲了如何使用Allure作为测试报告生成器&#xff0c;来替代TestNG自带的测试报告或ReportNG测试报告&#xff0c;生成更加美观的&#xff08;领导更喜欢的&#xff09;测试报表。话不多说&#xff0c;一起来学习吧~ 二、A…

蒙特卡洛算法详解

蒙特卡洛算法是20世纪十大最伟大的算法之一&#xff0c;阿法狗就采用了蒙特卡洛算法。 1、定义 蒙特卡洛方法也称为 计算机随机模拟方法&#xff0c;它源于世界著名的赌城——摩纳哥的Monte Carlo(蒙特卡洛)。 它是基于对大量事件的统计结果来实现一些确定性问题的计算。其实…

什么是独立性?如何提高独立性?

独立是每个人必经的成长阶段&#xff0c;也是实现人生价值最重要的途径。没有独立就不能实现真正意义上的人生。独立是我们克服困难、实现抱负的最重要的精神力量&#xff0c;也是我们收获幸福的保障。1、什么是独立性&#xff1f;独立性是意志指不受他人影响、能够独立解决问题…

迟到两年的求职总结经验分享

迟到两年的求职总结&经验分享 写在前面 ​ 号主于2021年3月-2021年9月断断续续参加了校园招聘&#xff0c;包括但不限于&#xff1a;暑期实习、秋招提前批、秋招正式批。收获offer包括但不限于&#xff1a;某互联网推荐算法工程师、某通讯公司数据挖掘工程师、某金融科技…

docker 安装mysql8

docker 安装mysql8无法远程登录 # 启动容器 docker run \ -p 13306:3306 \ --name mysql \ --privilegedtrue \ --restartalways \ -v /home/mysqldata/mysql:/etc/mysql \ -v /home/mysqldata/mysql/logs:/logs \ -v /home/mysqldata/mysql/data:/var/lib/mysql \ -v /etc/l…

C++11线程间共享数据

C11线程间共享数据 使用全局变量等不考虑安全的方式以及原子变量这里就不进行说明了。 在多线程中的全局变量&#xff0c;就好比现实生活中的公共资源一样&#xff0c;比如你有一个同时只能允许一个人做饭的厨房&#xff0c;那么在你占用期间&#xff0c;你的室友就必须等待。…

synchronized锁的升级

synchronized锁优化的背景 用锁能够实现数据的安全性&#xff0c;但是会带来性能的下降 无锁能够基于线程并行提升程序性能&#xff0c;带来安全性的下降 java5 synchronized默认是重量级锁&#xff0c;java6以后引入偏向锁和轻量锁&#xff0c;java15 逐步废弃了偏向锁 …

机器学习实战(第二版)读书笔记(4)——seq2seq模型注意力机制(BahdanauAttention,LuongAttention)详解

一、Seq2seq模型 机器学习实战(第二版)读书笔记(1)——循环神经网络&#xff08;RNN&#xff09; 中详细介绍了RNN如下图1所示&#xff0c;可以发现RNN结构大多数对序列长度比较局限&#xff0c;对于机器翻译等任务(输入输出长度不想等N to M)&#xff0c;RNN没办法处理&…

SVN使用:Mac电脑中修改SVN输出信息为英文的方法

前言 作为软件开发人员&#xff0c;关于项目代码管理以及维护想必都不陌生&#xff0c;尤其是在团队协作的时候&#xff0c;多人开发维护同一个项目更是需要代码管理。关于项目代码管理维护工具&#xff0c;常用的就是Git、SVN等管理工具。本篇文章只来分享一下关于SVN的配置设…

C语言学习笔记-常量

“常量”的广义概念是&#xff1a;‘不变化的量’。例如&#xff1a;在计算机程序运行时&#xff0c;不会被程序修改的量。 以上是百度百科上对常量的部分定义。C语言的学习过程中将会接触很多的常量&#xff0c;不同类型的常量其定义、用法等会有所差异。要搞清楚他们的相似与…

如何恢复已删除的文件?5分钟搞定的简单方法。

本文介绍如何使用文件恢复程序恢复已删除的文件。它包括与恢复已删除文件相关的提示。 如何恢复已删除的文件 从硬盘驱动器恢复已删除的文件并不是一件疯狂的事情&#xff0c;但一旦您意识到文件已被删除&#xff0c;就尝试恢复会有所帮助。被删除的文件通常不会被真正删除&am…

终于有人把数据仓库讲明白了

数仓概念 ⚫ 数据仓库&#xff08;英语&#xff1a;Data Warehouse&#xff0c;简称数仓、DW&#xff09;,是一个用于存储、分析、报告的数据系统。 ⚫ 数据仓库的目的是构建面向分析的集成化数据环境&#xff0c;分析结果为企业提供决策支持&#xff08;Decision Support&am…

Linux入门教程|| Linux 忘记密码解决方法|| Linux 远程登录

很多朋友经常会忘记Linux系统的root密码&#xff0c;linux系统忘记root密码的情况该怎么办呢&#xff1f;重新安装系统吗&#xff1f;当然不用&#xff01;进入单用户模式更改一下root密码即可。 步骤如下&#xff1a; 重启linux系统 3 秒之内要按一下回车&#xff0c;出现如…

解决Error: Electron failed to install correctly, please delete......报错的问题

问题 在启动electron项目的时候&#xff0c;报mlgb错 Error: Electron failed to install correctly, please delete node_modules/electron and try installing again 搞了 好久 才解决 原因 升级Electron到7.0.0&#xff0c;提示Electron failed to install correctly, p…