如何教你从0到1实现一个简单的数据库系统(一)

作为一个web从业者,我们每天都和关系型数据库打交道,但是对我们来说只是黑箱,因此我们就很想知道数据库是怎么样工作的?于是乎我就想弄明白数据库到底是怎么运作的,它的基本原理是什么?因此我决定从以下几个问题入手来弄明白数据库系统

  • 数据库中的数据在内存或者磁盘中以什么格式保存?
  • 什么时候数据从内存移动到磁盘中?(持久化)
  • 为什么数据库的每个表有且只有一个主键?
  • 数据库中事务的回滚是怎样工作的?
  • 数据库中索引是怎样格式化的?
  • 什么时候以及如何去扫描全表?
  • 已准备好的语句以什么格式保存的?

换句话说,一个数据库是怎样工作的? 需要指出的是,我在这里从头写了一个简单的mini数据库,模仿的是sqlite,因为它的设计相对于MySQL或者PostgreSQL来说更小,设计更加简单移动,容易理解明白,更加能上手模仿明白!我也能更好的理解数据库工作的原理。整个数据库我存放在单个文件中。


在它们的网站中有许多关于sqlite的实际实现的文章。 SQLite数据库系统: 设计与实现.

一个查询通过一系列组件能够检索或者修改数据。前端有以下部分组成

  • 标记器(tokenizer)
  • 解析器(parser)
  • 代码生成器(code generator)

从前端输入一个sql查询,输出sqlite虚拟机字节码(本质上是一个可以对数据库进行操作的编译程序)。换句话说我们有一个控制台作为输入输出用,当一个sql查询在控制台被输入,然后控制台显示slqite虚拟机的字节码。 后端的组成如下

  • 虚拟机(virtual machine)
  • B-tree(b树)
  • 页(pager)
  • 操作系统接口(os interface)

虚拟机以前端生成的字节码为指令,接着它(字节码指令)能操作一个或者数据库表或者索引,这些数据库表或者索引存放在b-tree的数据结构中。VM本质上讲就是字节码指令类型的一个大的switch语句。

每个b-tree由多个节点组成。每个节点的长度为一页。B-tree能够检索一页从磁盘或者把他通过发送一个命令给pager存回到磁盘中去。

pager可以接受一些命令来读写pages的数据。它负责以适当的偏移量来读取/写入数据库文件。它也可以将最近访问的page缓存在内存中,并且能决定在什么时候这些在缓存中的page被写回到磁盘中。

os interface层面取决与在那个操作系统上编译sqlite,这本次课程中呢将不支持多个平台。

千里之行始于足下 ,所以让我们从更直接的开始:REPL

制作一个简单的REPL

sqlite从read-execute-print循环开始当你输入某些命令的时候

~ sqlite3
SQLite version 3.16.0 2016-11-04 19:09:39
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> create table users (id int, username varchar(255), email varchar(255));
sqlite> .tables
users
sqlite> .exit
~

为了做到上面的内容,我们的函数需要一个无限循环来打印标识符,获取一行数据,然后处理这行命令:

int main(int argc, char* argv[]) {
  InputBuffer* input_buffer = new_input_buffer();
  while (true) {
    print_prompt();
    read_input(input_buffer);

    if (strcmp(input_buffer->buffer, ".exit") == 0) {
      close_input_buffer(input_buffer);
      exit(EXIT_SUCCESS);
    } else {
      printf("Unrecognized command '%s'.\n", input_buffer->buffer);
    }
  }
}

我们需要将把InputBuffer 定义为一个小包装器,他包含了我们需要的状态,以便于与getline()交互(关于getline会在后续详细讲解)。

typedef struct {
  char* buffer;
  size_t buffer_length;
  size_t input_length;
} InputBuffer;

InputBuffer* new_input_buffer() {
  InputBuffer* input_buffer = (InputBuffer*)malloc(sizeof(InputBuffer));
  input_buffer->buffer = NULL;
  input_buffer->buffer_length = 0;
  input_buffer->input_length = 0;
  return input_buffer;
}

