C++实现简易数据库
文章目錄
- 前言
- 一、實(shí)現(xiàn)REPL
- 二、SQL的解析前端
- 1.SQL解析
- 2.實(shí)現(xiàn)一個(gè)虛擬機(jī)
- 設(shè)計(jì)儲(chǔ)存結(jié)構(gòu)
- 執(zhí)行儲(chǔ)存結(jié)構(gòu)
- 如何實(shí)現(xiàn)一個(gè)分頁
- 實(shí)現(xiàn)光標(biāo)
- BTREE是什么
- 實(shí)現(xiàn)Btree
- 總結(jié)
前言
參考網(wǎng)上的數(shù)據(jù)庫開發(fā)資料,將代碼及實(shí)現(xiàn)邏輯梳理,經(jīng)過幾步踩坑,最終編譯通過(Windows平臺(tái)),實(shí)驗(yàn)成功。
git地址:代碼
可執(zhí)行文件:下載
測試命令 ,寫入模擬數(shù)據(jù)
打印btree節(jié)點(diǎn)
.btree一、實(shí)現(xiàn)REPL
REPL(Read-Eval-Print Loop,簡稱REPL) “讀取-求值-輸出”循環(huán), 也被稱做交互式頂層構(gòu)件,是一個(gè)簡單的交互式的編程環(huán)境。本實(shí)例啟動(dòng)無限循環(huán),接受輸入字符串,實(shí)現(xiàn)交互式窗口
二、SQL的解析前端
1.SQL解析
并單獨(dú)封裝一個(gè)do_meta_command函數(shù)來處理它。
以.開頭的非sql語句稱作元命令 (meta command) 所以我們?cè)谝婚_始就檢查是否以其開頭
sql語句目前僅定義了如下簡單的兩種狀態(tài)字節(jié)碼
enum StatementType { STATEMENT_INSERT, STATEMENT_SELECT };
同時(shí)再將上一步成功轉(zhuǎn)化后得到的statement交給虛擬機(jī)進(jìn)行解析。
2.實(shí)現(xiàn)一個(gè)虛擬機(jī)
根據(jù)得到的statement讓虛擬機(jī)偽執(zhí)行一下對(duì)應(yīng)sql語句的操作效果。
void DB::excute_statement(Statement &statement) {switch (statement.type){case STATEMENT_INSERT:std::cout << "Executing insert statement" << std::endl;break;case STATEMENT_SELECT:std::cout << "Executing select statement" << std::endl;break;} } void DB::start() {while (true){print_prompt();std::string input_line;std::getline(std::cin, input_line);if (parse_meta_command(input_line)){continue;}Statement statement;if (parse_statement(input_line, statement)){continue;}execute_statement(statement);} }設(shè)計(jì)儲(chǔ)存結(jié)構(gòu)
只支持單表 user ,目前規(guī)定所儲(chǔ)存的類型結(jié)構(gòu)如下
| id | 整型 (integer) |
| username | 可變字符串 (varchar 32) |
| 可變字符串 (varchar 255) |
如果將一行行的數(shù)據(jù)了,合理的儲(chǔ)存起來呢?在實(shí)現(xiàn)Btree前,先選擇將它分組到 頁面 ***(Page)***當(dāng)中去,然后將這些頁面以數(shù)組的形式排列。此外,我們需要在一頁中,盡可能的將其緊密排列,意味著數(shù)據(jù)應(yīng)該一個(gè)挨著一個(gè)。
| id | 4 | 0 |
| username | 32 | 4 |
| 255 | 36 | |
| 總計(jì) | 291 |
通過實(shí)現(xiàn)序列化 (serialize) 以及反序列化 (serialize) 來達(dá)成該目的。
同時(shí)注意到我們這里寫了一個(gè)(char *)的強(qiáng)制轉(zhuǎn)化類型,是為了讓編譯器明白,偏移量 (offset) 是以單個(gè)字節(jié) (bytes) 為單位的
接下來,創(chuàng)建的Table來儲(chǔ)存這些分頁。同時(shí)與大多數(shù)計(jì)算機(jī)系統(tǒng)一樣,將其設(shè)置為4k大小。
#define TABLE_MAX_PAGES 100 const uint32_t PAGE_SIZE = 4096; const uint32_t ROWS_PER_PAGE = PAGE_SIZE / ROW_SIZE; const uint32_t TABLE_MAX_ROWS = ROWS_PER_PAGE * TABLE_MAX_PAGES; class Table { public:uint32_t num_rows;void *pages[TABLE_MAX_PAGES];Table(){num_rows = 0;for (uint32_t i = 0; i < TABLE_MAX_PAGES; i++){pages[i] = NULL;}}~Table(){for (int i = 0; pages[i]; i++){free(pages[i]);}} };外還應(yīng)該知道,表頁該從何處開始讀寫。
void *row_slot(Table &table, uint32_t row_num) {uint32_t page_num = row_num / ROWS_PER_PAGE;void *page = table.pages[page_num];if (page == NULL){// Allocate memory only when we try to access pagepage = table.pages[page_num] = malloc(PAGE_SIZE);}uint32_t row_offset = row_num % ROWS_PER_PAGE;uint32_t byte_offset = row_offset * ROW_SIZE;return (char *)page + byte_offset; }執(zhí)行儲(chǔ)存結(jié)構(gòu)
對(duì)于insert操作,我們首先判斷它是否超出儲(chǔ)存限制,針對(duì)操作執(zhí)行結(jié)果,我們同樣添加了與我們之前類似的枚舉類狀態(tài)碼
enum ExecuteResult { EXECUTE_SUCCESS, EXECUTE_TABLE_FULL };
之后,若滿足對(duì)應(yīng)條件,我們尋找到合適的內(nèi)存插入位置,將我們輸入的行以serialize_row的方式填充到內(nèi)存page當(dāng)中。
類似的對(duì)于select操作,我們僅需從page中對(duì)應(yīng)位置通過deserialize_row的方式獲取到即可。
ExecuteResult DB::execute_select(Statement &statement, Table &table) {for (uint32_t i = 0; i < table.num_rows; i++){Row row;void *page = row_slot(table, i);deserialize_row(page, row);std::cout << "(" << row.id << ", " << row.username << ", " << row.email << ")" << std::endl;}return EXECUTE_SUCCESS; }最后將我們所設(shè)計(jì)好的操作交給虛擬機(jī)來執(zhí)行即可。
void DB::execute_statement(Statement &statement, Table &table) {ExecuteResult result;switch (statement.type){case STATEMENT_INSERT:result = execute_insert(statement, table);break;case STATEMENT_SELECT:result = execute_select(statement, table);break;}switch (result){case EXECUTE_SUCCESS:std::cout << "Executed." << std::endl;break;case EXECUTE_TABLE_FULL:std::cout << "Error: Table full." << std::endl;break;} }void DB::start() {Table table;while (true){print_prompt();std::string input_line;std::getline(std::cin, input_line);if (parse_meta_command(input_line)){continue;}Statement statement;if (parse_statement(input_line, statement)){continue;}execute_statement(statement, table);} }如何實(shí)現(xiàn)一個(gè)分頁
現(xiàn)在將Table中 void *pages[TABLE_MAX_PAGES] 遷移到Pager;
class Pager { public:int file_descriptor;uint32_t file_length;void *pages[TABLE_MAX_PAGES];Pager(const char *filename);void *get_page(uint32_t page_num);void pager_flush(uint32_t page_num, uint32_t size); };構(gòu)造這個(gè)Pager對(duì)象。
Pager::Pager(const char *filename) {file_descriptor = open(filename,O_RDWR | // Read/Write modeO_CREAT, // Create file if it does not existS_IWUSR | // User write permissionS_IRUSR // User read permission);if (file_descriptor < 0){std::cerr << "Error: cannot open file " << filename << std::endl;exit(EXIT_FAILURE);}file_length = lseek(file_descriptor, 0, SEEK_END);for (uint32_t i = 0; i < TABLE_MAX_PAGES; i++){pages[i] = nullptr;} }我們看到,我們創(chuàng)建了一個(gè)新的file_descriptor用作我們物理磁盤上儲(chǔ)存交互,并且設(shè)置了file_length屬性來獲取其文件大小。
此外我們?cè)谶@當(dāng)中添加了一個(gè)get_page函數(shù)來作用于row_slot當(dāng)中,用于獲取指定頁的內(nèi)存。邏輯依舊十分簡單,如果我們沒有獲取到頁面,我們就創(chuàng)建一個(gè)新的頁面,并且將其存儲(chǔ)到pages數(shù)組中。
實(shí)現(xiàn)光標(biāo)
顯然我們現(xiàn)在要指向table開頭/結(jié)尾,所以我們需要實(shí)現(xiàn)一個(gè)cursor,它可以指向table開頭,也可以指向table結(jié)尾。注意我們即然使用了cursor,也即指向這個(gè)詞,我們?cè)诖颂幨褂玫木褪侵羔?#xff0c;使用的其實(shí)一直就是DB::table唯一對(duì)象。
class Cursor { public:Table *table;uint32_t row_num;bool end_of_table;Cursor(Table *&table, bool option);void *cursor_value();void cursor_advance(); }; Cursor::Cursor(Table *&table, bool option) {this->table = table;if (option){// start at the beginning of the tablerow_num = 0;end_of_table = (table->num_rows == 0);}else{// end of the tablerow_num = table->num_rows;end_of_table = true;} }同時(shí)我們將不再使用row_slot這個(gè)函數(shù),而轉(zhuǎn)為使用cursor_value函數(shù)。
void *Cursor::cursor_value() {uint32_t page_num = row_num / ROWS_PER_PAGE;void *page = table->pager.get_page(page_num);uint32_t row_offset = row_num % ROWS_PER_PAGE;uint32_t byte_offset = row_offset * ROW_SIZE;return (char *)page + byte_offset; }再看一下select操作,我們先創(chuàng)建一個(gè)指向table開頭的cursor,然后同樣調(diào)用cursor_value函數(shù)便可直接獲得對(duì)應(yīng)分頁信息。再使用cursor_advance函數(shù),將cursor往后推進(jìn)一個(gè)row。
BTREE是什么
B樹 (B-tree) 是一種自平衡的樹,能夠保持?jǐn)?shù)據(jù)有序。這種資料結(jié)構(gòu)能夠讓查找數(shù)據(jù)、順序訪問、插入數(shù)據(jù)及刪除的動(dòng)作,都在對(duì)數(shù)時(shí)間內(nèi)完成。 B樹,概括來說是一個(gè)一般化的二元搜尋樹(binary search tree)一個(gè)結(jié)點(diǎn)可以擁有2個(gè)以上的子結(jié)點(diǎn)。與自平衡二叉查找樹不同,B樹適用于讀寫相對(duì)大的數(shù)據(jù)塊的存儲(chǔ)系統(tǒng),例如磁盤。
B樹減少定位記錄時(shí)所經(jīng)歷的中間過程,從而加快存取速度。 B樹這種數(shù)據(jù)結(jié)構(gòu)可以用來描述外部存儲(chǔ)。這種資料結(jié)構(gòu)常被應(yīng)用在數(shù)據(jù)庫和文件系統(tǒng)的實(shí)現(xiàn)上。
與二叉樹不同,B-Tree中的每個(gè)結(jié)點(diǎn)可以有超過2個(gè)子結(jié)點(diǎn)。每個(gè)結(jié)點(diǎn)最多可以有m子結(jié)點(diǎn),其中m稱為樹的“階”。為了保持樹的大部分平衡,我們還說結(jié)點(diǎn)必須至少有m/2子結(jié)點(diǎn)(四舍五入)。
但實(shí)際上,我們?cè)谶@里使用的是B-Tree的一個(gè)變種情況,即:B+Tree。
我們?cè)谄渲袃?chǔ)存我們的數(shù)據(jù),并且每個(gè)結(jié)點(diǎn)中存在多個(gè)鍵值對(duì),而且這個(gè)鍵值是按照順序排列的。
使用這種結(jié)構(gòu)我們的查找時(shí)間復(fù)雜度是O(log(n)),而且插入和刪除的時(shí)間復(fù)雜度也是O(log(n))。
實(shí)現(xiàn)Btree
BTree的結(jié)點(diǎn)是不同的,存在內(nèi)部結(jié)點(diǎn)和葉子結(jié)點(diǎn)的差異性
現(xiàn)在來看我們重中之重的常量
我們將每個(gè)結(jié)點(diǎn)設(shè)置儲(chǔ)存其本身結(jié)點(diǎn)類型,指向其父節(jié)點(diǎn)的指針(通過這個(gè)我們可以實(shí)現(xiàn)查找其兄弟結(jié)點(diǎn)),以及標(biāo)記是否為根結(jié)點(diǎn)。我們將這三個(gè)數(shù)據(jù)定義為元數(shù)據(jù)作為結(jié)點(diǎn)的標(biāo)頭所存儲(chǔ)。
我們來定義葉子結(jié)點(diǎn)需要儲(chǔ)存的實(shí)際數(shù)據(jù)。
/** Leaf Node Header Layout*/ 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;我們儲(chǔ)存其中包含多少個(gè)CELL,即對(duì)應(yīng)的鍵值對(duì)(id與ROW中信息形成對(duì)應(yīng)關(guān)系)。
我們定義我們鍵值對(duì)的鍵和值的大小,以及每個(gè)CELL的大小。同時(shí)我們也定義了葉子結(jié)點(diǎn)的最大CELL數(shù)量,以及它的空間大小,通過一個(gè)結(jié)點(diǎn)對(duì)應(yīng)一個(gè)頁面 (Page)。
當(dāng)葉子結(jié)點(diǎn)滿了之后,需要對(duì)其進(jìn)行分裂,這個(gè)分裂標(biāo)準(zhǔn)是什么?
我們需要將現(xiàn)有的 cell 分成兩個(gè)部分:上半部分與下半部分。
上半部分的 key 嚴(yán)格大于下半部分的 key ,這樣我們就可以將 cell
分成兩個(gè)部分。因此,我們分配一個(gè)新的葉子結(jié)點(diǎn),并將對(duì)應(yīng)的上半部分移入
該葉子結(jié)點(diǎn)。
按照慣例,先看一下假設(shè)我們修復(fù)錯(cuò)誤后應(yīng)該如何正常打印我們的B-Tree。
核心仍舊是使用二分搜索+遞歸。我們知道內(nèi)部結(jié)點(diǎn)中所儲(chǔ)存的孩子結(jié)點(diǎn)的指針的右側(cè)儲(chǔ)存的是該孩子指針?biāo)淖畲蟮膋ey值,所以我們只需要將被搜索的key不斷與key_to_right進(jìn)行比較,直到找到key的位置。
此外,當(dāng)我們找到了對(duì)應(yīng)的孩子結(jié)點(diǎn)時(shí),要注意判斷其類型仍舊為InternalNode,我們需要遞歸調(diào)用internal_node_find。亦或是我們找到了LeafNode,我們僅需要返回一個(gè)相應(yīng)指向該結(jié)點(diǎn)的Cursor對(duì)象即可。
為了保證我們能夠在打印到第一個(gè)葉子結(jié)點(diǎn)的末端時(shí),自動(dòng)跳轉(zhuǎn)到第二個(gè)葉子結(jié)點(diǎn),我們?cè)谄錁?biāo)頭設(shè)置相應(yīng)的next_leaf字段來指向下一個(gè)葉子結(jié)點(diǎn)。
最后更新拆分后的父結(jié)點(diǎn)。
總結(jié)
本文實(shí)例有兩處可改善
- 基于固定的存儲(chǔ)結(jié)構(gòu)(user)
- REPL只能在本機(jī)交互
- select insert 命令最簡版
感興趣小伙伴,可以一塊基于socket實(shí)現(xiàn)REPL,同時(shí)能夠支持create table 等命令。
總結(jié)
以上是生活随笔為你收集整理的C++实现简易数据库的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一款写书、写手册、电子书制作工具
- 下一篇: 华为面试题:请编写一个字符串压缩程序,将