DolphinDB 用户入门指南之金融篇(2)
3. 创建数据库和数据表
为了满足海量数据存储和查询的需要,DolphinDB 将数据表按照分区划分成小粒度的数据块,分布式地存储在各个节点上;此外根据不同的应用场景,DolphinDB 开发设计了不同的存储引擎。因此,在建库建表时,比起传统的数据库,DolphinDB 需要额外设置更复杂的配置参数,例如分区方案、存储引擎等等。
为了循序渐进地进行理解和学习,本章将从分布式和分区设计开始讲解,然后以一个具体的建库建表案例进行分析,以帮助用户进行理解。
3.1 分布式架构与数据分区
分布式是 DolphinDB 实现海量数据存储、并行计算提速的关键架构。分布式架构对用户开放的“接口”叫做分布式表,在 DolphinDB 中指路径以 “dfs://” 开头的数据库下存储的表。
DolphinDB 的分布式是基于分布式文件系统实现的,是一个逻辑上的概念,而不是指物理上的多机存储。即使是单服务器单节点,也可以创建一个分布式表。分布式表的数据按 1 个或多个维度,在逻辑上分成很多个部分,每一个部分称之为分区。

图 3-1 单节点范围分区目录示意图
为了保证数据的高可用性、可扩展性,提升系统的负载均衡,用户可以部署一个分布式集群。
在分布式集群中,控制节点负责集群的监控、元数据管理,多个控制节点(至少 3 个)可以构成一个 raft 组,以实现高可用集群部署。数据存储和计算的工作由集群中的数据节点和计算节点负责,控制节点不参与此类工作。数据节点和计算节点既可以部署在同一台物理机,也可以部署在不同的物理机,每台物理机通过一个代理节点对其上部署的数据节点和计算节点进行管理。
数据节点负责数据存储,同时也支持内存计算,在分布式集群中,分区可以按照设定包含固定数量的副本,分散地存储在数据节点上;计算节点只支持计算,不支持存储,它主要用于一些复杂且开销较大的计算任务。
注:对于单节点部署的 server 实例,它既是控制节点又是数据节点。
以一个 3 个数据节点的集群为例,数据按天值分区同时按股票代码哈希分区,双副本存储。集群中的数据存储架构如下图所示(最小的蓝色方块表示分区的一个副本):

图 3-2 分布式数据存储架构
分布式存储必然涉及到分布式的存储/查询/计算,因此 DolphinDB 数据库实现了事务对分布式的支持。DolphinDB 的分布式实现以 map-reduce 框架为核心。当用户发起分布式查询时,查询语句会下沉(map)到各个分区实现并行计算,最后汇总(merge)计算结果到查询发起节点,如有必要,还可以基于汇总结果做进一步计算(reduce)。

图 3-3 mr 框架示意图
3.2 设置合理分区
数据分区主要发挥以下作用:
- 缩小查询范围:如果查询条件包含分区列,则可以通过剪枝操作,快速过滤掉不必要的分区查询,避免全表扫描带来的开销。
- 提高计算性能:将数据分散存储在不同的节点上,系统可以并行处理计算请求,提高数据处理速度。
- 提高可靠性和容错能力:通过数据复制和冗余存储,分布式系统可以在某些节点失败时继续运行,不会丢失数据,从而提高系统的可靠性。节点故障时,也可以从分区副本中恢复数据。
- 增强可扩展性:分布式存储系统可以添加更多的节点来存储数据分区,从而实现水平扩展,以应对数据量的增长。
分区粒度(即每个分区包含的数据量大小)会对查询性能造成影响,这里总结为:
- 分区粒度过大:单分区数据量大,查询时 IO 开销高。受到 license 内存限制,可能导致内存溢出、内存磁盘频繁切换等问题。
- 分区粒度过小:单分区数据量小,查询涉及分区多,查询子任务数量大,将造成节点间通讯成本增加。此外,由于控制节点存储分区元数据,分区过多,控制节点元数据信息爆炸,可能导致控制节点内存不足。最后,由于涉及的分区多,导致系统读写频繁,从而造成很多低效的磁盘访问(小文件读写),造成系统负荷过重。
用户建表时需要指定分区字段,系统将依据分区字段的值按照分区模式划分分区,分区模式由用户在建库时指定。下面结合图例对不同分区模式的机制进行说明,假设原始数据为:

图 3-4 示例原始数据
- VALUE 分区:用户需要指定分区字段的部分值(以列表形式提供,如以日期分区,可以提供 2022.01.01..2022.01.31),数据依据分区字段值进行分区,一个值对应一个分区。
注意:官网下载的安装包中的配置项默认设置了 newValuePartitionPolicy=add,即支持写入不在值分区内的数据时,值分区也可以自动拓展。因此,用户在配置 VALUE 分区的模式时,可以只指定少量字段值,以避免元数据过多(例如用户设置了 2000.01.01..2030.01.01,会造成分区元数据很大)。

图 3-5 Date 字段按天进行 VALUE 分区
- HASH 分区:用户指定固定的 HASH 分区数,数据依据分区字段值调用哈希函数 hashBucket 映射后的结果进行分区,每个哈希值对应一个分区。

图 3-6 SID 字段按 HASH 分区划分为 50 个桶
- RANGE 分区:用户指定分区范围,数据依据分区字段值划分到对应的范围中,一个范围对应一个分区。范围分区不支持自动拓展,用户需要手动调用函数 addRangePartitions。

图 3-7 Date 字段按年进行 RANGE 分区
- LIST 分区:用户指定以列表形式指定哪些值属于哪些。实践中,金融场景客户很少使用这种分区方式。
- COMPO 分区:将多个字段的不同的分区方案进行组合。
注:参考阅读:分区数据库设计和操作
金融领域通常将时间字段作为分区字段,指定分区模式时,最常用的是 VALUE 和 RANGE 分区,如日 K 线数据按照年划分 RANGE 分区,分钟 K 线按照日划分 VALUE 分区等。但是部分场景下,仅依靠一层分区,仍无法控制分区在合理的范围,此时可以通过 COMPO 组合分区进行更细粒度的划分,例如逐笔数据按照日划分 VALUE 分区,同时按照股票代码 HASH 分区。 HASH 分区是灵活调整分区粒度的一个重要的方式。
调整分区粒度的方法:
- 降低分区粒度:(1)采用 COMPO 分区;(2)增加分区个数;(3)将 RANGE 分区改为 VALUE 分区。
- 增加分区粒度:(1)采用 RANGE 分区取代 VALUE 分区;(2)HASH 分区减小哈希映射数。
合理的分区大小推荐:
- OLAP 引擎:100MB - 300MB (压缩前)
- TSDB/PKEY 引擎:400MB - 1GB(压缩前)
选择合适的存储引擎:
分布式数据可选的存储引擎有 TSDB, OLAP 和 PKEY,其中 TSDB 和 OLAP 是较为基础的两个存储引擎。TSDB 引擎相较于 OLAP 引擎而言,底层设计更加复杂,详情可参考 TSDB 存储引擎简介 。从用户层面来说,最显著的功能特点就是 TSDB 除了分区机制外,还额外增加一层字段索引机制,因而更适用于大数据点查的场景。对于较小的数据量可以继续用 OLAP 存储甚至存储为无分区的维度表。如果对数据有主键约束的需求可以使用 PKEY 引擎进行存储。
常见的金融数据分区方案参见:存储金融数据的分区方案最佳实践。此处摘选了一部分股票的分区方案供参考:
表 3-1 不同种类股票数据的分区方案示例
| 产品大类 | 数据集 | 存储引擎 | 分区方案 | 分区列 | 排序列 |
|---|---|---|---|---|---|
| 股票 | Level2 快照(沪深分表) | TSDB | 按天 VALUE 分区 + 按股票代码 HASH25 分区 | 交易日期 + 股票代码 | 股票代码 + 交易时间 |
| 股票 | Level1 快照(沪深分表) | TSDB | 按天 VALUE 分区 + 按股票代码 HASH25 分区 | 交易日期 + 股票代码 | 股票代码 + 交易时间 |
| 股票 | 逐笔委托(沪深分表) | TSDB | 按天 VALUE 分区 + 按股票代码 HASH25 分区 | 交易日期 + 股票代码 | 股票代码 + 交易时间 |
| 股票 | Level1 快照(沪深合并) | TSDB | 按天 VALUE 分区 + 按股票代码 HASH50 分区 | 交易日期 + 股票代码 | 交易所类型 + 股票代码 + 交易时间 |
| 股票 | 逐笔委托(沪深合并) | TSDB | 按天 VALUE 分区 + 按股票代码 HASH50 分区 | 交易日期 + 股票代码 | 交易所类型 + 股票代码 + 交易时间 |
| 股票 | 逐笔成交(沪深合并) | TSDB | 按天 VALUE 分区 + 按股票代码 HASH50 分区 | 交易日期 + 股票代码 | 交易所类型 + 股票代码 + 交易时间 |
| 股票 | 日 K 线 | OLAP | 按年 RANGE 分区 | 交易时间 | 无 |
| 股票 | 分钟 K 线 | OLAP | 按天 VALUE 分区 | 交易时间 | 无 |
注:
1. 分区方案在 DolphinDB 创建数据库时指定,指定后,该数据库下的表将以该方案划分数据。以一个具体的建库代码为例,create database "dfs://stock_lv2_snapshot" partitioned by VALUE(2024.01.01..2024.01.02), HASH([SYMBOL, 50]), engine='TSDB',其中 partition by 用于指定分区方案,即VALUE(2024.01.01..2024.01.02), HASH([SYMBOL, 50]),该方案表示的含义如下:
- 按天 VALUE 分区:属于同一天的数据都存储在同一个分区
- 按 SYMBOL 类型的字段(如股票代码)HASH 50 分区:对分区字段调用哈希函数 hashbucket,buckets 参数为 50,将数据映射到 0~49 中的一个键,属于同一个键的数据都存储在同一个分区。
engine用于指定存储引擎,本例中为 TSDB。
2. 排序列是 TSDB 引擎特有的一个参数(sortColumns),用于设置 TSDB 引擎的索引机制,OLAP 引擎无需指定排序列。
3. DolphinDB 的事件类型精度最粗到月,例如 2021.01M,因此对于按年分区,无法通过值分区实现,必须使用 RANGE 分区,参考代码:
dataRange = 2020.01M + (0..10)*12
create database "dfs://year" partitioned by RANGE(dataRange)3.3 建库建表
DolphinDB 可以通过函数建库建表,也可以使用 SQL 进行建库建表,本教程以 SQL 编程进行示例,相关参考文档create。
3.3.1 快速创建一个数据库
以股票 Level2 快照(沪深合并)数据为例,创建一个数据库,参考金融数据分区方案最佳实践教程,分区方案如下:
表 3-2 沪深股票 Level2 快照数据分区方案
| 产品大类 | 数据集 | 存储引擎'engine' | 分区方案partitionType+partitionScheme |
| 股票 | Level2 快照(沪深合并) | TSDB | 按天分区 + 按股票代码 HASH50 分区 |
// 环境清理:如果重复试验可执行此步,请确保自身环境内不存在同名数据库,否则会被删除
drop database if exists "dfs://stock_lv2_snapshot"
// 创建一个路径为 "dfs://stock_lv2_snapshot" 的数据库,按照日期值 + SYMBOL 哈希组合分区,存储引擎为 TSDB
create database "dfs://stock_lv2_snapshot" partitioned by VALUE(2024.01.03..2024.01.04), HASH([SYMBOL, 50]), engine='TSDB'3.3.2 分区方案确立规则(可跳过)
为了能够在更通用的场景确立分区方案,下面以模拟的快照数据为例进行演示。
STEP1. 数据量估算
假设一共需要存储 5000 只股票的快照数据。先模拟一天单只股票的数据,数据量为 4802 行。
use MockData
t = stockSnapshot(tradeDate=2020.01.06, securityNumber=1)
exec count(*) from t
// output: 4802为了估算整体的数据量大小,可以先估算单行数据的大小。下面给出两种单行数据量的估算方法。
方法一:根据表字段的存储的字节数估算。
通常而言,用户自己在存储数据时,都会维护一个字段信息表。这里为了方便大家理解,直接调用 schema 函数获取模拟数据的字段信息。
// 调用 schema 函数获取表字段信息(字段名,类型)
s = schema(t).colDefs
// output:
name typeString typeInt extra comment
------------------ ---------- ------- ----- -------
TradeDate DATE 6
TradeTime TIME 8
MDStreamID SYMBOL 17
Securityid SYMBOL 17
SecurityIDSource SYMBOL 17
TradingPhaseCode SYMBOL 17
ImageStatus INT 4
PreCloPrice DOUBLE 16
......下表在 schema 字段信息的基础上补充了字段含义说明和字节数的信息,字节数是对照表数据类型获取的,对于数组向量,这里的估算方式为:单行数据大小 = Σ字段类型对应字节数。
字段信息表(附录)
| 字段含义 | 入库字段名 | 入库数据类型 | 字节数 | |
|---|---|---|---|---|
| 1 | 数据生成日期 | TradeDate | DATE | 4 |
| 2 | 数据生成时间 | TradeTime | TIME | 8 |
| 3 | 行情类别 | MDStreamID | SYMBOL | 4 |
| 4 | 证券代码 | SecurityID | SYMBOL | 4 |
| 5 | 证券代码源 | SecurityIDSource | SYMBOL | 4 |
| 6 | 交易阶段 | TradingPhaseCode | SYMBOL | 4 |
| 7 | 快照类型(全量/更新) | ImageStatus | INT | 4 |
| 8 | 昨日收盘价 | PreCloPrice | DOUBLE | 8 |
| 9 | 成交笔数 | NumTrades | LONG | 8 |
| 10 | 成交总量 | TotalVolumeTrade | LONG | 8 |
| 11 | 成交总金额 | TotalValueTrade | DOUBLE | 8 |
| 12 | 最近价 | LastPrice | DOUBLE | 8 |
| 13 | 开盘价 | OpenPrice | DOUBLE | 8 |
| 14 | 最高价 | HighPrice | DOUBLE | 8 |
| 15 | 最低价 | LowPrice | DOUBLE | 8 |
| 16 | 今收盘价 | ClosePrice | DOUBLE | 8 |
| 17 | 升跌1(最新价-昨收价) | DifPrice1 | DOUBLE | 8 |
| 18 | 升跌2(最新价-上一最新价) | DifPrice2 | DOUBLE | 8 |
| 19 | 股票市盈率1 | PE1 | DOUBLE | 8 |
| 20 | 股票市盈率2 | PE2 | DOUBLE | 8 |
| 21 | 基金T-1净值 | PreCloseIOPV | DOUBLE | 8 |
| 22 | 基金实时参考净值 | IOPV | DOUBLE | 8 |
| 23 | 委托买入总量 | TotalBidQty | LONG | 8 |
| 24 | 加权平均买入价格 | WeightedAvgBidPx | DOUBLE | 8 |
| 25 | 债券加权平均委买价格 | AltWAvgBidPri | DOUBLE | 8 |
| 26 | 委托卖出总量 | TotalOfferQty | LONG | 8 |
| 27 | 加权平均卖出价格 | WeightedAvgOfferPx | DOUBLE | 8 |
| 28 | 债券加权平均委卖价格 | AltWAvgAskPri | DOUBLE | 8 |
| 29 | 涨停价 | UpLimitPx | DOUBLE | 8 |
| 30 | 跌停价 | DownLimitPx | DOUBLE | 8 |
| 31 | 持仓量 | OpenInt | INT | 4 |
| 32 | 权证溢价率 | OptPremiumRatio | DOUBLE | 8 |
| 33 | 卖价10档 | OfferPrice | DOUBLE[] | 80 |
| 34 | 买价10档 | BidPrice | DOUBLE[] | 80 |
| 35 | 卖量10档 | OfferOrderQty | LONG[] | 80 |
| 36 | 买量10档 | BidOrderQty | LONG[] | 80 |
| 37 | 申买10档委托笔数 | BidNumOrders | INT[] | 40 |
| 38 | 申卖10档委托笔数 | OfferNumOrders | INT[] | 40 |
| 39 | ETF申购笔数 | ETFBuyNumber | INT | 4 |
| 40 | ETF申购数量 | ETFBuyAmount | LONG | 8 |
| 41 | ETF申购金额 | ETFBuyMoney | DOUBLE | 8 |
| 42 | ETF赎回笔数 | ETFSellNumber | INT | 4 |
| 43 | ETF赎回数量 | ETFSellAmount | LONG | 8 |
| 44 | ETF赎回金额 | ETFSellMoney | DOUBLE | 8 |
| 45 | 债券到期收益率 | YieldToMatu | DOUBLE | 8 |
| 46 | 权证执行的总数量 | TotWarExNum | DOUBLE | 8 |
| 47 | 买入撤单笔数 | WithdrawBuyNumber | INT | 4 |
| 48 | 买入撤单数量 | WithdrawBuyAmount | LONG | 8 |
| 49 | 买入撤单金额 | WithdrawBuyMoney | DOUBLE | 8 |
| 50 | 卖出撤单笔数 | WithdrawSellNumber | INT | 4 |
| 51 | 卖出撤单数量 | WithdrawSellAmount | LONG | 8 |
| 52 | 卖出撤单金额 | WithdrawSellMoney | DOUBLE | 8 |
| 53 | 买入总笔数 | TotalBidNumber | INT | 4 |
| 54 | 卖出总笔数 | TotalOfferNumber | INT | 4 |
| 55 | 买入委托成交最大等待时间 | MaxBidDur | INT | 4 |
| 56 | 卖出委托最大等待时间 | MaxSellDur | INT | 4 |
| 57 | 买方委托价位数 | BidNum | INT | 4 |
| 58 | 卖方委托价位数 | SellNum | INT | 4 |
| 59 | 入库时间 | LocalTime | TIME | 4 |
| 60 | 消息序列号 | SeqNo | INT | 4 |
| 61 | 委托卖量50档 | OfferOrders | LONG[] | 400 |
| 62 | 委托买量50档 | BidOrders | LONG[] | 400 |
对照表计算出单行数据字节数,并估算单日单只股票的数据大小:
rowSize = 19 * 4 + 35 * 8 + 2 * 40 + 4 * 80 + 2 * 400
rowSize
// output: 1,556(B)
oneDayPerStock = 4802 * 1556 \ 1024 \ 1024
oneDayPerStock
// output: 7.12(MB)方法二:直接导入部分数据估算单行数据的字节数,并估算单日单只股票的数据量。
// 这里直接取模拟的第一行数据为例
rowSize = t[0].values().memSize()
// output: 1,728(B)
oneDayPerStock = 4802 * 1728 \ 1024 \ 1024
oneDayPerStock
// output: 7.9(MB)注: 这里以模拟数据为例,但是实际场景下,数据可能是来源于文件导入或者流订阅写入(插件、API等),此使需要把 t 替换成实际的数据源。
STEP2. 分区方案制定
根据实践经验,TSDB 存储引擎较合理的分区粒度是 400MB~1GB,根据上面的估算,单日数据量约为 7~8MB。以 8 MB 为基准,如果按月分区则单分区数据量约为 240MB,粒度略小。若设置为 2 个月一个分区,数据量满足需求,但是必须设置分区类型为范围分区(RANGE),而 RANGE 分区不支持自动拓展,随着数据量增长,可能会造成范围外的数据无法写入,用户需要手动调用函数 addRangePartitions 进行分区拓展。因此,对于时间类型,推荐使用可自动拓展分区的值分区(VALUE)作为分区类型。
注: 按年分区只能用 RANGE 分区方案,因为 DolphinDB 最粗粒度的时间类型仅支持到月,不支持年,因此无法通过 VALUE 分区实现分区。具体的年分区脚本参见 3.4 问题 1。
那么为了满足需求,这里可以提出两种分区方案:
- 方案一. 按日 VALUE 分区 + 按股票字段 HASH 分区:如果设置 HASH 桶数为 50,5000 支股票哈希后每个桶约为 100 支,则单分区数据量约为 800 MB,满足需求。
- 方案二. 按月 VALUE 分区 + 按股票字段 HASH 分区:单月单只股票的数据量是 240MB, 因此需要确保每个桶的股票数为 3 或者 4 才能满足需求,则 HASH 桶数为 1250 ~ 1666,满足需求。
STEP3. 分区方案评估
分析方案二,局限性如下:(1)HASH 桶数过多,每个桶只对应了 3~4 只股票;(2)快照查询的场景下,按日检索是一个高频的查询方式。在此场景下,按月分区的性能没有按日分区高。(3)一般场景下不会按照 HASH 值进行查询,HASH 分区更多是用在降低分区粒度的场景。
综上选取方案一,是一个较好的选择,即按天分区 + 按股票代码 HASH 50 分区。
3.3.3 快速创建一个数据表
DolphinDB SQL 建表语法和传统 SQL 类似,除了指定的表名、字段名、字段类型外,额外增加了一些数据库的配置参数。
create table dbPath.tableName (
schema[columnDescription]
// 字段名 字段类型 [comment = 字段注解, compress = 压缩方案, index = 向量索引]
)
[partitioned by partitionColumns], // 分区列
[sortColumns], // 排序列
[keepDuplicates=ALL], // 去重机制
[sortKeyMappingFunction] // 排序列映射函数
[softDelete=false] // 软删除配置
[comment] // 表注释配置参数的用法详见 createPartitionedTable 参数说明。
根据金融数据最佳实践方案,创建数据库 "dfs://stock_lv2_snapshot" 下的分布式表 "snapshot",设置分区列为 TradeDate(对应按日 VALUE 分区)和 SecurityID(对应 SYMBOL HASH 分区)。后续数据写入时,将依据这分区字段的值,将数据划分存储到不同的分区中。
表 3-3 沪深股票 Level2 快照数据分区方案
| 产品大类 | 数据集 | 分区列partitionColumns | 排序列sortColumns |
| 股票 | Level2 快照(沪深合并) | 交易日期 + 股票代码 | 交易所类型 + 股票代码 + 交易时间 |
create table "dfs://stock_lv2_snapshot"."snapshot"(
//comment添加字段注释,compress指定压缩方法
TradeDate DATE[comment="交易日期", compress="delta"]
TradeTime TIME[comment="交易时间", compress="delta"]
MDStreamID SYMBOL
SecurityID SYMBOL
SecurityIDSource SYMBOL
TradingPhaseCode SYMBOL
ImageStatus INT
PreCloPrice DOUBLE
NumTrades LONG
TotalVolumeTrade LONG
TotalValueTrade DOUBLE
LastPrice DOUBLE
OpenPrice DOUBLE
HighPrice DOUBLE
LowPrice DOUBLE
ClosePrice DOUBLE
DifPrice1 DOUBLE
DifPrice2 DOUBLE
PE1 DOUBLE
PE2 DOUBLE
PreCloseIOPV DOUBLE
IOPV DOUBLE
TotalBidQty LONG
WeightedAvgBidPx DOUBLE
AltWAvgBidPri DOUBLE
TotalOfferQty LONG
WeightedAvgOfferPx DOUBLE
AltWAvgAskPri DOUBLE
UpLimitPx DOUBLE
DownLimitPx DOUBLE
OpenInt INT
OptPremiumRatio DOUBLE
OfferPrice DOUBLE[]
BidPrice DOUBLE[]
OfferOrderQty LONG[]
BidOrderQty LONG[]
BidNumOrders INT[]
OfferNumOrders INT[]
ETFBuyNumber INT
ETFBuyAmount LONG
ETFBuyMoney DOUBLE
ETFSellNumber INT
ETFSellAmount LONG
ETFSellMoney DOUBLE
YieldToMatu DOUBLE
TotWarExNum DOUBLE
WithdrawBuyNumber INT
WithdrawBuyAmount LONG
WithdrawBuyMoney DOUBLE
WithdrawSellNumber INT
WithdrawSellAmount LONG
WithdrawSellMoney DOUBLE
TotalBidNumber INT
TotalOfferNumber INT
MaxBidDur INT
MaxSellDur INT
BidNum INT
SellNum INT
LocalTime TIME
SeqNo INT
OfferOrders LONG[]
BidOrders LONG[]
)
partitioned by TradeDate, SecurityID,
sortColumns=["SecurityID","TradeTime"],
keepDuplicates=ALL脚本说明:
与传统关系型数据库不同,DolphinDB create table 语句支持对分布式存储和分区的支持,对于分布式表,表名处指定的是 dbPath.tbName 即 “数据库名.数据表名”。此外,用户需要在字段声明后添加一部分分布式表的配置,如分区配置 partitioned by 等(部分配置只适用于对应的存储引擎,如 sortColumns 和 keepDuplicates 是 TSDB 引擎专属的配置)。
在字段类型声明中,INT[], LONG[], DOUBLE[] 是 DolphinDB 特有的 ARRAY 类型,用于声明数组向量字段,数组向量是一种特殊的向量,用于存储可变长度的二维数组,详情参考 数组向量(array vector) 。
思考题:
(1)模拟写入几天的数据,写入后,尝试重新查询出表中的数据,观察数据的查询顺序,思考下述两个问题:
- 查询顺序和写入顺序是否一致?它和分区之间的关系是怎么样的?
- 查询顺序是否按照 sortColumns 排序?若不是,则思考 sortColumns 起到怎么样的作用?
re1 = select top 10000 * from loadTable("dfs://stock_lv2_snapshot", "snapshot")
re2 = select top 10000 * from loadTable("dfs://stock_lv2_snapshot", "snapshot") order by SecurityID, TradeTime
eqObj(re1.values(), re2.values())模拟数据的写入脚本可以参考:
// 定义按天写入的函数
def writeDataByDate(date, dbName, tbName, stockNum): tableInsert(loadTable(dbName, tbName), stockSnapshot(tradeDate=date, securityNumber=stockNum))
// 写入几个交易日的数据,此处使用交易日历函数获取 5 天交易日
dates = getMarketCalendar('XSHE', 2022.01.01, 2022.01.10)
// [2022.01.04,2022.01.05,2022.01.06,2022.01.07,2022.01.10]
// each 函数循环在提交后台写入任务,一天一个 job,每个 job 返回写入的数据量
each(date -> submitJob("writeData2snapshot", "write by date", writeDataByDate{, "dfs://stock_lv2_snapshot", "snapshot", 100}, date), dates)
// 查看 job 的状态,获取所有 job 的返回结果
getRecentJobs(5)
each(getJobReturn, getRecentJobs(5).jobId)
// [480200,480200,480200,480200,480200]
// 查询表的总数据量,exec 查询返回一个标量
exec count(*) from loadTable("dfs://stock_lv2_snapshot", "snapshot")
// 2401000注: 阅读 TSDB 存储引擎详解
(2)以上表字段 bidPrice 和 offerPrice ,存储的是 10 档的价格。将其存储为一列 DOUBLE[] 类型字段和拆分成 10 列 DOUBLE 字段存储,两者的区别是什么,数组向量存储有什么优越性?
注: 阅读 Array Vector 的最佳实践指南
3.4 常见问题
分区
1. 创建数据库时如何按年进行分区?
DolphinDB 不支持年这一数据类型,因此无法按年进行值分区。按年分区需使用 RANGE 分区进行,可参考下述脚本:
create database "dfs://yearDB" partitioned by RANGE(date(2010.01M+12*1..10)), engine='TSDB'2. 建库建表完成后,是否支持修改分区方案?
由于数据存储是依赖于分区的,因此不支持直接修改分区方案。最佳做法是重新创建一个新分区方案的库表,然后将数据迁移过去,最后删除原来的库表。
可以借助 mr + repartitionDS 函数进行数据迁移(以本教程的快照数据为例):
// 创建一个新的分区方案的数据库表
drop database if exists "dfs://stock_lv2_snapshot_1"
create database "dfs://stock_lv2_snapshot_1" partitioned by VALUE(2022.01M..2022.12M), HASH([SYMBOL, 10]), engine='TSDB'
// 省略部分表字段代码
create table "dfs://stock_lv2_snapshot_1"."snapshot"(
TradeDate DATE[comment="交易日期", compress="delta"]
TradeTime TIME[comment="交易时间", compress="delta"]
...
)
partitioned by TradeDate, SecurityID,
sortColumns=[`SecurityID,`TradeTime],
keepDuplicates=ALL
// 由于 repartitionDS 只能按照一个字段重新分区,这里选用了不会造成新库表写入分区冲突的 TradeDate 字段
snapshot = loadTable("dfs://stock_lv2_snapshot", "snapshot")
ds = repartitionDS(<select * from snapshot>,`TradeDate, VALUE, 2022.01M..2022.12M);
// 定义 map 函数用于写入新的库表
def writeDS(dst_dbPath, dst_tbName, data){
dstTb=loadTable(dst_dbPath, dst_tbName)
dstTb.append!(data)
flushTSDBCache()
}
// mr 迁移数据,如果内存不够大:
1. 串行迁移,即 parallel = false
2. ds 数据源拆分成更小的单元,如按日拆分
mr(ds, writeDS{"dfs://stock_lv2_snapshot_1", "snapshot"}, parallel=false)
// 删除旧的库表
drop database if exists "dfs://stock_lv2_snapshot" 社区版默认配置执行上述代码时,可能会报错:The size of the write table cannot exceed the size of cacheEngine. 或者 Out of Memory. 推荐根据注释方案调整写入粒度,参考脚本如下:
ds = repartitionDS(<select * from snapshot>,`TradeDate, VALUE, 2022.01.01..2022.12.31);
mr(ds, writeDS{"dfs://stock_lv2_snapshot_1", "snapshot"}, parallel=false)数据库&数据表
1. 表字段命名的规范是什么?能否以数字开头?
创建表时,列名必须由中文或英文字母、数字或下划线 (_)组成,且必须以中文或英文字母开头。
有以下几种场景可能出现特殊列名,即不满足上述需求的列名:
as函数取别名:table(1 2 3 as "_aa")addColumn添加列:t.addColumn("_bb", INT)- pivot by 行列转换:
t = table(2022.01.01 + 1..5 as date, `a`a`c`c`b as sym, 1..5 as val)
select last(val) from t pivot by sym, date
// output:
sym 2022.01.02 2022.01.03 2022.01.04 2022.01.05 2022.01.06
--- ---------- ---------- ---------- ---------- ----------
a 1 2
b 5
c 3 4 2. 是否支持修改数据库名、数据表名、字段名?
不支持修改数据库名,但是可以通过函数 renameTable 和 rename! 修改表名和字段名,但需要注意 TSDB 引擎存储的分布式表不支持对字段进行改名、修改值和删除的操作。
3. 建库时 "dfs://" 路径是什么含义?此类库中的表存储在什么路径下?
"dfs://" 用于声明数据库的类型是分布式数据库,除此以外,还有 "oltp://" 用于声明在线事务处理数据库。所有 "dfs://" 开头的数据库中的数据都存储在 volumes 参数配置的路径下,可通过 getConfig("volumes") 查看。
单节点模式下,volumes 的默认目录是 <HomeDir>/storage。集群模式下,默认目录是<HomeDir>/<nodeAlias>/storage。

图 3-7 单节点环境下数据表存储路径示意图

图 3-8 集群环境下数据表存储路径示意图
4. 存储时,推荐选择宽表还是窄表?各自的适用的场景是什么?
- 宽表:截面计算便捷,数据写入速度上优于窄表;增删改基于字段操作,开销较大。
- 窄表:增删改基于记录操作,比较便捷开销较小;截面计算不便,需要通过 pivot 转换为宽表。
以中高频因子存储为例,由于增删因子操作较为频番,综合性能考虑窄表优于宽表,具体可参考 中高频多因子库存储最佳实践 中的分析步骤去评估是存储为宽表还是窄表。
5. 存储时,推荐选择分布式表还是维度表?各自的适用的场景是什么?
首先要理解维度表的概念,维度表实质上是一个持久化且不进行分区的表。前文提到分区的主要目的是将海量数据划分为小块,方便快速定位查找,因此如果数据量较小且需要持久化,则推荐存储为维度表,否则存储为分布式表。
6. 表中是否可以存储字典、多维数据等对象?
不支持存储字典,如果是类似 json 数据,可以通过 BLOB 超长字符串的格式存储在表中;表中只能存储数组向量、列式元组(仅内存表)等类型强一致的二维对象,不支持超过二维或者类型不一致的二维数组存储。
7. 是否支持多线程并发写入数据表,目前报错:filepath xxx has been owned by transaction.
DolphinDB 默认不允许对同一分区进行并发写入,否则会造成分区冲突,报上述错误。有一个方案是创建数据库时配置 atomic='CHUNK',设置后,系统允许用户多线程并发写入同一个分区,但由于不能完全保证事务的原子性,可能出现部分分区写入成功而部分分区写入失败的情况。同时由于采用了重试机制,写入速度可能较慢。
事务
1. 删除数据库是事务操作吗?是否支持回滚?
是的,如果删除数据库(dropDatabase)或者删除分区(dropParition)失败,系统会自动回滚事务。
2. 删除数据时产生下述报错,如何解决:
- 报错:deleteSubChunks failed, path "/xxx" does not exist.
- 报错:deleteSubChunks failed on '/dbName, chunk xxxx is not in COMPLETE state'
该报错是在数据节点上删除数据时,找不到对应的 chunk 分区信息时产生的。
下面给出一些此类问题的排查思路:
- 首先在控制节点调用函数
getClusterChunksStatus函数查看对应数据库下的分区状态信息,如果报错给出了 chunk 信息,则重点查看该 chunk 的状态。需要重点关注并记录的信息有 version(chunk 版本),state(chunk 状态) 和 replicaCount(副本数)。 - 然后在数据节点调用
pnodeRun(getAllChunks{"/dbName%"})统计一下所有 chunk 的副本数是否有缺失,然后针对报错的 chunk 记录一下 version 和 state 信息。 - 比较控制节点和数据节点 version 是否一致;检查副本数是否与控制节点记录的元数据一致;确认 chunk 状态是否都为完成状态(COMPLETE)。
若排查得到分区不一致,推荐将上述信息截图后提交给官方进行排查,有运维经验的用户可以参照教程 分区状态不一致 中的方法尝试解决。
- 报错:<ChunkInRecovery>deleteSubChunks failed on '/xxx', chunk xxx is in RECOVERING state.若等待分区恢复耗时过久,则可以通过下述脚本强制删除处在恢复状态的分区:
use ops
dropRecoveringPartitions(dbPath="dfs://xxx", tableName="zzz")
// tableName 为可选参数,可不指定3.5 下一步阅读
概念学习