SQLite克隆|第九步:树的递归搜索

上一节我们说到当根节点变为内节点后无法再插入数据,为了解决这个问题,我们使用递归搜索来解决这个问题,具体而言,从根节点出发二分查找根节点的关键词,如果遇上相等的,再去查找它的孩子,如果孩子仍为内节点,就继续递归查找键所对应元胞的光标(cursor);否则,如果孩子为叶子结点,则直接返回该叶子节点对应key的元胞的cursor:

//二分搜索键对应的胞元所在位置(cursor)
Cursor *internal_node_find(Table* table,uint32_t page_num,uint32_t key){
    void* node = get_page(table->pager,page_num);//获取内节点
    uint32_t num_keys=*internal_node_num_keys(node);//获取内节点关键词数量
    uint32_t min_index=0;
    uint32_t max_index=num_keys;
    //二分搜索
    while(min_index!=max_index){
        uint32_t index = (min_index+max_index)/2;
        uint32_t key_to_right=*internal_node_key(node,index);
        if(key_to_right>=key){
            max_index=index;
        } else{
            min_index=index+1;
        }
    }

    uint32_t child_num=*internal_node_child(node,min_index);//返回孩子节点所在页数
    void* child =get_page(table->pager,child_num);//读取对应孩子节点
    //如果孩子节点为叶子结点返回对应key所在胞元编号,如果是内节点,则递归搜索直到找到叶子结点
    switch (get_node_type(child)) {
        case NODE_LEAF:
            return leaf_node_find(table,child_num,key);
        case NODE_INTERNAL:
            return internal_node_find(table,child_num,key);
    }
}

直到这,我们的数据库已经可以支持两层的B-Tree储存,然而当第二层叶子结点已经满载的情况下,再去添加一个数据元胞时,就会报错。这是因为在我们的代码中有这么几行:

//创建新的叶子结点并分割当前节点元素
void leaf_node_split_and_insert(Cursor* cursor,uint32_t key,Row* value){
     //...省略...
    //需要更新节点的父节点
    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);
    }
}

也就是说当我们添加数据时会首先判断该节点是否为根节点,如果是根节点,才可以继续拆分,也就是说,目前为止我们只能够实现1+2:一个根节点两个满载的叶节点。我们后续会解决这个问题。但是首先,我们想更新一下我们的select函数,因为我们的select还是使用行来遍历需要显示的数据:

ExecuteResult execute_select(Statement* statement, Table* table) {
    Row row;
    Cursor* cursor=table_start(table);
    while(!(cursor->end_of_table)){
        deserialize_row(cursor_value(cursor),&row);
        print_row(&row);
        cursor_advance(cursor);
    }
    free(cursor);
    return EXECUTE_SUCCESS;
}

这会导致一个什么问题呢?光标每次读完一个数据便下移一行,然而当我们的叶节点拆分之后,再从根节点打印数据显然是错误的。所以首先我们的table_start()就不可以定位在根节点的开头,而是定位在最左子节点的开头。让我们先重写一下table_start()函数:

//初始化光标(表头)
Cursor* table_start(Table* table){
    Cursor *cursor=table_find(table,0);//查找key为0的光标(即使不存在也会返回开头的光标)

    void *node = get_page(table->pager,cursor->page_num);
    uint32_t num_cells =*leaf_node_num_cells(node);
    cursor->end_of_table=(num_cells==0);
    return cursor;
}

当我们修改完该函数时,我们会发现也只能实现显示最左节点内的数据单元。要扫描整个表,我们需要在遍历完第一个叶节点之后跳到第二个叶子节点。为此,我们将在名为“ next_leaf”的叶节点头中保存一个新字段,该字段将在右侧保留叶的兄弟节点的页码。最右边的叶子节点的next_leaf值为0,表示没有兄弟姐妹(页面0始终为表的根节点保留)。叶节点设计修改如下:

//叶节点头设计
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_NEXT_LEAF_SIZE=sizeof(uint32_t);//兄弟节点所在页码指针所占空间
const uint32_t LEAF_NODE_NEXT_LEAF_OFFSET=LEAF_NODE_NUM_CELLS_OFFSET+LEAF_NODE_NUM_CELLS_SIZE;//兄弟节点所在页码指针偏移量
const uint32_t LEAF_NODE_HEADER_SIZE=
        COMMON_NODE_HEADER_SIZE+
        LEAF_NODE_NUM_CELLS_SIZE+
        LEAF_NODE_NEXT_LEAF_SIZE;//叶节点头所占空间

//返回下一个兄弟节点的页码
uint32_t *leaf_node_next_leaf(void *node){
    return node+LEAF_NODE_NEXT_LEAF_OFFSET;
}

//初始化一个叶节点
void initialize_leaf_node(void *node){
    set_node_type(node,NODE_LEAF);
    set_node_root(node,false);
    *leaf_node_next_leaf(node)=0;
    *leaf_node_num_cells(node)=0;
}

void leaf_node_split_and_insert(Cursor* cursor,uint32_t key,Row* value){
	//...省略...
    *leaf_node_next_leaf(new_node)=*leaf_node_next_leaf(old_node);//新节点的兄弟为旧节点的兄弟
    *leaf_node_next_leaf(old_node)=new_page_num;//旧结点的兄弟为新节点

此外,我们还需要对调用光标的函数进行修改。我们之前的光标的cursor_advance()函数是将光标指向下一个元胞,如果读取元胞编号大于等于该节点元胞总数,则设置end_of_table标志为true。而我们现在想要它在读取完一个节点之后去读取它的兄弟节点:

//光标下移到下一个元胞
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))){
        uint32_t next_page_num=*leaf_node_next_leaf(node);
        //如果没有兄弟节点,则读取结束
        //如果有兄弟节点,则相应赋值光标
        if(next_page_num==0){
            cursor->end_of_table=true;
        } else{
            cursor->page_num=next_page_num;
            cursor->cell_num=0;
        }
    }
}

你可能感兴趣的:(SQLite克隆|第九步:树的递归搜索)