数据库 PJ-2 基于B+树的简单数据库

本文档详细介绍了使用B+树构建数据库系统的过程,包括文件存储结构、数据库基本操作(如插入、删除、检索)以及优化策略。在B+树中,每个条目由整数A和12字节字符串B组成,通过键B进行索引。文件存储以页为单位,支持增删查改操作,并确保数据持久化。在删除操作中,通过多次删除直至找不到目标键值,保持数据一致性。此外,代码展示了如何处理B+树节点的分裂、合并以及平衡操作。
1930713****

实验内容

已有一个数据库 myjql 的框架,现在在这个框架中写一棵组织在文件里的 B+ 树,以此实现数据库基本功能。

数据库文件要可持久化存储,也就说退出数据库后会留下数据库文件,再次打开时数据不会丢失。

数据库中每个条目由 16 个字节组成:A(int) + B(12字节字符串)。

按照 B 为关键字建立索引。

要求支持如下操作:

  • select: 列出所有的元组
  • select s: 列出所有 B 为 s 的元组
  • insert i s: 插入一个 A = iB = s 的条目
  • delete s: 删除所有 B 为 s 的条目
  • .exit: 退出数据库

测试集:大约有 6e6 级别的条目。

时间限制:180 s

内存限制:6 MB

硬盘空间限制:无

注意:不能把索引存在内存里,否则一定超出内存空间限制。


文件的结构设计

B+树的条目存在叶子结点上,因此文件里可以只存放一棵B+树。可以按照结点为单位在文件中存储B+树。只要知道每个结点在文件中的首位置,就可以用fseek函数在文件中定位结点并且读取,因此我们用一个整数类型来表示一个结点在文件中的首位置,把这个整数称为node_ptr类。

除了结点,为了方便管理文件信息,还需要一些元数据。

元数据:
typedef unsigned int node_ptr;
struct FILE_INFO{
   
   
  node_ptr root;
  unsigned int lenth; 
  unsigned int entry_num;
};

root:存下B+树的根结点

lenth:B+树文件的总长度,可以据此在文件内分配新的结点

entry_num:树内条目总数

新建结点并且返回起始位置的代码:

node_ptr New_NODE()
{
   
   
  node_ptr ret = finfo.lenth;
  finfo.lenth += sizeof(struct NODE);
  return ret;
}
结点:
struct NODE{
   
   
  node_ptr lsibling,rsibling;
  int is_leaf;
  int key_number;
  char data[IDX_SIZE];
};

lsibling,rsibling:左右邻居结点,用来进行合并、迁移,对于叶子结点可以用来有序遍历。

is_leaf:标记该结点是否是叶子结点。

key_number:用来表示结点现存的键值的数量。

data:char数组,用来存放键值和指针(或对于叶子来说,条目的A值)。4 个字节放一个整数,接着 12 字节放一个字符串,这样无论是叶子结点上条目的A值+B值,还是非叶子结点上的子节点号+键值,都可以用 16个连续的char类型来表示。

结点的大小

因为一个页空间的大小是 4096 字节,因此我将一个结点直接设置为一个页的大小,并且根据计算,一个B+树的结点大概最多能存放252个条目(或键值与子结点号),也就是说这棵树的度(Degree)大概在 126 ,也就是说除了根结点,所有结点应该是 126~252 叉的,应当具有至少50%的占有率。

定义了一些宏来表示这些大小信息:

#define PAGE_SIZE 4096
#define NODE_SIZE 4096
#define DEGREE ((NODE_SIZE-16-16-4)/32)
#define IDX_SIZE (NODE_SIZE-16)

文件存储的实现

myjql 需要能够读取识别上面构造的数据库文件,并且在增删元素的时候修改文件,退出的时候留下文件。

