實(shí)現(xiàn)自己的數(shù)據(jù)庫(kù)四

一前言

上一篇已經(jīng)說明了B+樹的一些原理是目,也講到,我們目前采用的持久化數(shù)據(jù)的方式标捺,而且我們是單獨(dú)的插入數(shù)據(jù)懊纳,沒有任何元數(shù)據(jù)信息,雖然插入的速度很快宜岛,因?yàn)槭遣捎米芳拥姆绞匠び弧5沁@種方式插入速度很快,像上次所說萍倡,查詢和刪除的速度會(huì)很慢身弊。

數(shù)據(jù)結(jié)構(gòu)性能對(duì)比圖

我們以前用的就是非排序數(shù)組行,保存的是數(shù)據(jù)列敲,沒有其他信息阱佛。插入性能最好,但是刪除和查找時(shí)間復(fù)雜度為O(n),排序數(shù)組查找很快戴而,可以采用二分法查找凑术,時(shí)間復(fù)雜度為O(log(n)),但是插入和刪除時(shí)間復(fù)雜度為O(n),而采用B+樹方式所意,且保存了元數(shù)據(jù)和主鍵的情況下淮逊,無(wú)論是查找、插入還是刪除扶踊,性能都達(dá)到了均衡泄鹏。

二 改造

2.1 元數(shù)據(jù)信息

采用樹的方式來保存數(shù)據(jù)庫(kù)的數(shù)據(jù)時(shí)候,就不能是簡(jiǎn)單的只記錄原始信息秧耗,還需要記錄諸如子節(jié)點(diǎn)指針信息备籽,
為了方便遍歷到兄弟節(jié)點(diǎn),還需要保存這指向父節(jié)點(diǎn)的指針信息(這里面的指針類似c語(yǔ)言的指針分井,在磁盤保存時(shí)候车猬,要看具體的實(shí)現(xiàn)霉猛,可能是個(gè)頁(yè)號(hào))。


拆分根節(jié)點(diǎn)

同樣我們?yōu)榱藚^(qū)分子節(jié)點(diǎn)和葉子節(jié)點(diǎn)珠闰,需要保存節(jié)點(diǎn)的類別惜浅,以及是否為root節(jié)點(diǎn)這些信息。

用這種排序的樹保存數(shù)據(jù)伏嗜,還有個(gè)好處赡矢,就是遍歷比較方便。

節(jié)點(diǎn)類型

typedef enum { NODE_INTERNAL, NODE_LEAF } NodeType;

root節(jié)點(diǎn)元數(shù)據(jù)

/*
 * Common Node Header Layout
 */
// 節(jié)點(diǎn)類型數(shù)據(jù)的大小阅仔,其實(shí)只有一個(gè)bit就可以區(qū)分葉子節(jié)點(diǎn)和根節(jié)點(diǎn),這里面浪費(fèi)了點(diǎn)
const uint32_t NODE_TYPE_SIZE = sizeof(uint8_t);
// 節(jié)點(diǎn)類型的偏移弧械,放在頁(yè)節(jié)點(diǎn)的開頭
const uint32_t NODE_TYPE_OFFSET = 0;
// 是否為root的元數(shù)據(jù)大小
const uint32_t IS_ROOT_SIZE = sizeof(uint8_t);
// 是否為root的元數(shù)據(jù)的偏移量
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;
// 整個(gè)Node節(jié)點(diǎn)的元數(shù)據(jù)整體尺寸
const uint8_t COMMON_NODE_HEADER_SIZE =
  NODE_TYPE_SIZE + IS_ROOT_SIZE + PARENT_POINTER_SIZE;

除了root節(jié)點(diǎn)外八酒,就是葉子節(jié)點(diǎn),葉子節(jié)點(diǎn)保存完整的數(shù)據(jù)信息刃唐,所以和root節(jié)點(diǎn)的內(nèi)容有所不同羞迷。

/*
 * Leaf Node Header Layout
 */