接下来,我们需要定义一个print_prompt()函数,来打印标识符,并且在读取每一行之前都需要调用它

void print_prompt() { printf("db > "); }

读取输入的每一行数据,使用getline()函数

size_t getline(char **lineptr, size_t *n, FILE *stream);

lineptr:指向变量的指针,该变量用于指向包含读取行的缓冲区.如果设置为'NULL'则由getline()来分配,因此即使命令失败,该用户也会释放它。 n:用来保存分配buffer大小的变量的指针。 stream:读取输入流,我们会以标准输出。 return value:读取字节的大小,它可能小于buffer的大小。 我们将告诉getline()把读取一行的数据存放到input_buffer->buffer中,并且分配的buffer大小存到input_buffer->buffer_lenth。我们把返回值存到input_buffer->input_length中。 buffer开始是null,因此getline()分配足够的内存来保存输入并且使buffer指向输入。

void read_input(InputBuffer* input_buffer){
  ssize_t bytes_read =
      getline(&(input_buffer->buffer), &(input_buffer->buffer_length), stdin);

  if (bytes_read <= 0) {
    printf("Error reading input\n");
    exit(EXIT_FAILURE);
  }

  // Ignore trailing newline
  input_buffer->input_length = bytes_read - 1;
  input_buffer->buffer[bytes_read - 1] = 0;
}

现在我们定义了一个适合的函数,来释放分配给InputBuffer *的内存和buffer中元素各自的结构。(在read_input()函数中getline()函数分配内存给input_buffer->buffer).

void close_input_buffer(InputBuffer* input_buffer){
  free(input_buffer->buffer);
  free(input_buffer);
}

最终我们将解析执行命令.目前我们只有一个.exit命令能解析执行,该命令用来终止程序的。否则我们会打印一个错误信息并继续进行循环。

if (strcmp(input_buffer->buffer, ".exit") == 0) {
  close_input_buffer(input_buffer);
  exit(EXIT_SUCCESS);
} else {
  printf("Unrecognized command '%s'.\n", input_buffer->buffer);
}

哇塞我们终于可以检验我们的成果了!!运行程序,我们首先输入.tables命令,会打印出Unrecognized command .tables(未能识别.tables命令)然后输入.exit

~ ./db
db > .tables
Unrecognized command '.tables'.
db > .exit
~

好了,我们已经成功的完成了一个REPL,在下一个章节,我们开始开发我们的命令语言。同时,下面是我们本章节的代码:

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
  char* buffer;
  size_t buffer_length;
  size_t input_length;
} InputBuffer;

InputBuffer* new_input_buffer() {
  InputBuffer* input_buffer = malloc(sizeof(InputBuffer));
  input_buffer->buffer = NULL;
  input_buffer->buffer_length = 0;
  input_buffer->input_length = 0;

  return input_buffer;
}

void print_prompt() { printf("db > "); }

void read_input(InputBuffer* input_buffer) {
  size_t bytes_read =
      getline(&(input_buffer->buffer), &(input_buffer->buffer_length), stdin);

  if (bytes_read <= 0) {
    printf("Error reading input\n");
    exit(EXIT_FAILURE);
  }

  // Ignore trailing newline
  input_buffer->input_length = bytes_read - 1;
  input_buffer->buffer[bytes_read - 1] = 0;
}

void close_input_buffer(InputBuffer* input_buffer) {
    free(input_buffer->buffer);
    free(input_buffer);
}

int main(int argc, char* argv[]) {
  InputBuffer* input_buffer = new_input_buffer();
  while (true) {
    print_prompt();
    read_input(input_buffer);

    if (strcmp(input_buffer->buffer, ".exit") == 0) {
      close_input_buffer(input_buffer);
      exit(EXIT_SUCCESS);
    } else {
      printf("Unrecognized command '%s'.\n", input_buffer->buffer);
    }
  }
}

想要了解更多的后续文章,请关注我哦,微信公众号很简书昵称同名

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容