nolladb 是一个基于 Rust 开发的 simple 关系型数据库
用 Rust 之前,为了理解 Rust 的一些核心概念,我写了一篇博客
在写这个数据库时,为了充分理解 B-Tree 这个数据结构,我还写了一篇关于这个的博客
另外,为了学习数据库的原理,我还去看了那个著名的数据库课程 CMU 15-445/645,并做了几篇对应的笔记,见
目前来说笔记还未更新完,后续会接着更新
如下图所示
对于 Table 来讲,它的结构主要分 5 个
table_rows
table_rows 是由 Rc 和 RefCell 指针管理的一个 HashMap
如图所示
这个 HashMap 由 2 部分组成,左边的是 key,右边的是 value
这里的 key 存放的是 column 的名称,即字符串
这里的 value 存放的是一个 BTreeMap,BTreeMap 节点也由 2 部分组成
BTreeMap的key存放row_idBTreeMap的value存放name对应的具体的值,也就是字符串
对于 table_rows 的 key 来说,每个 key 可以有不同的类型,本项目中存在 4 种
Integer / Bool / Text / Real
在上图中,name 对应的 type 是 Text,所以在其对应的 BTreeMap 中,value 存的值就是 string 也就是字符串
为什么要用到 HashMap,是因为在针对某一个 column 进行操作的场合很方便
当然 table_rows 在本项目中还是整体作为 数据备份 使用
table_columns
table_columns 是一个数组,这个数组里面每个元素都存放着有关 column 的信息,比如 column 的名称,column 的 datatype,以及一些约束等等,最重要的 1 项还是它里面的 index 索引,这个索引指向的也是一个 BTreeMap
如图所示
可以看到对比于 table_columns 里面的 BTreeMap 来说,这里面的 BTreeMap 的 key 和 value 是 反着 存的
为什么要用到数组,是因为在一些要查询 column 元数据的场合很方便,并且也有利于遍历查询
indexes即表索引,也是一个HashMaptable_name即表名primary_key即主键most_recent_row_id,对应于每一条记录的row_id
对于创建表的 SQL 语句来说,首先需要解析这条语句成 ast,然后拿到其中的 表名、需要创建的 column
对于 column 来说,有几个条件是需要做确认的
Bool和Real类型不能拿来做主键- 表里面不能有重复的主键,最多只能有 1 个
- 如果某个
column不是主键,那么它的约束默认就是unique和not null
对于创建表来说,它总体也要经过如下的步骤
- 需要检查表是否已经被创建过了,这个需要到
Database里面找 - 创建表,初始化一些基本的信息
- 把表插入到
Database中
对于 INSERT 这个 SQL 语句来说,首先需要解析这条语句成 ast,然后拿到其中的 表名、column 相关的名称 以及 column 相关的值
然后 INSERT 需要经过如下步骤
- 首先需要确认的是有没有主键
- 有主键的话,需要查看要
INSERT的列的列名包不包含主键 - 如果包含,直接找到这一列更新
row_id - 如果不包含,那就直接插入这一列的数据,不用更新
row_id
- 还需要检查
INSERT的VALUES中,还有没有column的值是没有被设置的,如果有那么这些没有被设置的column的值,将其默认置为null,然后对这条记录做更新 - 记录插入完成,随即更新
row_id
对于 Table 数据来说,它的整个信息都保存在 Database 里,对于 Database,它的结构是这样的
pub struct Database {
pub database_name: String,
pub tables: HashMap<String, Table>,
}Database 通过 HashMap 来管理多个 Table
那么谁来管理整个 Database 呢? 就是 DatabaseManager,它的结构如下
pub struct DatabaseManager {
pub database: HashMap<String, Database>,
}它也是通过 HashMap 来管理 Database,主要的作用就是 保存 和 读取 Database 对应的 .db 文件的信息,然后将它加载进内存中,然后 Database 就可以获取到这些信息,那么在当前 Session 里面就可以对对应的 Table 进行操作了
对于 DatabaseManager 来讲,每次做完保存操作,都会去更新对应的 .dmf 文件,读取时就从这个文件里面读取保存的 database 相关的信息,然后加载进内存
保存 和 读取 都是操作二进制流
主要是
sqlparser 0.13rstest 0.12rustyline 9.1.2prettytable-rs 0.8bincode 1.3.3thiserror 1.0.30
- 支持命令行接口
- 支持部分
meta命令和 SQL 语句的解析 - 支持
执行简单的命令 - 使用
thiserror支持标准错误处理 - 支持创建
Table - 支持创建
Table时解析重复列 - 支持创建
Table时解析多个PRIMARY KEY - 支持简单
INSERT查询命令的解析 - 拥有专门为
PRIMARY KEY初始化的内存型BTreeMap索引 - 支持唯一
KEY约束
git clone git@github.com:strugglebak/nolladb.git
cd nolladb
cargo run test.dbcd nolladb
cargo test- 实现简单
SELECT查询 - 实现 JOINS
- INNER JOIN
- LEFT OUTER JOIN
- CROSS JOIN
- 实现预写日志
- 实现页模块
- 实现事务 ACID
- 并发
- 锁管理
- 实现复合索引
- 实现连接管理
- 实现不同场景下的存储引擎
- 实现
LSM Tree && Sorted Strings Table应对大量写的场景 - 实现更快的
B-Tree应对大量读的场景
- 实现
由于 Rust 的 lifetime 的原因,抽代码不太好抽,一个建议是能不用引用的地方尽量不用引用,有引用的地方代码就需要考虑 lifetime 了
目前来说并没有实现 SELECT 和 UPDATE 对应的功能,主要还是因为对于 SELECT 这种 Query 来讲,它的组合方式有很多,组合的条件也有很多,虽然能用库解析成 ast,但是中间需要考虑的条件有很多种,对于 SELECT 来讲它大体需要经过如下几个步骤
- Logical Plan
- 针对 Logical Plan 的优化
- Physical Plan
其中 Logical Plan 用的比较多的方式是用 Vocanlo Model,但由于时间有限所以没有继续写下去了
后面会参考下 databend 这个里面有关 SQL Plan 的代码写一版出来,顺便分析下它的代码是怎么写的,可能后面会更一篇文章




