SQLite克隆|第八步:拆分叶节点

拆分叶节点

    • 内节点结构
    • 内节点读取和写入
    • 根节点
    • 分割算法
    • 创建新根
    • 树的可视化

内节点结构

目前我们实现了B-Tree基本的数据结构,但是只限于单节点情况,在这种情况下能实现键排序和防止键重复。然而,我们的根节点现在只能存储14行数据,当大于14行时我们设置了错误提示。在本节,我们来实现叶节点的拆分,即生成树的第二层,将根节点转化为内节点。所以第一步,我们需要设计内节点的结构:

//内节点头设计
const uint32_t INTERNAL_NODE_NUM_KEYS_SIZE=sizeof(uint32_t);//内节点关键词数量所占内存
const uint32_t INTERNAL_NODE_NUM_KEYS_OFFSET=COMMON_NODE_HEADER_SIZE;//内节点关键词数量偏移地址
const uint32_t INTERNAL_NODE_RIGHT_CHILD_SIZE=sizeof(uint32_t);//内节点右孩子地址所占内存
const uint32_t INTERNAL_NODE_RIGHT_CHILD_OFFSET=
        INTERNAL_NODE_NUM_KEYS_OFFSET+INTERNAL_NODE_NUM_KEYS_SIZE;//内节点右孩子地址的偏移地址
const uint32_t INTERNAL_NODE_HEADER_SIZE=COMMON_NODE_HEADER_SIZE+
        INTERNAL_NODE_NUM_KEYS_SIZE+
        INTERNAL_NODE_RIGHT_CHILD_SIZE;//内节点头大小

//内节点体设计
const uint32_t INTERNAL_NODE_CHILD_SIZE=sizeof(uint32_t);//孩子指针
const uint32_t INTERNAL_NODE_KEY_SIZE=sizeof(uint32_t);//关键词
const uint32_t INTERNAL_NODE_CELL_SIZE=
        INTERNAL_NODE_CHILD_SIZE+INTERNAL_NODE_KEY_SIZE;//一个元胞由孩子指针和关键词组成

我们的内节点也应该继承普通节点的结构,总的来说,我们设计的内节点应该结构如下图所示:
SQLite克隆|第八步:拆分叶节点_第1张图片
从图中我们可以看到由于每个孩子节点指针/键对都非常小,因此我们可以在每个内部节点中容纳510个键和511个孩子节点指针。这意味着我们将不必遍历树的许多层来找到给定的键。

内节点个数 最大叶节点数量 叶结点大小
0 511^0 = 1 4 KB
1 511^1 = 512 ~2 MB
2 511^2 = 261,121 ~1 GB
3 511^3 = 133,432,831 ~550 GB

同样的,由于孩子指针,键和浪费的空间的开销,我们无法为每个叶节点存储完整的4 KB数据。但是我们可以从磁盘上仅加载4页来搜索500 GB的数据。这就是B树是适用于数据库的数据结构的原因。

内节点读取和写入

为了更方便我们调用内节点,我们为内节点设计了一系列调用函数:

//返回内节点关键词数量的地址
uint32_t *internal_node_num_keys(void *node){
    return node+INTERNAL_NODE_NUM_KEYS_OFFSET;
}

//返回内节点右孩子指针的地址
uint32_t *internal_node_right_child(void* node){
    return node+INTERNAL_NODE_RIGHT_CHILD_OFFSET;
}

//返回内节点中元胞地址
uint32_t *internal_node_cell(void* node,uint32_t cell_num){
    return node+INTERNAL_NODE_HEADER_SIZE+cell_num*INTERNAL_NODE_CELL_SIZE;
}

//返回内节点中孩子节点地址
uint32_t* internal_node_child(void* node,uint32_t child_num){
    uint32_t num_keys=*internal_node_num_keys(node);//获取内节点关键词数量
    //如果孩子编号大于关键词数,返回错误
    if(child_num>num_keys){
        printf("Tried to access child_num %d>num_keys %d\n",child_num,num_keys);
        exit(EXIT_FAILURE);
    }else if(child_num==num_keys){
        return internal_node_right_child(node);//孩子编号和关键词数相等,返回右孩子节点地址
    }else{
        return internal_node_cell(node,child_num);//孩子编号小于关键词数,返回对应元胞地址
    }
}

