SQLite克隆|第六步:引入B-Tree

上一节中,我们引入了光标的概念,这一切都是为了引入一个重要的数据结构——B-Tree。B-Tree的相关基础知识我们就不再赘述了,感兴趣可以自行百度。
除此之外,我们需要知道,我们为什么要使用树形结构来作为数据库的结构:

  • 搜索特定值的速度很快(对数时间)
  • 插入/删除已寻址的值很快(重新平衡的时间稳定)
  • 快速遍历一类值(与哈希图不同)

节点类型

为此我们首先定义节点类型:

//节点类型
typedef  enum{
    NODE_INTERNAL,
    NODE_LEAF
}NodeType;

节点头

定义了节点类型之后,我们需要设计一下节点头的内容。我们定义一个节点映射一个内存页,节点需要在页面开头存储一些元数据:每个节点将存储它是什么类型的节点,是否是根节点以及指向其父节点的指针(以允许查找节点的同级节点)。我为每个标头字段的大小和偏移量定义常量:

//B-Tree节点头内容(节点单位为页)
const uint32_t NODE_TYPE_SIZE=sizeof(uint8_t);//节点类型所占空间
const uint32_t NODE_TYPE_OFFSET=0;//节点类型偏移量
const uint32_t IS_ROOT_SIZE=sizeof(uint8_t);//根节点标志所占空间
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;//指向父节点指针偏移量
const uint8_t COMMON_NODE_HEADER_SIZE=NODE_TYPE_SIZE+IS_ROOT_SIZE+PARENT_POINTER_SIZE;//节点头所占空间

叶节点格式

上面我们定义的节点头是所有的节点都有的设计模式,然而叶节点还包含一系列的键值对,我们令一个键值对整体为一个胞元,所以我们的叶节点头还需要包含胞元的相关量定义。

//叶节点头设计
const uint32_t LEAF_NODE_NUM_CELLS_SIZE=sizeof(uint32_t);//叶节点内胞元数量所占空间
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;//叶节点头所占空间

此外我们还需要设计叶节点体的结构:

//叶节点体设计
const uint32_t LEAF_NODE_KEY_SIZE=sizeof(uint32_t);//胞元关键词所占空间
const uint32_t LEAF_NODE_KEY_OFFSET=0;//胞元关键词偏移量
const uint32_t LEAF_NODE_VALUE_SIZE=ROW_SIZE;//胞元值所占空间(等于数据行所占空间行)
const uint32_t LEAF_NODE_VALUE_OFFSET=LEAF_NODE_KEY_OFFSET+LEAF_NODE_KEY_SIZE;//胞元值偏移量
const uint32_t LEAF_NODE_CELL_SIZE=LEAF_NODE_KEY_SIZE+LEAF_NODE_VALUE_SIZE;//胞元大小(关键词+值)
const uint32_t LEAF_NODE_SPACE_FOR_CELLS=PAGE_SIZE-COMMON_NODE_HEADER_SIZE;//每一页留给所有胞元的空间
const uint32_t LEAF_NODE_MAX_CELLS=
        LEAF_NODE_SPACE_FOR_CELLS/LEAF_NODE_CELL_SIZE;//最大胞元数量

我们的叶节点格式
上图为我们的叶节点的结构设计。另请注意,最后还有一些浪费的空间。我们在节点头之后存储了尽可能多的胞元,但是剩余的空间无法容纳整个胞元。我们将其保留为空以避免在节点之间拆分单元格。

叶节点访问

当我们设计了叶节点的结构之后,为了以后调用方便,我们需要对叶节点的字段进行访问,我们定义如下几个函数:

//返回节点胞元数量的OFFSET
uint32_t *leaf_node_num_cells(void* node){
    return node+LEAF_NODE_NUM_CELLS_OFFSET;
}

//返回编号为cell_num的胞元的OFFSET
void* leaf_node_cell(void *node,uint32_t cell_num){
    return node+LEAF_NODE_HEADER_SIZE+cell_num*LEAF_NODE_CELL_SIZE;
}

//返回编号为cell_num的胞元的关键词的OFFSET
uint32_t *leaf_node_key(void* node,uint32_t cell_num){
    return leaf_node_cell(node,cell_num);
}

//返回编号为cell_num的胞元的值的OFFSET
void* leaf_node_value(void* node,uint32_t cell_num){
    return leaf_node_cell(node,cell_num)+LEAF_NODE_KEY_SIZE;
}

//初始化一个节点使得节点胞元数量为0
void initialize_leaf_node(void *node){
    *leaf_node_num_cells(node)=0;
}

页面缓存器和数据表

因为我们使用了全新的数据结构,所以页面缓存器和数据表都会作相应调整。首先我们需要对页面缓存器和数据表结构进行调整。

typedef struct{
    int file_descriptor;//文件描述
    uint32_t file_length;//文件长度
    uint32_t num_pages;//页总页数
    void* pages[TABLE_MAX_PAGES];//定义页
}Pager;

//定义数据表结构:行数,页
typedef struct {
    uint32_t root_page_num;//root_page所在页数
    Pager* pager;//包含页面缓存结构
}Table;

对于页面缓存器,由于我们现在不不使用行作为单位,而使用页面作为单元,所以我们的pager_flush函数不需要再存在size参数,同理db_close,pager_open函数也要做相应的调整。

