让我们建立一个简单的数据库1

第 1 部分 - 简介和设置 REPL

作为一名网络开发人员,我在工作中每天都要使用关系数据库,但对我来说,关系数据库是个黑匣子。我有一些问题:

  • 数据以什么格式保存?(内存和磁盘)
  • 数据何时从内存转移到磁盘?
  • 为什么每个表只能有一个主键?
  • 如何回滚事务?
  • 如何格式化索引?
  • 何时以及如何进行全表扫描?
  • 准备语句以什么格式保存?

换句话说,数据库是如何工作的?

为了搞清楚这个问题,我正在从头开始编写一个数据库。它以 sqlite 为模型,因为它的设计比 MySQL 或 PostgreSQL 更小巧,功能更少,所以我更有希望理解它。整个数据库存储在一个文件中!

Sqlite

他们的网站上有很多关于 sqlite 内部结构的文档,另外我还有一本《SQLite 数据库系统:设计与实现》一书。

 sqlite 架构
(https://www.sqlite.org/zipvfs/doc/trunk/www/howitworks.wiki)

为了检索或修改数据,查询需要经过一系列组件。前端包括:

  • 标记符
  • 解析器
  • 代码生成器

前端的输入是 SQL 查询,输出是 sqlite 虚拟机字节码(本质上是可在数据库上运行的编译程序)。

后端包括:

  • 虚拟机
  • B 树
  • 事务管理器(pager)
  • 操作系统界面

虚拟机将前端生成的字节码作为指令。然后,它可以对一个或多个表或索引执行操作,每个表或索引都存储在称为 B 树的数据结构中。虚拟机本质上是一个关于字节码指令类型的大型开关语句。

每个 B 树都由许多节点组成。每个节点长度为一页。B 树可以通过向事务管理器(pager)发出命令,从磁盘检索页面或将页面保存回磁盘。

事务管理器(pager)接收读取或写入数据页的命令。它负责在数据库文件中的适当偏移位置进行读/写。它还会将最近访问过的页面缓存在内存中,并确定何时需要将这些页面写回磁盘。

千里之行始于足下,让我们从更简单的 REPL 开始。

制作简单的 REPL

从命令行启动 Sqlite 时,它会启动一个读取-执行-打印循环:

~ 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()交互时需要存储的状态的小型封装。(稍后详述)

typedef struct {
  char* buffer;
  size_t buffer_length;
  ssize_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()

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

lineptr :变量指针,用于指向包含读取行的缓冲区。如果设置为 NULL,它将被 getline 调用,因此即使命令失败,用户也应释放它。

n:指针,指向用于保存已分配缓冲区大小的变量。

stream :要读取的输入流。我们将从标准输入流读取数据。

return value(返回值):读取的字节数,可能小于缓冲区的大小。

我们会告诉 getline 将读取的行存储在 input_buffer->buffer,将分配的缓冲区大小存储在 input_buffer->buffer_length。我们将返回值保存在 input_buffer->input_length 中。

buffer 开始时为空,因此 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 * 实例和相应结构的缓冲区元素分配的内存(getline 在 read_input 中为 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);
}

让我们试一试!

~ ./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;
  ssize_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) {
  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;
}

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);
    }
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值