“我无法创建的东西,我不明白。”–RichardFeynman为了理解,我正在用C从头开始??构建sqlite的克隆,并且我将在进行过程中记录我的过程。译注:cstsck在github上维护了一个简单的类似SQLite的数据库实现。通过这个简单的项目,您可以很好地了解数据库的工作原理。原标题:Let'sBuildaSimpleDatabase,本文为第一篇。第1部分介绍和设置REPL作为一名开发人员,我每天都在使用关系数据库。但对我来说,它们是一个黑盒子。我有一些问题:数据存储使用什么格式?(内存与磁盘)数据何时从内存移动到磁盘?为什么一张表只能有一个主键?事务回滚如何工作?索引的格式是什么?何时以及如何进行全表扫描?准备好的语句以什么格式存储?换句话说,数据库是如何工作的?为了解决这个问题,我从头开始编写了一个数据库。它是模仿SQLite实现的,因为SQLite在设计上比较小,相对于MySQL和PostgreSQL,功能相对要少很多,希望能更容易理解。在实现中,整个数据库存储在一个数据文件中。SQLite在SQLite网站上,有很多SQLite内部文档(https://www.sqlite.org/arch.html)。我还复制了一份文档(SQLite数据库系统:设计和实现。)(https://play.google.com/store/books/details?id=9Z6IQQnX1JEC)SQLite体系结构(https://www.sqlite。org/zipvfs/doc/trunk/www/howitworks.wiki)查询通过组件链来获取或修改数据。前端组件如下:分词器(tokenizer)、解析器(parser)、代码生成器(codegenerator),前端的输入是一条SQL语句。输出的是SQLite的虚拟机字节码(virtualmachinebytecode),它本质上是一个可以在数据库上运行的编译程序。译注:数据库实现查询优化模型分为传统的火山模型(Volcanomodel)和Codegen模型。本文作者实现了Codegen模型。后端包括以下组件:虚拟机(virtualmachine)B-tree页面管理(pager)系统接口(osinterface)虚拟机虚拟机使用前端生成的字节码作为指令。然后它可以对一个或多个表和索引执行操作,这些表和索引存储在称为B树的数据结构中。VM本质上是一个bigswitchstatementonthetypeofbytecodeinstruction(一个bigswitchstatementonthetypeofbytecodeinstruction)B-tree每个B-tree有很多节点。每个节点是一页的长度。B-tree可以通过向pager执行命令从磁盘中获取一个页面或者将页面保存回磁盘。pagerpager接受读取或写入数据页的命令。它负责读写数据库文件的适当偏移量。它还负责将当前访问的页面保存在内存中,并决定何时需要将这些页面写回磁盘。osinterface系统接口和SQLite根据不同的操作系统平台编译不同。在本系列教程中,我不会支持多平台适配。千里之行,始于足下,那么我们先从简单的说起:REPL实现了一个简单的REPL注解:REPL,Read-Execute-Print-Loop,即read-execute-printout-loop,这个过程。有时翻译成交互式解释器当您执行命令行命令时,SQLite开始读取-执行-打印循环:sqlite3SQLiteversion3.16.02016-11-0419:09:39输入“.help”获取使用提示.Connected到一个临时的内存数据库。使用“.openFILENAME”在持久数据库上重新打开。sqlite>创建表用户(idint,usernamevarchar(255),emailvarchar(255));sqlite>.tablesuserssqlite>.exit为了实现这个效果,我们的主程序需要有一个无限循环来打印这个提示,得到一行输入,然后处理这行输入:intmain(intargc,char*argv[]){InputBuffer*input_buffer=new_input_buffer();while(true){print_prompt();读取输入(输入缓冲区);如果(strcmp(input_buffer->buffer,".exit")==0){close_input_buffer(input_buffer);退出(退出成功);}else{printf("无法识别的命令'%s'。\n",input_buffer->buffer);我们将InputBuffer定义为我们需要存储的状态的包装器,以便与getline()函数进行交互(稍后会详细介绍)typedefstruct{char*buffer;size_tbuffer_lengthH;ssize_t输入长度;}输入缓冲区;InputBuffer*new_input_buffer(){InputBuffer*input_buffer=(InputBuffer*)malloc(sizeof(InputBuffer));input_buffer->buffer=NULL;;返回输入缓冲区;接下来,print_prompt()函数打印一条提示,让用户在执行此操作之前阅读每一行输入。voidprint_prompt(){printf("db>");}要读取命令行输入,您需要使用getline()函数:ssize_tgetline(char**lineptr,size_t*n,FILE*stream);(下面是getline函数的释义)lineptr:指向变量的指针,指向我们从命令行读取的命令,我们包含在buffer中。如果设置为NULL,则由getline()函数分配内存。并且后续由用户释放,即使命令行命令执行失败,也保证分配的内存会被释放。n:一个指针变量,指向已分配内存的缓冲区的大小(size)。stream:要读取的输入流,这里是从标准输入读取的。返回值(returnvalue,ssize_t类型):读取的字节数,可能小于缓冲区的大小。我们告诉getline()函数将读取的命令行保存到input_buffer->buffer,将缓冲区的大小保存到input_buffer->buffer_length,保存到input_buffer->input_lengthbuffer的返回值初始为NULL,所以getline()函数分配足够的内存用于存储输入的命令行数据,然后缓冲区用于指向这些数据。voidread_input(InputBuffer*input_buffer){ssize_tbytes_read=getline(&(input_buffer->buffer),&(input_buffer->buffer_length),stdin);if(bytes_read<=0){printf("读取输入错误\n");退出(退出失败);}//忽略尾随的换行符input_buffer->input_length=bytes_read-1;input_buffer->buffer[bytes_read-1]=0;现在可以定义一个函数来释放分配的InputBuffer实例和buffer元素各自数据结构的内存(在read_input()函数中,调用getline()函数为input_buffer->buffer分配内存)。voidclose_input_buffer(InputBuffer*input_buffer){free(input_buffer->buffer);免费(输入缓冲区);最后,我们解析并执行命令。现在这只是一个可识别的命令:.exit,一个终止程序的命令。对于其他命令,我们打印一条错误消息并继续程序循环。如果(strcmp(input_buffer->buffer,".exit")==0){close_input_buffer(input_buffer);退出(退出成功);}else{printf("无法识别的命令'%s'。\n",input_buffer->buffer);让我们试试吧!~./dbdb>.tables无法识别的命令“.tables”。db>.exit~好的,我们得到了一个可以工作的REPL。在下一部分中,我们将开始开发我们的命令语言。同时,下面是这部分的全部程序代码:1#include