//将pager->pages[page_num]中所有数据(不论是否满页)写入pager->file_descriptor
-void pager_flush(Pager* pager, uint32_t page_num, uint32_t size) {
+void pager_flush(Pager* pager, uint32_t page_num) {
   if (pager->pages[page_num] == NULL) {
     printf("Tried to flush null page\n");
     exit(EXIT_FAILURE);
@@ -242,7 +337,7 @@ void pager_flush(Pager* pager, uint32_t page_num, uint32_t size) {
   }
 
   ssize_t bytes_written =
-      write(pager->file_descriptor, pager->pages[page_num], size);
+      write(pager->file_descriptor, pager->pages[page_num], PAGE_SIZE);
 
   if (bytes_written == -1) {
     printf("Error writing: %d\n", errno);
  
//退出数据库时,将文件保存并将内存空间释放
void db_close(Table* table){
    Pager * pager =table->pager;//提取分页器
    //保存到文件
    for (uint32_t i=0;i<pager->num_pages;i++){
        if(pager->pages[i]==NULL){
            continue;
        }
        pager_flush(pager,i);//将pager->pages[i]中PAGE_SIZE大小的内容存入pager->file_descriptor指向的文件
        free(pager->pages[i]);//释放pager->pages[i]的内存
        pager->pages[i]=NULL;
    }
    //关闭数据库文件
    int result =close(pager->file_descriptor);
    if(result==-1){
        printf("Error closing db file.\n");
        exit(EXIT_FAILURE);
    }
    //多此一举?之前不是释放过了吗
    for (uint32_t i=0;i<TABLE_MAX_PAGES;i++){
        void* page=pager->pages[i];
        if(page){
            free(page);
            pager->pages[i]=NULL;
        }
    }
    free(pager);
    free(table);
}

//初始化页面缓存器
Pager* pager_open(const char* filename){
    int fd=open(filename,O_RDWR|O_CREAT,S_IWUSR|S_IRUSR);//以读写方式打开文件
    //打开失败则答应错误并退出
    if (fd==-1){
        printf("Unable to open file.\n");
        exit(EXIT_FAILURE);
    }
    //计算文件长度
    off_t file_length = lseek(fd,0,SEEK_END);
    //初始化分页器
    Pager* pager = malloc(sizeof(Pager));//初始化页面缓存器
    pager->file_descriptor=fd;//将文件标记符赋值给页面缓存器
    pager->file_length=file_length;//将文件长度赋值给页面缓存器
    pager->num_pages=(file_length/PAGE_SIZE);//计算文件总页数赋值给num_pages
    //如果加载出的文件不是整数个页面长度,则中断读取
    if(file_length%PAGE_SIZE!=0){
        printf("Db file is not a whole number of pages.Corrupt file.\n");
        exit(EXIT_FAILURE);
    }
    for (uint32_t i=0;i<TABLE_MAX_PAGES;i++){
        pager->pages[i]=NULL;
    }
    return pager;
}

光标

之前我们的光标指向数据表中的数据行,当我们引入了B-Tree,我们的光标需要进行调整,变为二维的的结构,使用节点编号和元胞编号定位一个元胞。

typedef  struct {
    Table* table;
    uint32_t page_num;//光标所指向的页编号
    uint32_t cell_num;//光标所指向的元胞编号
    bool end_of_table;//是否表尾的标识
}Cursor;

为了和B-Tree数据结构保持一致,我们也要调整调用光标的相关函数。

//初始化光标(表头)
Cursor* table_start(Table* table){
    Cursor* cursor = malloc(sizeof(Cursor));
    cursor->table=table;
    cursor->page_num=table->root_page_num;//光标指向根节点
    cursor->cell_num=0;//光标指向编号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;
}

//初始化光标(表尾)
Cursor* table_end(Table* table){
    Cursor* cursor=malloc(sizeof(Cursor));
    cursor->table=table;
    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;
}
//返回当前数据行的值
void* cursor_value(Cursor* cursor){
    uint32_t page_num=cursor->page_num;
    void* page=get_page(cursor->table->pager,page_num);//获得pager->page[page_num]所指向的内容首地址
    return leaf_node_value(page,cursor->cell_num);//返回光标指向元胞的值
}

//光标下移到下一个元胞
void cursor_advance(Cursor* cursor){
    uint32_t page_num=cursor->page_num;//获取光标指向页编号
    void* node=get_page(cursor->table->pager,page_num);//获取光标指向页
    cursor->cell_num+=1;//光标指向下一个元胞
    //如果光标指向节点尾,则设置表尾标识为true
    if(cursor->cell_num>=(*leaf_node_num_cells(node))){
        cursor->end_of_table=true;
    }
}

插入叶节点

B-Tree数据结构的基础设计完毕,我们需要考虑数据结构的使用,首先我们在文件打开时就需要进行调整:

//通过文件打开数据表
Table* db_open(const char* filename){
    Pager* pager = pager_open(filename);//将文件读取到页面缓存器
    Table *table=malloc(sizeof(Table));//初始化数据表
    table->pager = pager;//初始化页面缓存器,将文件中值导入
    table->root_page_num=0;//设置表的0编号页为根节点
    //如果是新文件,则初始化使第0页为根节点
    if(pager->num_pages==0){
        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);
    }
    //如果光标指向的编号元胞小于节点当前元胞总数,cell_num开始的元胞全部向后移一格
    if(cursor->cell_num<num_cells){
        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);
        }
    }
    *(leaf_node_num_cells(node))+=1;//节点元胞数量+1
    *(leaf_node_key(node,cursor->cell_num))=key;//将关键词保存到光标指定元胞编号的关键词
    serialize_row(value,leaf_node_value(node,cursor->cell_num));//将value中的元素放到光标指定的元胞处
}

//执行insert指令将输入转码后输出到内存并返回执行状态码
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);
    return EXECUTE_SUCCESS;
}

打印常量

此外,我们新增一个元命令函数用来打印我们的不变常量:

//打印系统常量
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) {
        close_input_buffer(input_buffer);
        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);
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值