数据模型

目前,DolphinDB 支持两种存储引擎:OLAP 和 TSDB。在创建数据库时,通过 database 函数的 engine 参数指定存储引擎。一个 database 只能使用一种引擎。

存储模型

OLAP 和 TSDB 存储引擎均采用数据分区技术,按照给定规则将大规模数据集水平分区,每个分区内的数据采用列式存储。

OLAP 数据表的每个列存储为一个文件,数据以追加的方式存储到相应的列文件中,因此,数据写入的顺序决定了它们的存储顺序。

TSDB 采用经典的 LSMT (Log Structured Merge Tree) 模型,引入了排序列(sortColumns:最后一列为时间列,其他列组合为 sortKey)。 在用户查询某个或少数几个设备(股票)在特定时间段数据的场景下,可以通过将设备(股票)标识、时间等列作为 TSDB 引擎的 sortColumns,以提升查询性能。

下面通过例子直观地解释 DolphinDB 的存储模型。首先,创建数据库和表:

$ n=10000
$ ID=rand(100, n)
$ dates=2017.08.07..2017.08.11
$ date=rand(dates, n)
$ vol=rand(1..10 join int(), n)
$ t=table(ID, date, vol)
$ if(existsDatabase("dfs://db1")){
$   dropDatabase("dfs://db1")
$ }
$ db=database(directory="dfs://db1", partitionType=RANGE, partitionScheme=0 50 100)
$ pt=db.createPartitionedTable(table=t, tableName=`pt, partitionColumns=`ID)
$ pt.append!(t);

上例在 OLAP 存储引擎中创建了数据库 db1 以及其中的数据表 pt,根据ID列的范围划分分区。ID 在 [0,50) 的记录会保存到同一个分区,ID 在 [50,100) 的记录会保存至另一个分区。由于该表仅有三列,其在每个分区的数据为三个列文件 date.col, ID.col 和 vol.col。如果副本为 2,那么表 pt 一共有 3*2*2=12 个列文件。