//返回内节点关键词地址
uint32_t *internal_node_key(void *node,uint32_t key_num){
    return internal_node_cell(node,key_num)+INTERNAL_NODE_CHILD_SIZE;
}

我们需要注意的是,我们的孩子节点指针中存放的是孩子节点所在的页码编号。

根节点

我们设计内节点的结构之后,我们需要考虑到,当我们把一个叶节点拆分后,会生成两个叶节点和一个内节点,在最开始的时候,我们只有一个叶节点,当我们拆分后,叶节点应该一部分变为左孩子,一部分变为右孩子,并生成一个内节点,此时也是根节点。而当我们叶节点达到511个后,再次添加叶节点就会造成内节点拆分并生成新的根节点,所以我们的根节点是动态更新的,我们需要在生成新的内节点时,去跟踪根节点。为此我们也设计了一系列的根节点跟踪函数:

//返回是否为根节点
bool is_node_root(void* node){
    uint8_t value=*((uint8_t*)(node+IS_ROOT_OFFSET));
    return (bool)value;
}

//设置节点是否为根节点
void set_node_root(void *node,bool is_root){
    uint8_t value=is_root;
    *((uint8_t*)(node+IS_ROOT_OFFSET))=value;
}

我们考虑需要调用根节点跟踪函数的函数,我们也要做相应调整:

//初始化一个叶节点并使得节点胞元数量为0
void initialize_leaf_node(void *node){
    set_node_type(node,NODE_LEAF);
    set_node_root(node,false);
    *leaf_node_num_cells(node)=0;
}

//初始化根节点
void initialize_internal_node(void *node){
    set_node_type(node,NODE_INTERNAL);
    set_node_root(node,false);
    *internal_node_num_keys(node)=0;
}

而当我们第一次调用dp_open打开一个新文件时,我们需要设置第0页为根节点:

     // New database file. Initialize page 0 as leaf node.
     void* root_node = get_page(pager, 0);
     initialize_leaf_node(root_node);
+    set_node_root(root_node, true);
   }
 
   return table;

分割算法

如果叶节点没有空间,我们会将该节点上的现有元胞和新元胞(插入)分为两个大小尽量平均的两个节点,按key严格排序,key较小的那部分从左向右排列(当第一次拆分时,作为左孩子),key较大的大部分放在最右,作为右孩子。

//分配新页面
uint32_t get_unused_page_num(Pager* pager) { return pager->num_pages; }

//被划分的两个叶节点大小
const uint32_t LEAF_NODE_RIGHT_SPLIT_COUNT = (LEAF_NODE_MAX_CELLS + 1) / 2;
const uint32_t LEAF_NODE_LEFT_SPLIT_COUNT =
    (LEAF_NODE_MAX_CELLS + 1) - LEAF_NODE_RIGHT_SPLIT_COUNT;

//创建新的叶子结点并分割当前节点元素
void leaf_node_split_and_insert(Cursor* cursor,uint32_t key,Row* value){
    void* old_node=get_page(cursor->table->pager,cursor->page_num);//获取当前节点
    uint32_t new_page_num=get_unused_page_num(cursor->table->pager);//从页面缓存器中获取未使用页面编号
    void *new_node=get_page(cursor->table->pager,new_page_num);//根据未使用的页面编号创建新节点
    initialize_leaf_node(new_node);//初始化新节点为叶子结点
    //将旧节点的元素一分为二,key小的放旧结点,key大的放在新节点
    for(int32_t i=LEAF_NODE_MAX_CELLS;i>=0;i--){
        void* destination_node;
        if(i>=LEAF_NODE_LEFT_SPLIT_COUNT){
            destination_node=new_node;
        }else{
            destination_node=old_node;
        }
        uint32_t index_within_node=i%LEAF_NODE_LEFT_SPLIT_COUNT;//重新定义元胞编号
        void *destination=leaf_node_cell(destination_node,index_within_node);//获取元胞应存入的地址
        if(i==cursor->cell_num){
            serialize_row(value,destination);
        }else if(i>cursor->cell_num){
            memcpy(destination,leaf_node_cell(old_node,i-1),LEAF_NODE_CELL_SIZE);
        }else{
            memcpy(destination,leaf_node_cell(old_node,i),LEAF_NODE_CELL_SIZE);
        }
    }
    //更新新旧节点的元胞数
    *(leaf_node_num_cells(old_node))=LEAF_NODE_LEFT_SPLIT_COUNT;
    *(leaf_node_num_cells(new_node))=LEAF_NODE_RIGHT_SPLIT_COUNT;
    //需要更新节点的父节点
    if(is_node_root(old_node)){
        return create_new_root(cursor->table,new_page_num);//若旧结点为根节点,则创建新节点充当父节点
    } else{
        printf("Need to implement updating parent after split.\n");
        exit(EXIT_FAILURE);
    }
}