// 葉子節(jié)點(diǎn)保存的cell數(shù)量,一個(gè)cell由key和value組成 可以看作一個(gè)key后面跟著持久化的行
const uint32_t LEAF_NODE_NUM_CELLS_SIZE = sizeof(uint32_t);
// 葉子節(jié)點(diǎn)的cell數(shù)量的偏移量
const uint32_t LEAF_NODE_NUM_CELLS_OFFSET = COMMON_NODE_HEADER_SIZE;
// 葉子節(jié)點(diǎn)的元數(shù)據(jù)大小
const uint32_t LEAF_NODE_HEADER_SIZE =
  COMMON_NODE_HEADER_SIZE + LEAF_NODE_NUM_CELLS_SIZE;

葉子節(jié)點(diǎn)保存的cell數(shù)量画饥,一個(gè)cell由key和value組成 可以看作一個(gè)key后面跟著持久化的行衔瓮。


葉子節(jié)點(diǎn)的結(jié)構(gòu)

示意圖很清楚說明了第一個(gè)字節(jié)是node_type,接著一個(gè)字節(jié)是is_root,后面四個(gè)字節(jié)是父節(jié)點(diǎn)的指針,再下面4個(gè)字節(jié)為cell的數(shù)量抖甘,這里面有個(gè)錯(cuò)誤就是寫了兩個(gè)热鞍,剩下內(nèi)容就是cell,即key+value衔彻,都是這樣部署的薇宠,Node節(jié)點(diǎn)不夠存cell的就浪費(fèi)了。

訪問葉子節(jié)點(diǎn)方法

// 葉子節(jié)點(diǎn)中cell數(shù)量地址獲取
uint32_t* leaf_node_num_cells(void* node) {
return node + LEAF_NODE_NUM_CELLS_OFFSET;
}
// 葉子節(jié)點(diǎn)上第cell_num個(gè)cell的偏移量的地址
void* leaf_node_cell(void* node, uint32_t cell_num) {
 return node + LEAF_NODE_HEADER_SIZE + cell_num * LEAF_NODE_CELL_SIZE;
}
// 葉子節(jié)點(diǎn)上第cell_num個(gè)key的偏移量艰额,因?yàn)閏ell的前面放的是key
uint32_t* leaf_node_key(void* node, uint32_t cell_num) {
  return leaf_node_cell(node, cell_num);
}
// 葉子節(jié)點(diǎn)上第cell_num個(gè)cell的value地址獲取
void* leaf_node_value(void* node, uint32_t cell_num) {
 return leaf_node_cell(node, cell_num) + LEAF_NODE_KEY_SIZE;
}
// 初始化一個(gè)節(jié)點(diǎn)澄港,將cell_num設(shè)置為0
void initialize_leaf_node(void* node) { *leaf_node_num_cells(node) = 0; }

2.2 Table和Pager的改動(dòng)

首先整個(gè)設(shè)計(jì)考慮簡(jiǎn)單點(diǎn),不支持部分頁(yè)面的讀取柄沮,每次讀取是讀取整個(gè)頁(yè)面回梧。

 const uint32_t PAGE_SIZE = 4096;
 const uint32_t TABLE_MAX_PAGES = 100;
 
 
 typedef struct {
   int file_descriptor;
   uint32_t file_length;
+  uint32_t num_pages;
   void* pages[TABLE_MAX_PAGES];
 } Pager;
 
 typedef struct {
   Pager* pager;
-  uint32_t num_rows;
+  uint32_t root_page_num;
 } Table;

在新的定義中,我們固定了每個(gè)表的最大行數(shù)祖搓。另外我們將page的數(shù)量保存在Pager中狱意。在Table中保存根頁(yè)面的page_num,這樣我們就可以通過表方便找到root頁(yè)面了棕硫。

下面是一些關(guān)鍵行數(shù)的修改:

void* get_page(Pager* pager, uint32_t page_num) {
     pager->pages[page_num] = page;
    if (page_num >= pager->num_pages) {
     pager->num_pages = page_num + 1;
    }
   return pager->pages[page_num];
  }

