db_tutorial实战:用C语言实现数据库的行级存储结构
你是否曾好奇数据库如何高效存储和管理数据?本文将带你深入了解db_tutorial项目中使用C语言实现的行级存储结构,通过实战案例掌握数据在内存和磁盘中的组织方式。读完本文,你将能够理解数据库存储的底层原理,掌握行结构设计、数据序列化与页管理的核心技术。
行结构设计:数据的基本单元
在数据库系统中,行(Row)是数据存储的基本单元。db_tutorial项目在db.c中定义了Row结构体,包含id、username和email三个字段:
typedef struct {
uint32_t id;
char username[COLUMN_USERNAME_SIZE + 1];
char email[COLUMN_EMAIL_SIZE + 1];
} Row;
为了高效计算各字段的大小和偏移量,项目使用了一个巧妙的宏定义size_of_attribute:
#define size_of_attribute(Struct, Attribute) sizeof(((Struct*)0)->Attribute)
通过这个宏,可以在编译时计算出每个字段的大小,如db.c所示:
const uint32_t ID_SIZE = size_of_attribute(Row, id);
const uint32_t USERNAME_SIZE = size_of_attribute(Row, username);
const uint32_t EMAIL_SIZE = size_of_attribute(Row, email);
进而计算出各字段在内存中的偏移量db.c:
const uint32_t ID_OFFSET = 0;
const uint32_t USERNAME_OFFSET = ID_OFFSET + ID_SIZE;
const uint32_t EMAIL_OFFSET = USERNAME_OFFSET + USERNAME_SIZE;
整个行的大小则是各字段大小之和db.c:
const uint32_t ROW_SIZE = ID_SIZE + USERNAME_SIZE + EMAIL_SIZE;
数据序列化:内存与磁盘之间的桥梁
当数据需要在内存和磁盘之间传输时,需要进行序列化(Serialize)和反序列化(Deserialize)。db_tutorial项目提供了两个关键函数来完成这一过程。
序列化函数serialize_row将Row结构体的数据按照预定的偏移量复制到目标缓冲区:
void serialize_row(Row* source, void* destination) {
memcpy(destination + ID_OFFSET, &(source->id), ID_SIZE);
memcpy(destination + USERNAME_OFFSET, &(source->username), USERNAME_SIZE);
memcpy(destination + EMAIL_OFFSET, &(source->email), EMAIL_SIZE);
}
反序列化函数deserialize_row则执行相反的操作,从缓冲区中提取数据并填充到Row结构体:
void deserialize_row(void* source, Row* destination) {
memcpy(&(destination->id), source + ID_OFFSET, ID_SIZE);
memcpy(&(destination->username), source + USERNAME_OFFSET, USERNAME_SIZE);
memcpy(&(destination->email), source + EMAIL_OFFSET, EMAIL_SIZE);
}
这种简单而高效的序列化方式确保了数据在内存和磁盘之间的高效传输。
页管理:数据存储的基本单位
数据库通常以页(Page)为单位管理磁盘空间。db_tutorial项目定义了页大小为4096字节db.c:
const uint32_t PAGE_SIZE = 4096;
Pager结构体db.c负责管理页的缓存和磁盘I/O:
typedef struct {
int file_descriptor;
uint32_t file_length;
uint32_t num_pages;
void* pages[TABLE_MAX_PAGES];
} Pager;
get_page函数负责从磁盘加载页到内存,如果页已经在内存中则直接返回:
void* get_page(Pager* pager, uint32_t page_num) {
// 实现细节省略
}
页的刷新由pager_flush函数负责,将内存中的页写回磁盘:
void pager_flush(Pager* pager, uint32_t page_num) {
// 实现细节省略
}
叶节点结构:行数据的存储容器
在B树结构中,叶节点(Leaf Node)直接存储行数据。db_tutorial项目定义了叶节点的布局,包括头部和单元格区域。
叶节点头部包含节点类型、是否为根节点、父指针、单元格数量和下一个叶节点指针db.c:
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;
每个叶节点单元格包含一个键和一个值(即行数据)db.c:
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;
叶节点的初始化由initialize_leaf_node函数完成:
void initialize_leaf_node(void* node) {
set_node_type(node, NODE_LEAF);
set_node_root(node, false);
*leaf_node_num_cells(node) = 0;
*leaf_node_next_leaf(node) = 0; // 0 represents no sibling
}
叶节点的结构可以用下图形象表示:
游标操作:数据访问的接口
游标(Cursor)提供了一种抽象的数据访问接口,用于遍历和操作数据库中的行。db_tutorial项目定义了Cursor结构体db.c:
typedef struct {
Table* table;
uint32_t page_num;
uint32_t cell_num;
bool end_of_table; // Indicates a position one past the last element
} Cursor;
table_start函数返回指向表中第一行的游标:
Cursor* table_start(Table* table) {
Cursor* cursor = table_find(table, 0);
// 实现细节省略
return cursor;
}
cursor_value函数返回游标当前指向的行数据:
void* cursor_value(Cursor* cursor) {
uint32_t page_num = cursor->page_num;
void* page = get_page(cursor->table->pager, page_num);
return leaf_node_value(page, cursor->cell_num);
}
cursor_advance函数将游标移动到下一行:
void cursor_advance(Cursor* cursor) {
// 实现细节省略
}
插入操作:数据写入的实现
插入操作是数据库的核心功能之一。db_tutorial项目的插入操作涉及解析SQL语句、准备数据和写入叶节点等步骤。
prepare_insert函数负责解析INSERT语句并提取数据:
PrepareResult prepare_insert(InputBuffer* input_buffer, Statement* statement) {
// 实现细节省略
}
叶节点的查找由leaf_node_find函数实现,使用二分查找高效定位插入位置:
Cursor* leaf_node_find(Table* table, uint32_t page_num, uint32_t key) {
// 实现细节省略
}
B树结构:高效查询的基础
db_tutorial项目实现了B树索引结构,用于高效查询数据。B树由内部节点(Internal Node)和叶节点组成。内部节点的结构定义在db.c:
const uint32_t INTERNAL_NODE_NUM_KEYS_SIZE = sizeof(uint32_t);
const uint32_t INTERNAL_NODE_NUM_KEYS_OFFSET = COMMON_NODE_HEADER_SIZE;
const uint32_t INTERNAL_NODE_RIGHT_CHILD_SIZE = sizeof(uint32_t);
const uint32_t INTERNAL_NODE_RIGHT_CHILD_OFFSET =
INTERNAL_NODE_NUM_KEYS_OFFSET + INTERNAL_NODE_NUM_KEYS_SIZE;
const uint32_t INTERNAL_NODE_HEADER_SIZE = COMMON_NODE_HEADER_SIZE +
INTERNAL_NODE_NUM_KEYS_SIZE +
INTERNAL_NODE_RIGHT_CHILD_SIZE;
B树的结构可以用下图表示:
总结与展望
本文详细介绍了db_tutorial项目中行级存储结构的实现,包括行结构设计、数据序列化、页管理、叶节点结构、游标操作、插入操作和B树结构等核心组件。这些组件共同构成了一个简单但功能完整的数据库存储引擎。
通过学习这些底层实现,我们可以更好地理解数据库的工作原理,为优化数据库性能和解决实际问题打下基础。未来,我们可以进一步扩展这个项目,添加更多高级功能,如事务支持、索引优化和并发控制等。
如果你对数据库实现感兴趣,可以通过README.md了解更多项目细节,或查看db.c源代码深入学习。此外,项目的各个部分实现可以在_parts目录中找到,例如part1.md介绍了项目的基本概念,part5.md详细讲解了B树的实现。
希望本文能帮助你更好地理解数据库的行级存储结构,为你的学习和工作提供参考。如果你有任何问题或建议,欢迎在项目社区中交流讨论。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考





