上一节中,我们引入了光标的概念,这一切都是为了引入一个重要的数据结构——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);
}
}