RDBMS written in Haskell for fun (and for suffering).
SlowQL使用Haskell编写。要运行测试用例,你需要安装stack,然后执行
slowql-exe #启动交互式命令行,输入\q退出。
stack test # 目前只支持测试用例
或者运行现成的Binary。
SlowQL主要划分为文件系统、记录管理、索引管理、系统管理、查询解析等模块。
文件系统为一个模拟的带缓存页式文件系统,可以认为能够支持惰性读和写操作;记录管理为数据的主要来源,支持基础的数据操作;索引管理模块提供按B+树存储的数据,以加快查询操作和实现部分约束(如主键约束);系统管理模块负责管理“数据库”本身,例如管理数据表和索引等;查询解析模块接受SQL语句,根据索引等构造优化的查询表达式,并且进行查询操作。
SlowQL重新实现了基于LRU缓存的文件系统,并提供了供上层使用的IO接口,这使得上层数据库逻辑能够与底层文件的管理解耦合,并且能够使用无副作用的纯函数实现。
为了进行高效的操作,文件系统暴露了一系列不安全的带指针接口。
SlowQL使用了Conduit(注:iteratee库已经过时,但其核心思想与Conduit是一致的,故此处采用Conduit)的思路对数据进行管理:SlowQL首先实现了若干种简单的底层文件数据结构(文件上的线性表、文件上的B+树等),并且将“枚举数据”作为数据结构的接口,而各种关系代数操作和输出到屏幕的操作只需要逐级接收这些数据并进行计算。
Conduit允许在语法上将不同的数据库元素(数据表、索引、关系代数操作)组装在一起,使得“关系代数表达式”作为一种中间代码成为可能,增强了开发的灵活性,并且使得数据的流动构成了一条完整的流水线,从而提高了运行效率;另一方面,Conduit模式可以自动完成对资源的管理(enumerator内部采用RAII的写法,每次只读入一块数据并且释放所有资源),而不需要把管理资源的责任交给语言的垃圾回收器,从而提高了效率。
“数据表”应当作为主信息源出现,提供最基本的增删改查操作,并且支持在数据表的基础上创建索引、约束等。
记录管理系统提供了查询、条件插入、条件修改、条件删除的接口。
查询功能,SlowQL提供了线性操作,用于抽象“对数据表的顺序访问操作”。
此外,SlowQL还支持条件插入、条件删除、条件数据的操作,用于配合相应的SQL语句。
SlowQL内部使用TParam和TValue来表示数据类型及数据的值,例如,当数据类型失配时会抛出TypeMismatch错误,当违反空约束时会抛出NullValue错误等。
SlowQL的上层数据类型与下层是分离的,这有利于上下层分离进行针对性的优化。
索引应当作为原始数据表的冗余出现,根据选择的列构建原始数据表的B+树,并且能够随数据表联动变化。
简便起见,SlowQL的索引为原始数据表的完全冗余,即索引存储的内容为原始数据表的所有内容。
BTreeTable实现了一个简单的B+树结构:节点分为中间节点和叶子节点两种,中间节点只存储用于比较的键值,叶子节点存储具体的数据;平级节点存在指向sibling的指针,使得横向遍历成为可能。 BTreeTable支持“从某叶子开始的线性查询”(因为使用了链表所以保证了与答案相关的线性复杂度),以及任意的单点插入删除操作。
在设计上,BTreeTable对数据表的结构是无知的:BTreeTable只是一棵“外置比较器的存放字节串的B+树”,需要在查询、插入、删除时具体指明比较器和键提取器。这使得B+树的实现能够与数据库本身的设计解耦合,并且根据需要调整BTreeTable的冗余程度(例如调整至只保存RID/指针等)。
具体实现上,BTreeTable试图在不使用其它语言(如调用C/C++编写的外部库)的情况下保持高效,例如,放弃函数式写法而采用了大量过程式语言风格的有状态的、带指针的不安全操作,并且较重构前取得较大的效率提升。
一方面“管理所有的表和索引”是一个规模较小的任务,可以简单完成(不需要考虑运行效率);另一方面,系统管理应当完成数据表和索引的综合任务。
SlowQL在LinearTable和BTreeTable的基础上进一步进行抽象,抽象出一系列基本数据类型、元组(数据)数据类型,以及基于元组操作的“表”Table和“索引”Index。Table和Index藉由前述基本数据结构实现,并且同样支持插入、删除、修改等基本操作,以及基于Conduit的查询操作(enumerate函数)。SlowQL将同一数据库内的表的信息和索引的信息统一管理,并且在查询解析阶段使用这些信息对查询操作进行优化。
存储上,SlowQL将工作目录下的slowql-data文件夹作为数据的存储路径,slowql-data下的每个文件夹存放一个数据库。每个数据库内以简单的二进制串存储该数据库的基本信息(.slowql文件)。
当一个数据库被使用时,出于高效与简单起见所有的数据表和索引都会被打开,等待使用。在插入、删除、查询前,SlowQL会检查待插入的条目是否存在重复(主键约束),若有则禁止插入。
最后,SlowQL提供了一个简单的REPL,供输入SQL语句以获得查询结果。这个REPL将作为SlowQL的主入口使用。
SlowQL对SQL语句的执行划分为三个步骤进行:第一步将输入的SQL语句解析为简单的SQL语法树,第二部根据SQL语法树和数据库的情况生成查询计划,第三步将查询计划编译为流水线(Conduit)表达式并且执行。
第一步为SQL语句的解析。该步骤较为简单,SQL语句的编译器通过happy解析BNF文法生成(文法参见src/SlowQL/SQL/Parser.y)。
第二步为SQL语法树到查询计划的生成。对于插入、查询和删除语句,SlowQL使用简单的方式完成;对于查询操作,SlowQL内置了若干关系代数原语(包括了基本的投影、选择、求交、笛卡尔积运算以及基于索引的连接-选择复合运算),能够根据数据库的索引情况生成较优的关系代数运算。简单起见,SlowQL采用的生成关系代数的基本规则大体如下:
- 表间引用最优先处理,尽可能匹配可用的索引以实现。
- 单表Where从句在参与笛卡尔积前先进行筛选运算:假如某个表只有单表Where从句,而且所有从句的交集是一个区间(包括精确匹配),则该操作被优化成利用索引的查询。
- 当无法进一步化简时,从左向右取笛卡尔积。
- 最后计算复合筛选运算和投影运算。由于惰性求值的存在,只有当筛选运算需要某个域,或者投影结果存在某个域时这个域才会被计算(解析),故将投影运算放在最后不会影响效率。
这样的简单算法导致优化方案与SQL语句的书写顺序有关:假如SQL语句的书写正好能够对应若干已经存在的索引,则该查询可以被索引优化。
第三部为关系代数表达式的执行。SlowQL通过将关系代数表达式转换为流水线的方式执行查询语句:对于每个关系代数原语,SlowQL都给出了将其转化为流水线组件的方法,求解关系代数表达式的过程就是构建查询流水线的过程;当在REPL中执行SQL语句时,流水线的末端被直接连接到标准输出。
出于调试等需求起见,SlowQL实现了一些简单的附加功能,列举如下。
- 关系代数表达式/查询计划的输出(explain):在执行查询语句前,SlowQL会先输出查询计划,然后进行执行。
- offset/limit 子句:为了调试方便,SlowQL实现了offset/limit子句;这一功能可以被用于简单分页等用途。
所有的测试用例都置于test/SlowQL/Test目录下。
对线性表的简单测试。
对B+树的简单测试。