DolphinDB是一款分析型的分布式时序数据库,内置处理流式数据处理引擎,具有内置的并行和分布式计算的功能,并提供分布式文件系统,支持集群扩展。DolphinDB以C++编写,响应速度极快。提供类似于Python的脚本语言对数据进行操作,支持类标准SQL的语法。提供其它常用编程语言的API,方便与已有应用程序集成。在金融领域中的历史数据分析建模与实时流数据处理,以及物联网领域中的海量传感器数据处理与实时分析等场景中表现出色。
InfluxDB是一款开源的时序数据库,由Go语言实现。它的核心是一款定制的存储引擎TSM Tree,对时间序列数据做了优化,优先考虑插入和查询数据的性能。InfluxDB使用类SQL的查询语言InfluxQL,并提供开箱即用的时间序列数学和统计函数。适用于监控、实时分析、物联网、传感器数据等应用场景。是目前最为流行的时间序列数据库。
本文将会对DolphinDB和InfluxDB进行性能测试对比。
在本次测试中,硬件配置如下:
设备:DellXPS 8920(07DC)
CPU:Inter® Core™ i7-7700 CPU @ 3.60GHz,4核心8线程
内存:16GB
硬盘:512GB SSD
操作系统:Ubuntu 16.04 x64
由于InfluxDB集群版本闭源,所以本次测试使用的DolphinDB和InfluxDB均为单机版本,并且所有配置项都是默认配置。
1. 数据生成
在本次测试中,我们使用NYSE网站上2016年10月24日纽约交易所的股票交易数据生成了Quotes_Big表。在其中取出一部分数据构成Quotes_Small表。表结构如表1所示。Quotes_Big表中的数据量为78,721,394条,Quotes_Small表中数据量为18,314,172条。数据下载链接及预处理脚本详见附录1。
由于DolphinDB和InfluxDB在存储方式上的差异,我们采用如下设计:我们将Time列指定为InfluxDB中的timestamp;将Exchange和Symbol列指定为InfluxDB中的Tag列(类似于带索引的列);将Bid_Price、Bid_Size、Offer_Price、Offer_Size指定为InfluxDB中的field列(类似于无索引的列)。在DolphinD系统中根据Symbol列在磁盘上进行RANGE分区,分为8个区。
表1.数据类型映射
2. 数据库查询性能测试
我们对11种常用的SQL查询进行了对比测试。Quotes_Small表的测试结果如下表2所示,Quotes_Big表的测试结果如下表3所示。其中,对于DolphinDB的测试,我们使用的是DolphinDB官方的GUI;对于InfluxDB,我们使用的是官方的PythonAPI。执行时间以毫秒为单位,只包含查询本身执行的时间,而不包含结果显示的时间。为了减少特殊值的影响,每个查询都执行10次,表中数据是10次查询的总时间。本次测试的脚本详见附录2。
在几乎所有测试中,DolphinDB的性能都领先InfluxDB多倍,某些情况下的差距甚至超过2个数量级(100倍)。InfluxDB唯一领先DolphinDB的测试是在Quotes_Small表的第4个测试项目,这是由于在数据量较小时DolphinDB并行搜索分区的优势不明显。然而,在数据量较大的情况下,DolphinDB在所有测试中上的性能都优于InfluxDB。DolphinDB可以并行搜索分区的设计使它在过滤性查询方面的性能全面超过了InfluxDB。
表2. Quotes_Small表查询性能测试结果(数据量:18,314,172)
表3. Quotes_Big表查询性能测试结果(数据量:78,721,394)
3. 内置函数计算性能测试
我们测试了8个DolphinDB和InfluxDB都提供的常用内置函数。在所有测试中,DolphinDB的性能都优于InfluxDB 1-2个数量级(10-100倍)。由于DolphinDB对带有滑动窗口的函数极好的优化,在“滑动平均值”(moving_average)的测试中,DolphinDB的性能领先InfluxDB 3个数量级以上(>1000倍)。
InfluxDB在Quotes_Big表的标准差与滑动平均值计算中在长时间的卡顿后引发内存不足的问题(out of memory),说明InfluxDB对大规模数据的复杂分析场景支持不足。而DolphinDB在此方面性能优异。
Quotes_Small表的测试结果如下表4所示,Quotes_Big表的测试结果如下表5所示。表格中数据的单位为毫秒。为减少特殊值的影响,每个计算都进行了10次。测试脚本详见附录3。
表4. Quotes_Small表计算性能测试结果(数据量:18,314,172)
表5. Quotes_Big表计算性能测试结果
4. I/O性能测试
我们分别使用了DolphinDB和InfluxDB的JavaAPI,比较了在相同的环境下写入相同规模的数据所需要的时间。由于InfluxDB的默认batch-size为5000,因此在两个数据库的写入程序中,单次插入的数据量都为5000条。我们还比较了从数据库中导出同样规模的csv文件所需要的时间。比较的结果是,无论是写入性能还是导出性能,DolphinDB都优于InfluxDB。对于相同规模的18,314,172条数据,DolphinDB的导出速度为InfluxDB的2倍;写入速度约为12倍。
测试结果如下表6所示。表格中的数据单位为毫秒。测试代码详见附录4。
表6. I/O性能测试结果
5. 磁盘占用空间测试
我们比较了Quotes_Small表和Quotes_Big表由DolphinDB和InfluxDB存储后在磁盘上的占用空间。结果显示,两款数据库都对数据进行了压缩存储,压缩率大致处于同一个数量级,都在20%-30%之间,但DolphinDB的压缩效果更佳。
测试结果如下表7所示,表中的数据单位为MB。
表7. 磁盘占用空间测试结果
6.其他方面对比
DolphinDB除了在基准测试中体现出优越的性能之外,还具有如下优势:
(1)InfluxDB通过InfluxQL来操作数据库,这是一种类SQL语言;而DolphinDB内置了完整的脚本语言,不仅支持SQL语言,而且支持命令式、向量化、函数化、元编程、RPC等多种编程范式,可以轻松实现更多的功能。
(2)InfluxDB对于特定文件格式数据例如csv文件的批量导入没有很好的官方支持。用户只能通过开源第三方工具或自己实现文件的读取,规整为InfluxDB指定的输入格式,再通过API进行批量导入。单次只能导入5000行,不仅操作复杂,效率也极其低下。与之对比,DolphinDB的脚本语言中提供了loadText、loadTextEx函数,用户可以在脚本中直接导入txt或csv文件,而且效率更高,对用户更友好。
(3)DolphinDB提供400余种内置函数,可满足金融领域的历史数据建模与实时流数据处理,及物联网领域中的实时监控与数据实时分析处理等不同的场景需求。提供时序数据处理需要的领先、滞后、累积窗口、滑动窗口等多种指标的函数,且在性能上进行了优化,性能极优。 因而与InfluxDB相比,DolphinDB拥有更多的适用场景。
(4)InfluxDB不支持表连接,而DolphinDB不仅支持表连接,还对asof join及window join等非同时连接方式做了优化。
(5)InfluxDB中,对时间序列的分组(GroupBy)最大单位是星期(week);而DolphinDB支持对所有内置时间类型的分组,最大单位为月(month)。因此在时间序列这一特性上,DolphinDB也有更好的支持。
(6)DolphinDB采用分布式表引擎时支持事务,而且在一个分区的多个副本写入时,保证强一致性。
附录
附录1. 数据下载即预处理脚本
数据下载链接:nyxdata
在DolphinDB中进行数据预处理,预处理脚本如下:
DATA_DIR = "…"
PTNDB_DIR = DATA_DIR + "/NYSETAQSeq"
db = database(PTNDB_DIR, SEQ, 16)
def convertTime(x) {
return string(nanotimestamp(2016.10.24).temporalAdd(x/1000, "us")).substr(0, 26)
}
// 产生 Quotes_Big.csv
Quotes = loadTextEx(db, `Quotes, , DATA_DIR+"/EQY_US_ALL_NBBO_20161024", '|')
Quotes_Big = select convertTime(time) as Time, Exchange, Symbol, Bid_Price, Bid_Size, Offer_Price, Offer_Size from Quotes
saveText(Quotes_Big, DATA_DIR + "/Quotes_Big.csv")
//产生Quotes_Small.csv
symbols = exec distinct Symbol from Quotes
symbols = symbols[0..(symbols.size() - 1) % 4 == 3]
Quotes_Small = select * from Quotes_Big where Symbol in symbols
saveText(Quotes_Small, DATA_DIR + "/Quotes_Small.csv")
// 建立数据库和数据表
if (existsDatabase(PTNDB_DIR)) {
dropDatabase(PTNDB_DIR)
}
sym = `A`D`G`J`M`P`S`V`Z
db = database(PTNDB_DIR, RANGE_PTN, symbol(sym))
// Quotes_Big表
Quotes = loadTextEx(db, `Quotes, `Symbol, DATA_DIR + “/Quotes_Big.csv”)
// Quotes_Small表
Quotes = loadTextEx(db, `Quotes, `Symbol, DATA_DIR + “/Quotes_Small.csv”)
附录2. 数据库查询性能脚本
表8. InfluxDB查询性能测试用例
表9. DolphinDB查询性能测试用例
附录3. 内置函数计算性能测试用例
表10. InfluxDB内置函数计算性能测试用例
表11. DolphinDB内置函数计算性能测试用例
附录4. I/O性能测试程序
InfluxDBInsert.java:
package test;
import org.influxdb.InfluxDB;
import org.influxdb.InfluxDBFactory;
import org.influxdb.dto.BatchPoints;
import org.influxdb.dto.Point;
import java.util.concurrent.TimeUnit;
public class InfluxDBInsert {
public static void main(String[] args) {
InfluxDBinfluxDB = InfluxDBFactory.connect("http://192.168.1.30:8086");
int size = 18314172;
long timer = 0;
for (int i = 0; i< size; i++) {
BatchPoints.BuilderbatchBuilder = BatchPoints.database("TEST");
for (int j = 0; j < 5000 &&i< size; i++, j++) {
Point.Builder builder = Point.measurement("TEST")
.time(System.nanoTime(), TimeUnit.NANOSECONDS)
.addField("Bid_Price", 2.5)
.addField("Bid_Size", 5)
.addField("Offer_Price", 12.5)
.addField("Offer_Size", 6)
.tag("Exchange", "B")
.tag("Sym", "ALV");
batchBuilder.point(builder.build());
}
long start = System.currentTimeMillis();
influxDB.write(batchBuilder.build());
timer += (System.currentTimeMillis() - start);
}
System.out.println(timer);
}
}
DolphinDBInsert.java:
package test;
import com.xxdb.DBConnection;
import com.xxdb.data.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class DolphinDBInsert {
public static void main(String[] args) throws IOException {
int size = 18314172;
int range = 5000;
BasicLongVector Time = new BasicNanoTimestampVector(range);
BasicDoubleVectorBid_Price = new BasicDoubleVector(range);
BasicIntVectorBid_Size = new BasicIntVector(range);
BasicDoubleVectorOffer_Price = new BasicDoubleVector(range);
BasicIntVectorOffer_Size = new BasicIntVector(range);
BasicStringVector Exchange = new BasicStringVector(range);
BasicStringVector Symbol = new BasicStringVector(range);
DBConnection conn = new DBConnection();
conn.connect("192.168.1.30", 8888);
conn.run("if(existsDatabase('/home/psui/Desktop/workspace/data/NYSETAQ')){dropDatabase('/home/psui/Desktop/workspace/data/NYSETAQ')}");
conn.run("db = database('/home/psui/Desktop/workspace/data/NYSETAQ', RANGE, 0 5 10)");
conn.run("tbl = table(200:0,`Time`Bid_Price`Bid_Size`Offer_Price`Offer_Size`Exchange`Symbol,[NANOTIMESTAMP,DOUBLE,INT,DOUBLE,INT,STRING,SYMBOL])");
conn.run("Quotes = db.createPartitionedTable(tbl,'Quotes','Bid_Size')");
long timer = 0;
for (int i = 0; i< size; i++) {
if (i + 5000 >= size) {
Time = new BasicNanoTimestampVector(size-i);
Bid_Price = new BasicDoubleVector(size-i);
Bid_Size = new BasicIntVector(size-i);
Offer_Price = new BasicDoubleVector(size-i);
Offer_Size = new BasicIntVector(size-i);
Exchange = new BasicStringVector(size-i);
Symbol = new BasicStringVector(size-i);
for (int j = 0; i< size; i++, j++) {
Time.setLong(j, System.nanoTime());
Bid_Price.setDouble(j, 2.4);
Bid_Size.setInt(j, 6);
Offer_Price.setDouble(j, 3.5);
Offer_Size.setInt(j, 2);
Exchange.setString(j, "A");
Symbol.setString(j, "ALV");
}
} else {
for (int j = 0; j < 5000 &&i< size; i++, j++) {
Time.setLong(j, System.nanoTime());
Bid_Price.setDouble(j, 2.4);
Bid_Size.setInt(j, 6);
Offer_Price.setDouble(j, 3.5);
Offer_Size.setInt(j, 2);
Exchange.setString(j, "A");
Symbol.setString(j, "ALV");
}
}
Map<String, Entity> map = new HashMap<>();
map.put("Time", Time);
map.put("Bid_Price", Bid_Price);
map.put("Bid_Size", Bid_Size);
map.put("Offer_Price", Offer_Price);
map.put("Offer_Size", Offer_Size);
map.put("Exchange", Exchange);
map.put("Symbol", Symbol);
long start = System.currentTimeMillis();
conn.upload(map);
conn.run("tbl = table(Time,Bid_Price,Bid_Size,Offer_Price,Offer_Size,Exchange,Symbol)");
conn.run("Quotes.append!(tbl)");
timer += (System.currentTimeMillis()-start);
}
conn.run("Quotee = loadTable(db, 'Quotes')");
Entity entity = conn.run("select count(*) from Quotee");
System.out.println(entity.getString());
System.out.println(timer);
}
}
数据导出:
InfluxDB:
$ influx -host localhost -database Quotes -format csv -execute “” >Quotes.csv
DolphinDB:
saveText(Quotes, DATA_DIR + "/Quotes.csv")