///////////////////////////////////////////
Pager* pager_open(const char* filename) {
   Pager* pager = malloc(sizeof(Pager));
   pager->file_descriptor = fd;
   pager->file_length = file_length;
  pager->num_pages = (file_length / PAGE_SIZE);
 
  if (file_length % PAGE_SIZE != 0) {
     printf("Db file is not a whole number of pages. Corrupt file.\n");
    exit(EXIT_FAILURE);
   }
 

2.3 游標(biāo)的更改

游標(biāo)定位數(shù)據(jù)髓涯,以前通過行來定位,現(xiàn)在通過頁(yè)面號(hào)和cell號(hào)來定位數(shù)據(jù)哈扮。

 typedef struct {
   Table* table;
-  uint32_t row_num;
+  uint32_t page_num;
+  uint32_t cell_num;
   bool end_of_table;  // Indicates a position one past the last element
 } Cursor;

游標(biāo)創(chuàng)建:

 Cursor* table_start(Table* table) {
   Cursor* cursor = malloc(sizeof(Cursor));
   cursor->table = table;
-  cursor->row_num = 0;
-  cursor->end_of_table = (table->num_rows == 0);
+  cursor->page_num = table->root_page_num;
+  cursor->cell_num = 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;
 }

表開始的游標(biāo)定位纬纪,游標(biāo)的頁(yè)面為表的根頁(yè)面編號(hào)蚓再,通過leaf_node_num_cells行數(shù)獲取root節(jié)點(diǎn)中cell的數(shù)量。

 Cursor* table_end(Table* table) {
   Cursor* cursor = malloc(sizeof(Cursor));
   cursor->table = table;
-  cursor->row_num = table->num_rows;
+  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;
 }

這個(gè)是表的結(jié)束頁(yè)的游標(biāo)包各,設(shè)置end_of_table的標(biāo)志摘仅。

 void* cursor_value(Cursor* cursor) {
-  uint32_t row_num = cursor->row_num;
-  uint32_t page_num = row_num / ROWS_PER_PAGE;
+  uint32_t page_num = cursor->page_num;
   void* page = get_page(cursor->table->pager, page_num);
-  uint32_t row_offset = row_num % ROWS_PER_PAGE;
-  uint32_t byte_offset = row_offset * ROW_SIZE;
-  return page + byte_offset;
+  return leaf_node_value(page, cursor->cell_num);
 }

獲取游標(biāo)處的值,獲取cell_num的value问畅,由于每個(gè)cell的大小一樣娃属,所以也是類似的取值方法。
游標(biāo)的遞增:

 void cursor_advance(Cursor* cursor) {
-  cursor->row_num += 1;
-  if (cursor->row_num >= cursor->table->num_rows) {
+  uint32_t page_num = cursor->page_num;
+  void* node = get_page(cursor->table->pager, page_num);
+
+  cursor->cell_num += 1;
+  if (cursor->cell_num >= (*leaf_node_num_cells(node))) {
     cursor->end_of_table = true;
   }
 }

這里面每次遞增护姆,只增加cell的數(shù)量矾端,也許你會(huì)說cell遞增到最后一個(gè)cell怎么辦,其實(shí)增加到最后到最后一個(gè)cell后卵皂,設(shè)置了表結(jié)束的標(biāo)志秩铆,循環(huán)退出了。

數(shù)據(jù)庫(kù)打開
打開數(shù)據(jù)庫(kù)灯变,如果是一個(gè)新的數(shù)據(jù)庫(kù)殴玛,初始化一個(gè)頁(yè)面作為葉子節(jié)點(diǎn)。

Table* db_open(const char* filename) {
   Pager* pager = pager_open(filename);
   Table* table = malloc(sizeof(Table));
   table->pager = pager;
  table->root_page_num = 0;

  if (pager->num_pages == 0) {
    // New database file. Initialize page 0 as leaf node.
    void* root_node = get_page(pager, 0);
    initialize_leaf_node(root_node);
  }
 
   return table;
 }

下面是個(gè)關(guān)鍵行數(shù)添祸,先葉子節(jié)點(diǎn)插入數(shù)據(jù):

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) {
    // 節(jié)點(diǎn)滿了
    printf("Need to implement splitting a leaf node.\n");
   exit(EXIT_FAILURE);
  }

  if (cursor->cell_num < num_cells) {
    // 為一個(gè)cell騰出位置位置
  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);
    }
  }
