实验内容
已有一个数据库 myjql 的框架,现在在这个框架中写一棵组织在文件里的 B+ 树,以此实现数据库基本功能。
数据库文件要可持久化存储,也就说退出数据库后会留下数据库文件,再次打开时数据不会丢失。
数据库中每个条目由 16 个字节组成:A(int) + B(12字节字符串)。
按照 B 为关键字建立索引。
要求支持如下操作:
select: 列出所有的元组select s: 列出所有 B 为 s 的元组insert i s: 插入一个A = i,B = 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

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

3286

被折叠的 条评论
为什么被折叠?



