DuckDB

2022-07-31

embeded database也是数据库领域的一个需求,其提供一个链接库,链接到其他进程中运行。当前的SQLite便是embeded database中的一种,只不过其场景是OLTP。在DuckDB之前,针对OLAP场景的embeded database尚未出现。

针对embeded analytical database,有如下几个需求:

  • 高性能的OLAP workload,且不会过多牺牲OLTP性能

很多场景中,既有高并发使用OLTP的更新,也有并行的OLAP查询存在。

  • 高度稳定性

如果embeded database挂掉,会影响其宿主直接宕机。

  • 高效的数据传输

因为embeded database和application运行在同一个进程中,共享相同的地址空间,因此这一点比较容易实现。

  • 嵌入性和可移植性

该database可以在任何环境中都能正常运行,不能依赖任何编译时或周二运行时外部库,同时也不能修改系统的Signal handling。

DuckDB则是一款完全符合上述几个要求的OLAP场景embeded database。

设计与实现

DuckDB的实现主要分如下几个部分:parser、logical planner、optimizer、physical planner以及execution engine。DuckDB任何一部分的实现都没有特别革命性的创新,只是将当前最优的实现和算法结合起来加以实现。

另外,作为一个embeded database,DuckDB没有自己的客户端协议,其仅仅提供了一个C/C++ API。另外,DuckDB提供了一个SQLite的兼容层,使得以前使用SQLite的应用程序可以很容易的切换到DuckDB。

Parser

DuckDB的SQL parser是从Postgres的libpg_query精简而来的。这样做的好处在于既可以提供一个功能丰富的、又稳定的的Parser实现。

由于Postgres是C语言写的,通过这个SQL parser会得到一个C语言结构体表示的parse tree。DuckDB会立即将其转换成自己内部的C++对象,以尽量减少Postgres中的数据结构渗透到DuckDB代码中的多个角落。

Logical Planner

Logical planner由binder和plan generator两部分组成:

  • binder将parse tree与schema的信息(列名、类型等)绑定起来

  • plan generator将parse tree转换成一颗逻辑查询操作符(scan, filter, project等)树。

经过planning阶段,我们可以获得一个fully type-resolved逻辑执行计划(logical plan)。

另外,DuckDB保存了所存储数据的统计信息,这些数据在Logical Planner阶段通过不同的expression trees向下传播,这些统计信息则用于Optimizer阶段。

Optimizer

DuckDB实现了基于规则(rule-based)和基于代价(cost-based)的优化器。Optimizer的主要任务是优化SQL,它将前面logical planner生成的logical plan转换成一个等价但执行代价更小的logical plan。常见的优化方式有谓词下推(predicate pushdown)、表达式重写(expression rewriting)、调整 join 顺序(join ordering)等。

Physical Panner

Physical planner将Logical plan转换成physical plan。Physical plan是一个真正可以执行的物理计划。在生成physical plan过程中,会选择合适的实现,例如:根据join predicates来选择是使用hash join或者merge join

Execution Engine

DuckDB实现了一个向量化(vectorized)的解释型执行引擎。 向量化可以利用 CPU 提供的 SIMD 指令加速计算。这可能会带来可移植性问题的原因而没有选择JIT的实现方式,是因为JIT需要依赖编译组件,比如LLVM。

Execution engine是以向量火山的模式开始执行查询计划的。Query execution以从物理执行计划的root节点拉取chunk data开始。chunk data是结果集中间表或者基表的水平子集。该节点将会递归的从子节点拉取数据,最终到达scan operator,该scan operator将会通过从persistent tables中读取以获取chunk data。当到达root节点的chunk是空时则代表该计划已经执行完。

Transaction

DuckDB通过MVCC提供了ACID兼容性。DuckDB实现了HyPer的MVCC可序列化变体,其实专门针对HTAP场景设计的。该变体以update in place的方式更新数据,并将previous states保存在一个单独的undo buffer,用于并发事务及中止。之所以使用MVCC,而不是其他更简单的方案(例如乐观并发控制),是因为DuckDB虽然是一个OLAP场景存储,但是并发更新也是一个比较频繁的需求。

Storage

DuckDB使用列式存储,并使用了读取优化的数据所存储布局。逻辑表被水平划分为chunks of columns,这些chunk of columns使用轻量的压缩方法压缩成physical block。Block中会保存min/max索引,用于在查询时判断该Block是否相关。另外,block稍微每个column保存了一个轻量的索引,用于进一步限制扫描的数据量。