// 增加cell_num,設(shè)置key和持久化row滚粟。
  *(leaf_node_num_cells(node)) += 1;
  *(leaf_node_key(node, cursor->cell_num)) = key;
  serialize_row(value, leaf_node_value(node, cursor->cell_num));
}

這個(gè)函數(shù)假設(shè)樹只有一個(gè)頁(yè)面,目前版本先不支持多個(gè)頁(yè)面。
插入操作:

 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);
}

三 打印命令

打印meta信息即元數(shù)據(jù)信息刃泌。

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) {
     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;
   }

沒啥需要特別說明的凡壤,只是支持一個(gè)打印常量元數(shù)據(jù)的命令。

四 樹的可視化

為了幫助調(diào)試蔬咬,增加打印樹的功能:

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);
  }
}

遍歷節(jié)點(diǎn)鲤遥,打印cell信息。增加meta命令:

MetaCommandResult do_meta_command(InputBuffer* input_buffer, Table* table) {
   if (strcmp(input_buffer->buffer, ".exit") == 0) {
     db_close(table);
     exit(EXIT_SUCCESS);
  } else if (strcmp(input_buffer->buffer, ".btree") == 0) {
    printf("Tree:\n");
    print_leaf_node(get_page(table->pager, 0));
   return META_COMMAND_SUCCESS;
   } else if (strcmp(input_buffer->buffer, ".constants") == 0) {
     printf("Constants:\n");
     print_constants();
     return META_COMMAND_SUCCESS;
   } else {
     return META_COMMAND_UNRECOGNIZED_COMMAND;
   }

這次最大的改動(dòng)是文件的存儲(chǔ)結(jié)構(gòu)改變林艘,改成B+樹方式盖奈,但是我們并沒有對(duì)文件里面的cell按照key進(jìn)行排序,而且我們只支持一個(gè)頁(yè)面狐援,不過仍然是重大的進(jìn)步钢坦,慢慢來吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末啥酱,一起剝皮案震驚了整個(gè)濱河市爹凹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌镶殷,老刑警劉巖禾酱,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡颤陶,警方通過查閱死者的電腦和手機(jī)颗管,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來滓走,“玉大人垦江,你說我怎么就攤上這事〗练剑” “怎么了比吭?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)姨涡。 經(jīng)常有香客問我衩藤,道長(zhǎng),這世上最難降的妖魔是什么涛漂? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任慷彤,我火速辦了婚禮,結(jié)果婚禮上怖喻,老公的妹妹穿的比我還像新娘。我一直安慰自己岁诉,他們只是感情好锚沸,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著涕癣,像睡著了一般哗蜈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上坠韩,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天距潘,我揣著相機(jī)與錄音,去河邊找鬼只搁。 笑死音比,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的氢惋。 我是一名探鬼主播洞翩,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼焰望!你這毒婦竟也來了骚亿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤熊赖,失蹤者是張志新(化名)和其女友劉穎来屠,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡俱笛,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年捆姜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嫂粟。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡娇未,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出星虹,到底是詐尸還是另有隱情零抬,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布宽涌,位于F島的核電站平夜,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏卸亮。R本人自食惡果不足惜忽妒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望兼贸。 院中可真熱鬧段直,春花似錦、人聲如沸溶诞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)螺垢。三九已至喧务,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間枉圃,已是汗流浹背功茴。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留孽亲,地道東北人坎穿。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像返劲,于是被迫代替她去往敵國(guó)和親赁酝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容