作为一名网页开发者,我每天在工作中都使用关系型数据库,但对我来说它们就像一个黑匣子。我有一些疑问:
 
  1. 数据是以什么格式保存的?(在内存中和磁盘上)
  1. 它何时从内存移动到磁盘?
  1. 为什么每个表只能有一个主键?
  1. 回滚事务是如何工作的?
  1. 索引是如何格式化的?
  1. 全表扫描何时以及如何发生?
  1. 预处理语句是以什么格式保存的?
 
换句话说,数据库是如何工作的?
 
为了弄清楚这些事情,我正在从零开始编写一个数据库。它是以 SQLite 为模型的,因为它被设计得更小,功能比 MySQL 或 PostgreSQL 少,所以我有更大的希望理解它。整个数据库都存储在一个单一的文件中!
 

Sqlite

 
他们的网站上有很多关于SQLite内部结构的文档,而且我还有一本《SQLite数据库系统:设计与实现》的书。下面这张图是sqlite的架构,来自于(https://www.sqlite.org/zipvfs/doc/trunk/www/howitworks.wiki
notion image
 
一个查询通过一系列组件链来检索或修改数据。front-end由以下组成:
  • tokenizer
  • parser
  • code generator
 
front-end的输入是一个SQL查询,输出是SQLite virtual machine bytecode(本质上是一个可以操作数据库的编译程序)。
 
back-end由以下组成:
  • virtual machine
  • B-tree
  • pager
  • os interface
 
virtual machine将front-end生成的bytecode作为指令。然后,它可以对一个或多个表或索引执行操作,每个表或索引都存储在一个称为B树的数据结构中。
 
每个B树由许多节点组成。每个节点的长度为一页。B树可以通过向分页器发出命令来从磁盘检索页面或将其保存回磁盘。
 
pager接收读取或写入数据页面的命令。它负责在数据库文件中的适当偏移处进行读写。它还在内存中保留最近访问页面的缓存,并确定何时需要将这些页面写回磁盘。
 
os interface是根据不同操作系统以不同的方式编译SQLite。
 
千里之行始于足下,所以让我们从一些更简单的事情开始:REPL(Read-Eval-Print Loop,读取-求值-打印循环)。
 

Making a Simple REPL

 
SQLite在从命令行启动时开始一个读取-执行-打印循环:
 
为了做到这一点,我们的主函数将有一个无限循环,它打印提示符,获取一行输入,然后处理该行输入:
 
 
我们将定义InputBuffer作为一个小包装器,用于存储我们需要与getline()交互的状态。(稍后再详细介绍)
 
 
接下来,print_prompt() 会向用户打印一个提示符。我们在读取每行输入之前都会这样做。
 
要读取一行输入,请使用getline()函数:
 
lineptr: 一个指向我们用来指向包含读取行的缓冲区的变量的指针。如果它被设置为NULL,那么getline会为它分配内存,因此即使命令失败,用户也应该释放它。
n: 一个指向我们用来保存分配缓冲区大小的变量的指针。
stream: 要读取的输入流。我们将从标准输入读取。
返回值: 读取的字节数,可能小于缓冲区的大小。
我们告诉getline将读取的行存储在input_buffer->buffer中,并将分配的缓冲区大小存储在input_buffer->buffer_length中。我们将返回值存储在input_buffer->input_length中。
buffer最初为null,因此getline分配足够的内存来保存输入行,并使buffer指向它。
 
 
现在应该定义一个函数,用于释放为InputBuffer *实例及其相应结构体的缓冲区元素分配的内存(getline在read_input中为input_buffer->buffer分配内存)。
 
 
最后,我们解析并执行命令。目前只识别一个命令:.exit,它用于结束程序。否则我们会打印一个错误消息并继续循环。
 
 
让我们试试吧!
 
 
好的,我们已经有了一个工作的REPL。在下一部分,我们将开始开发我们的命令语言。与此同时,这是这部分的整个程序:
 
💡
有什么问题欢迎在下面评论区讨论~
 
Loading...