$ if(existsDatabase("dfs://TSDB_db1")){
$   dropDatabase("dfs://TSDB_db1")
$ }
$ db=database(directory="dfs://TSDB_db1", partitionType=RANGE, partitionScheme=0 50 100, engine="TSDB")
$ Pt1=db.createPartitionedTable(table=t, tableName=`pt1, partitionColumns=`ID, compressMethods="", sortColumns=`vol)
$ Pt1.append!(t);

上例在 TSDB 存储引擎中创建了数据库 TSDB_db1 以及其中的数据表 pt1,根据 ID 列的值划分分区(ID位于[0,50)区间的记保存到一个分区,[50,100)的记录保存至另一分区), sort column 为 vol 列,每个文件按照 vol 进行排序。分区内的数据以 level file 的形式存储,保存在 level 0 内的列文件为以 “0_” 开头;保存在 leve 1 内的列文件为以 “1_” 开头,以此类推。

除了上例使用的范围分区外,DolphinDB 还支持哈希分区、值分区、列表分区和组合分区,用户可以根据业务特点对数据进行均匀分割。具体请参考 创建数据库和表 一节。

数据压缩

DolphinDB 支持无损压缩,采用兼顾压缩速度和压缩率的 LZ4 压缩算法。压缩率与数据重复频率有关,若同一列中的重复项越多,压缩率就会越高。对金融数据,磁盘数据的压缩率一般可达到 20% 到 25% 左右。

OLAP 采用增量压缩策略,每次只对新增数据进行压缩,因此批量写入有助于提升压缩效果。若每次仅写入一行记录,且 cache engine 未开启,则存于磁盘的数据并未进行压缩。

在时序数据库中,时间越近的数据越有可能被访问,因此,TSDB 存储引擎通过压缩 cache 的方式,以在 cache 中缓存更多的数据,从而减少I/O开销,提高性能。cache 的压缩率约为20%,相当于可以缓存5倍的数据。 对于存盘数据,在磁盘上的 PAX 列式文件采用 Block 级别压缩(即一次按照一个大小为16KB的 Block 来压缩)。

对于不同数据类型建议采用不同压缩方式,进一步提高压缩率:

  • 时间列建议指定 compressMethods 为 “delta”。

  • 其余列默认采用 “lz4” 压缩算法。

数据写入

现代计算机操作系统为了提升 I/O 性能,都会提供页面缓冲。当数据写入文件时,并不是直接写入到磁盘,而是写入操作系统的缓冲页面中。如果操作系统崩溃或掉电,可能会导致数据丢失。 DolphinDB 对数据库 log(包括 redo log 和元数据的 edit log)的写入提供了两种策略:一种是在每次写入后强制刷入磁盘;另外一种是每次写入后不强制刷入磁盘, 而是由操作系统决定何时刷入磁盘。强制刷盘策略保证了数据的安全,但是会降低数据库的写入性能。在实践中,一种既经济又能提升性能的策略是将数据库 log 写入容量较小但性能较高的 SSD 盘, 将数据写入容量大但性能较低的 HDD 盘。如果应用场景对数据的安全和可靠性要求较高,可以设置 dataSync=1 启用强制刷盘策略。

通用场景下,数据写入后会直接刷盘,若每次进行小批量写入会极大影响磁盘 I/O 性能以及压缩效果。为此,DolphinDB 维护了一个 Cache Engine,以实现数据缓存后批量写入的功能。 Cache Engine 可由配置参数 OLAPCacheEngineSize开启。写入缓存功能必须与 Redo Log 配合使用,即开启 Cache Engine 时必须指定 dataSync = 1。 当缓存中的数据量达到阈值(OLAPCacheEngineSize的30%)时,系统才会将数据写入磁盘,这样避免了频繁的磁盘操作。

启用 TSDB 引擎必须强制开启 Cache Engine,可以通过配置参数 TSDBCacheEngineSize 为它指定 Cache Engine 的容量。

TSDB 引擎的数据先写入 Cache Engine,在 Cache Engine 内进行分区,然后按照 sortColumns 进行排序。当写入的数据达到 TSDBCacheEngineSize 时,进行刷盘,若不满足该条件, 超过十分钟会强制刷盘。

TSDB 的数据在磁盘存储为 level file。level file 内的数据存储单位是数据块。在 level file 内维护了一个数据块的索引。 数据块之间先按照 sort key 进行排序,然后按照对应的列连续地存储在一起。在同一数据块内,数据按照 sortColumns 的时间列排序。

各 level file 文件分级存储(level 0, 1, 2, 3),数据首先存在 level 0 内,当 level 0 内所有 level file 的数量超过10个或总大小大于 level 1 单个文件的容量上限(256M), 系统会将 level 0 内所有 level file 合并压缩为level 1 内的 level file,以此类推。通过不断进行文件的合并压缩,可以有效控制 TSDB 内 level file 数量。当 level 0 的 level file 总数过多时,也可通过函数 triggerTSDBCompaction 手动触发 level 0 文件的合并,减少 level file 数量以提升查询效率。

数据读取

OLAP 引擎将每列数据存储为一个列文件,所以读取数据时,需要从磁盘读取该列所在的分区,解压后加载到内存。这种数据读取方式使得 OLAP 引擎在高吞吐量查询情况下拥有较好的性能。 但若需要更新或删除某条数据,OLAP 会将整个分区数据加载到内存中,再对这条数据进行更新或删除,因此性能开销大。

查询数据时,TSDB 引擎会将查询数据分区内 level file 的索引部分加载内存,然后根据索引,定位数据块的位置。因此若查询语句中按 sortKey 顺序指定过滤条件,可以快速定位到数据所在的位置。

因此这种数据读取方式,非常适合单点查询,因为单点查询可以通过索引快速定位到对应的文件,且只需要加载对应数据块到内存,无需加载整个分区,所以查询性能极优。 但注意,数据量过大可能导致 level file 数量较多,此时TSDB 的单点查询效率就会降低。

适用场景

OLAP 和 TSDB 都为海量结构化数据的存储、检索、分析和计算设计。通过上文的介绍,可以了解到 OLAP 引擎和 TSDB 引擎在数据存储、读取和写入上存在差别。下面列出它们主要应用场景:

OLAP 引擎主要应用场景:

  • 需要高吞吐量查询;

  • 需要扫描分析大量数据,全表扫描等场景,如:查询所有股票在某个时间段内的交易量等。

OLAP 引擎不适合以下场景:

  • 需要存储多列,比如列数超过1000个;

  • 查询某个 key (设备或股票)在某个时间点或时间段的数据。

TSDB 引擎相较于 OLAP 引擎无上述限制,尤其适合以下场景:

  • 单点查询,如查询某个或某几个设备(股票)在给定某个时间段内的数据;

  • 需要对数据进行去重存储;

  • 支持存储数组向量(array vector)。