创建新根

在上一个部分中,有一个create_new_root()函数我们还没有定义,它的功能是创建新根。首先我们明确一下我们的函数应该要实现的功能:在上一个部分中,我们实现了创建新节点并将旧结点分为两部分,key小的部分存入旧节点,key大的部分存入新节点。在create_new_root()中,我们希望将之前的根节点中的内容存到新定义的一个节点,然后将根页面初始化为具有两个子节点的新内部节点:

//创建新的根节点,并指向左右孩子
void create_new_root(Table* table,uint32_t right_child_page_num){
    //初始化左右孩子
    void* root=get_page(table->pager,table->root_page_num);
    void *right_child=get_page(table->pager,right_child_page_num);
    uint32_t left_child_page_num=get_unused_page_num(table->pager);
    void* left_child=get_page(table->pager,left_child_page_num);
    memcpy(left_child,root,PAGE_SIZE);//把根节点复制到左孩子
    set_node_root(left_child, false);
    //将根页面初始化为具有两个子节点的新内部节点
    initialize_internal_node(root);
    set_node_root(root,true);
    *internal_node_num_keys(root)=1;//关键词数量为1
    *internal_node_child(root,0)=left_child_page_num;//编号为0的孩子节点为left_child
    uint32_t left_child_max_key=get_node_max_key(left_child);//获取左孩子最大关键词
    *internal_node_key(root,0)=left_child_max_key;//设置root编号为0的关键词为左孩子最大关键词
    *internal_node_right_child(root)=right_child_page_num;//设置右孩子指针(以页码为标志)
}

树的可视化

之前我们由于单节点,所以打印树只是简单的面向单节点,在这么我们引入了新的节点结构——内节点,所以我们还需要调整树的可视化函数:

//树的可视化
void indent(uint32_t level){
    for (uint32_t i=0;i<level;i++){
        printf(" ");
    }
}
void print_tree(Pager* pager,uint32_t page_num,uint32_t indentation_level){
    void *node=get_page(pager,page_num);
    uint32_t num_keys,child;
    switch (get_node_type(node)) {
        case (NODE_LEAF):
            num_keys=*leaf_node_num_cells(node);//获取关键词数量
            indent(indentation_level);
            printf("- leaf (size %d)\n",num_keys);//打印关键词数量
            //打印每一个关键词
            for(uint32_t i=0;i<num_keys;i++){
                indent(indentation_level+1);
                printf("- %d\n",*leaf_node_key(node,i));
            }
            break;
        case (NODE_INTERNAL):
            num_keys=*internal_node_num_keys(node);//获取关键词数量
            indent(indentation_level);
            printf("- internal (size %d)\n",num_keys);//打印关键词数量
            //先打印左节点,再打印关键词
            for(uint32_t i=0;i<num_keys;i++){
                child=*internal_node_child(node,i);//孩子节点指向页数
                print_tree(pager,child,indentation_level+1);
                indent(indentation_level+1);
                printf("- key %d\n",*internal_node_key(node,i));
            }
            //最后打印右节点
            child=*internal_node_right_child(node);
            print_tree(pager,child,indentation_level+1);
            break;
    }
}

可以注意到,我们在insert数据元胞时调用了如下代码:

Cursor *table_find(Table* table,uint32_t key){
    uint32_t root_page_num=table->root_page_num;//初始化根节点编号
    void* root_node=get_page(table->pager,root_page_num);//初始化根节点
    if(get_node_type(root_node)==NODE_LEAF){
        return leaf_node_find(table,root_page_num,key);//返回元胞插入位置
    }else{
        printf("Need to implement searching an internal node.\n");
        exit(EXIT_FAILURE);
    }
}

也就是说目前为止,我们只可以实现在单节点情况下insert元胞,一旦根节点不是叶节点便错误退出。所以在下一步,我们需要调整insert函数。

你可能感兴趣的:(SQLite克隆|第八步:拆分叶节点)