程序开始阶段读文件:
void open_file(const char* filename) {
   
   
  /* open file */
  if(!(fp = fopen(filename,"r+")))//如果文件不存在,新建一个文件,初始化元信息和B+树
  {
   
   
    fp = fopen(filename,"w+");
    finfo.entry_num=0;
    finfo.lenth=sizeof(struct FILE_INFO);
    finfo.root=New_NODE();

    struct NODE node;//建立一个结点,并且插入一条哨兵条目
    memset(&node,0,sizeof(node));
    node.is_leaf=1;
    node.key_number=1;
    np2char(0,node.data);
    memcpy(node.data+4,"\0\0\0\0\0\0\0\0\0\0\0",12);
    write_node(finfo.root,&node);
  }
  else//如果存在,直接把元数据读出来
  {
   
   
    fseek(fp,0,SEEK_SET);
    fread(&finfo,sizeof(struct FILE_INFO),1,fp);
  }
}
程序结束阶段改写元数据:
void exit_nicely(int code) {
   
   
  /* do clean work */
  fseek(fp,0,SEEK_SET);
  fwrite(&finfo,sizeof(struct FILE_INFO),1,fp);
  exit(code);
}
程序执行中读一个结点:
struct NODE node;
fseek(fp,n,SEEK_SET);
fread(&node,sizeof(struct NODE),1,fp);
//从文件把以n为起始位置的结点读到node
程序执行中写一个结点:
void write_node(node_ptr n,struct NODE* node)
{
   
   
  fseek(fp,n,SEEK_SET);
  fwrite(node,sizeof(struct NODE),1,fp);
}

基本操作

分析:

在这个项目中,要求键值相同的条目按照时间后先排序,也就是说相同键值,后到者排在前面。这个看似需要用时间戳维护,但其实不然。当我们插入一个条目 (A,B) 的时候,我们认为此时B+树中所有小于 B 的值都比 B 小,而B+树中所有大于等于 B 的值都比当前插入的这个条目大。这样自然而然,插入条目虽然没有存时间戳的信息,但天然按照时间戳的顺序进行了排序,不需要维护额外的时间戳信息,可谓大道至简。

B+树插入:

递归到叶子进行插入,每递归一层,从文件里把对应结点的信息读入到内存。

如果当前结点满出,则分裂结点,回溯的时候在父亲结点插入新分裂出来的子节点。

结束当前层的时候要注意写回内存。

注意各种细节即可。

node_ptr tree_insert(node_ptr n,char * retkey)
{
   
   
  
  assert(n);
  struct NODE node;
  char newkey[12];

  fseek(fp,n,SEEK_SET);
  fread(&node,sizeof(struct NODE),1,fp);

  //printf("\n node:%d key_number:%d\n",n,node.key_number);
  
  int i;
  for(i = 0;i < node.key_number;i++ )
  {
   
   
    if(strlessequal(statement.row.b,node.data+(i*16+4)))break;
  }
  node_ptr newchildentry;

  if(!node.is_leaf)newchildentry = tree_insert(char2np(node.data+(i*16)),newkey);
  else {
   
   newchildentry = statement.row.a;memcpy(newkey,statement.row.b,12);}
  
  //printf("\nchildentry:%d\n",newchildentry);
  if(!node.is_leaf && !newchildentry)return 0;

  if(!node.is_leaf)i++;
  for(int j=node.key_number;j>=i;j--)
    memcpy(node.data+((j+1)*16),node.data+(j*16),16);
  if(!node.is_leaf)
  {
   
   
    memcpy(node.data+(i*16+4),node.data+(i*16-12),12);
    memcpy(node.data+(i*16-12),newkey,12);
    np2char(newchildentry,node.data+(i*16));
  }
  else
  {
   
   
    np2char(newchildentry,node.data+(i*16));
    memcpy(node.data+(i*16+4),newkey,12);
  }
  node.key_number++;

  if(node.key_number <= 2*DEGREE)
  {
   
   
    write_node(n,&node);
    return 0;
  }
  else
  {
   
   
    struct NODE new_node;
    memset(&new_node,0,sizeof(new_node));
    node_ptr new_id = New_NODE();
    if(node.rsibling)
    {
   
   
      struct NODE rnode;
      memset(&rnode,0,sizeof(rnode));
      fseek(fp,node.rsibling,SEEK_SET);
      fread(&rnode,sizeof(struct NODE),1,fp);
      rnode.lsibling = new_id;
      write_node(node.rsibling,&rnode);
    }
    new_node.rsibling = node.rsibling;
    new_node.is_leaf = node.is_leaf;
    new_node.lsibling = n;
    node.rsibling = new_id;
    if(!node.is_leaf)
    {
   
   
      memcpy(new_node.data,node.data+((DEGREE+1)*16),DEGREE*16+4);
      memset(node.data+((DEGREE+1)*16),0,DEGREE*16+4);
      new_node.key_number = node.key_number = DEGREE;
      memcpy(retkey,node.data+(DEGREE*16+4),12);//UNCLEAR
      //printf("\nretkey:%s\n",retkey);
    }
    else
    {
   
   
      memcpy(new_node.data,node.data+((DEGREE)*16),(DEGREE+1)*16);
      new_node.key_number = DEGREE + 1;
      node.key_number = DEGREE;
      memcpy(retkey,new_node.data+4,12);//UNCLEAR
    }
    
    write_node(n,&node);
    write_node(new_id,&new_node);

    if(n==finfo.root)
    {
   
   
      struct NODE new_root;
      memset(&new_root,0,sizeof(new_root));
      int root_id = New_NODE();
      finfo.root = root_id;
      new_root.is_leaf = 0;
      new_root.key_number = 1;
      new_root.lsibling = new_root.rsibling = 0;
      np2char(n,new_root.data);
      memcpy(new_root.data+4,retkey,12);
      np2char(new_id,new_root.data+16);
      write_node(root_id,&new_root);
    }
    return new_id;
  }
}
B+树删除:

递归到叶子进行删除,每递归一层,从文件里把对应结点的信息读入到内存。

如果删除之后当前结点占有率低于50%,从左右兄弟选一个平均分配一下所存条目,如果两者条目之和能够用一个结点存储,就合并结点。回溯的时候在父亲结点删除合并消去的子节点。

返回是否找到了要删的目标结点。

注意各种细节即可。(长码警告)

int upd;
char updkey[12];
int tree_delete(node_ptr fa,struct NODE *fa_node,node_ptr n,uint32_t loc,node_ptr *childentry)
{
   
   
  struct NODE node;
  node_ptr oldchildenty=0;

  fseek(fp,n,SEEK_SET);
  fread(&node,sizeof(struct NODE),1,fp);

  int i;
  for(i = 0;i < node.key_number;i++ )
  {
   
   
    if(strless(statement.row.b,node.data+(i*16+4)))break;
  }
  int exist=0;

  if(!node.is_leaf)exist = tree_delete(n,&node,char2np(node.data+(i*16)),i,&oldchildenty);
  else exist = i && !strcmp(statement.row.b,node.data+(i*16-12));
  //if(statement.row.b[0]=='z')printf("i:%d key_num:%d exist:%d node:%d\n",i,node.key_number,exist,n);
  if(upd)
  {
   
   
    if(fa && loc)
    {
   
   
      memcpy(fa_node->data+(loc*16-12),updkey,12);
      upd=0;
      write_node(fa,fa_node);
    }
  }
  if(!node.is_leaf && !oldchildenty){
   
   *childentry = 0;return exist;}
  if(node.is_leaf && !exist){
   
   *childentry = 0;return exist;}

  node.key_number--;
  if(oldchildenty)i=oldchildenty;
  if(!i)
  {
   
   
    if(fa && loc)
    {
   
   
      //printf("George node:%d key:%s fakey:%s\n",n,statement.row.b,fa_node->data+(loc*16-12));
        
      memcpy(fa_node->data+(loc*16-12),node.data+4,12);
      write_node(fa,fa_node);
    }
    for(int j=i;j<=node.key_number;j++)
      memcpy(node.data+(j*16),node.data+((j+1)*16),16);
  }
  else
  {
   
   
    for(int j=i;j<=node.key_number;j++)
      if(!node.is_leaf) memcpy(node.data+(j*16-12),node.data+((j+1)*16-12),16);
      else memcpy(node.data+(j*16-16),node.data+((j+1)*16-16
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值