上一节我们说到当根节点变为内节点后无法再插入数据,为了解决这个问题,我们使用递归搜索来解决这个问题,具体而言,从根节点出发二分查找根节点的关键词,如果遇上相等的,再去查找它的孩子,如果孩子仍为内节点,就继续递归查找键所对应元胞的光标(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;
}
}
}