DolphinDB内存表详解

内存表是DolphinDB数据库的重要组成部分。内存表不只能够直接用于存储数据,实现高速数据读写,并且能够缓存计算引擎的中间结果,加速计算过程。本教程主要介绍DolphinDB内存表的分类、使用场景以及各类内存表在数据操做以及表结构(schema)操做上的异同。redis


1. 内存表类别

根据不一样的使用场景以及功能特色,DolphinDB内存表能够分为如下四种:数据库

  • 常规内存表
  • 键值内存表
  • 流数据表
  • MVCC内存表


1.1 常规内存表数组

常规内存表是DolphinDB中最基础的表结构,支持增删改查等操做。SQL查询返回的结果一般存储在常规内存表中,等待进一步处理。缓存

  • 建立

使用table函数可建立常规内存表。table函数有两种用法:第一种用法是根据指定的schema(字段类型和字段名称)以及表容量(capacity)和初始行数(size)来生成;第二种用法是经过已有数据(矩阵,表,数组和元组)来生成一个表。安全

使用第一种方法的好处是能够预先为表分配内存。当表中的记录数超过容量时,系统会自动扩充表的容量。扩充时系统首先会分配更大的内存空间(增长20%到100%不等),而后复制旧表到新的表,最后释放原来的内存。对于规模较大的表,扩容的成本会比较高。所以,若是咱们能够事先预计表的行数,建议建立内存表时预先分配一个合理的容量。若是表的初始行数为0,系统会生成空表。若是初始行数不为0,系统会生成一个指定行数的表,表中各列的值都为默认值。例如:服务器

//建立一个空的常规内存表
t=table(100:0,`sym`id`val,[SYMBOL,INT,INT])

//建立一个10行的常规内存表
t=table(100:10,`sym`id`val,[SYMBOL,INT,INT])
select * from t

sym id val
--- -- ---
    0  0  
    0  0  
    0  0  
    0  0  
    0  0  
    0  0  
    0  0  
    0  0  
    0  0  
    0  0

table函数也容许经过已有的数据来建立一个常规内存表。下例是经过多个数组来建立。数据结构

sym=`A`B`C`D`E
id=5 4 3 2 1
val=52 64 25 48 71
t=table(sym,id,val)
  • 应用

常规内存表是DolphinDB中应用最频繁的数据结构之一,仅次于数组。SQL语句的查询结果,分布式查询的中间结果都存储在常规内存表中。当系统内存不足时,该表并不会自动将数据溢出到磁盘,而是Out Of Memory异常。所以咱们进行各类查询和计算时,要注意中间结果和最终结果的size。当某些中间结果再也不须要时,请及时释放。关于常规内存表增删改查的各类用法,能够参考另外一份教程内存分区表加载和操做多线程


1.2 键值内存表并发

键值内存表是DolphinDB中支持主键的内存表。经过指定表中的一个或多个字段做为主键,能够惟一肯定表中的记录。键值内存表支持增删改查等操做,可是主键值不容许更新。键值内存表经过哈希表来记录每个键值对应的行号,所以对于基于键值的查找和更新具备很是高的效率。mvc

  • 建立

使用keyedTable函数可建立键值内存表。该函数与table函数很是相似,惟一不一样之处是增长了一个参数指明键值列的名称。

//建立空的键值内存表,主键由sym和id字段组成
t=keyedTable(`sym`id,1:0,`sym`id`val,[SYMBOL,INT,INT])

//使用向量建立键值内存表,主键由sym和id字段组成
sym=`A`B`C`D`E
id=5 4 3 2 1
val=52 64 25 48 71
t=keyedTable(`sym`id,sym,id,val)
注意:指定容量和初始大小建立键值内存表时,初始大小必须为0。

咱们也能够经过keyedTable函数将常规内存表转换为键值内存表。例如:

sym=`A`B`C`D`E
id=5 4 3 2 1
val=52 64 25 48 71
tmp=table(sym, id, val)
t=keyedTable(`sym`id, tmp)
  • 数据插入和更新的特色

往键值内存表中添加新纪录时,系统会自动检查新记录的主键值。若是新记录中的主键值不存在于表中,那么往表中添加新的记录;若是新记录的主键值与已有记录的主键值重复时,会更新表中该主键值对应的记录。请看下面的例子。

首先,往空的键值内存表中插入新记录,新记录中的主键值为AAPL, IBM和GOOG。

t=keyedTable(`sym,1:0,`sym`datetime`price`qty,[SYMBOL,DATETIME,DOUBLE,DOUBLE]);
insert into t values(`APPL`IBM`GOOG,2018.06.08T12:30:00 2018.06.08T12:30:00 2018.06.08T12:30:00,50.3 45.6 58.0,5200 4800 7800);
t;

sym  datetime            price qty 
---- ------------------- ----- ----
APPL 2018.06.08T12:30:00 50.3  5200
IBM  2018.06.08T12:30:00 45.6  4800
GOOG 2018.06.08T12:30:00 58    7800

再次往表中插入一批主键值为AAPL, IBM和GOOG的新记录。

insert into t values(`APPL`IBM`GOOG,2018.06.08T12:30:01 2018.06.08T12:30:01 2018.06.08T12:30:01,65.8 45.2 78.6,5800 8700 4600);
t;

sym  datetime            price qty 
---- ------------------- ----- ----
APPL 2018.06.08T12:30:01 65.8  5800
IBM  2018.06.08T12:30:01 45.2  8700
GOOG 2018.06.08T12:30:01 78.6  4600

能够看到,表中记录条数没有增长,可是主键对应的记录已经更新。

继续往表中插入一批新记录,新记录自己包含了重复的主键值MSFT。

能够看到,表中有且仅有一条主键值为MSFT的记录。

  • 应用场景

(1)键值表对单行的更新和查询有很是高的效率,是数据缓存的理想选择。与redis相比,DolphinDB中的键值内存表兼容SQL的全部操做,能够完成根据键值更新和查询之外的更为复杂的计算。

(2)做为时间序列聚合引擎的输出表,实时更新输出表的结果。具体请参考教程使用DolphinDB计算K线

 


1.3 流数据表

流数据表顾名思义是为流数据设计的内存表,是流数据发布和订阅的媒介。流数据表具备自然的流表对偶性(Stream Table Duality),发布一条消息等价于往流数据表中插入一条记录,订阅消息等价于将流数据表中新到达的数据推向客户端应用。对流数据的查询和计算均可以经过SQL语句来完成。

  • 建立

使用streamTable函数可建立流数据表。streamTable的用法和table函数彻底相同。

//建立空的流数据表
t=streamTable(1:0,`sym`id`val,[SYMBOL,INT,INT])

//使用向量建立流数据表
sym=`A`B`C`D`E
id=5 4 3 2 1
val=52 64 25 48 71
t=streamTable(sym,id,val)

咱们也可使用streamTable函数将常规内存表转换为流数据表。例如:

sym=`A`B`C`D`E
id=5 4 3 2 1
val=52 64 25 48 71
tmp=table(sym, id, val)
t=streamTable(tmp)

流数据表也支持建立单个键值列,能够经过函数keyedStreamTable来建立。但与keyed table的设计目的不一样,keyedstreamtable的目的是为了在高可用场景(多个发布端同时写入)下,避免重复消息。一般key就是消息的ID。

  • 数据操做特色

因为流数据具备一旦生成就不会发生变化的特色,所以流数据表不支持更新和删除记录,只支持查询和添加记录。流数据一般具备连续性,而内存是有限的。为解决这个矛盾,流数据表引入了持久化机制,在内存中保留最新的一部分数据,更旧的数据持久化在磁盘上。当用户订阅旧的数据时,直接从磁盘上读取。启用持久化,使用函数enableTableShareAndPersistence,具体参考流数据教程

  • 应用场景

共享的流数据表在流计算中发布数据。订阅端经过subscribeTable函数来订阅和消费流数据。

 

1.4 MVCC内存表

MVCC内存表存储了多个版本的数据,当多个用户同时对MVCC内存表进行读写操做时,互不阻塞。MVCC内存表的数据隔离采用了快照隔离模型,用户读取到的是在他读以前就已经存在的数据,即便这些数据在读取的过程当中被修改或删除了,也对以前正在读的用户没有影响。这种多版本的方式可以支持用户对内存表的并发访问。须要说明的是,当前的MVCC内存表实现比较简单,更新和删除数据时锁定整个表,并使用copy-on-write技术复制一份数据,所以对数据删除和更新操做的效率不高。在后续的版本中,咱们将实现行级的MVCC内存表。

  • 建立

使用mvccTable函数建立MVCC内存表。例如:

//建立空的流数据表
t=mvccTable(1:0,`sym`id`val,[SYMBOL,INT,INT])

//使用向量建立流数据表
sym=`A`B`C`D`E
id=5 4 3 2 1
val=52 64 25 48 71
t=mvccTable(sym,id,val)

咱们能够将MVCC内存表的数据持久化到磁盘,只需建立时指定持久化的目录和表名便可。例如,

t=mvccTable(1:0,`sym`id`val,[SYMBOL,INT,INT],"/home/user1/DolphinDB/mvcc","test")

系统重启后,咱们能够经过loadMvccTable函数将磁盘中的数据加载到内存中。

loadMvccTable("/home/user1/DolphinDB/mvcc","test")

咱们也可使用mvccTable函数将常规内存表转换为MVCC内存表。

sym=`A`B`C`D`E
id=5 4 3 2 1
val=52 64 25 48 71
tmp=table(sym, id, val)
t=mvccTable(tmp)
  • 应用场景

当前的MVCC内存表适用于读多写少,并有持久化须要的场景。譬如动态的配置系统,须要持久化配置项,配置项的改动不频繁,已新增和查询操做为主,很是适合MVCC表。

 

2. 共享内存表

DolphinDB中的内存表默认只在建立内存表的会话中使用,不支持多用户多会话的并发操做,固然对别的会话也不可见。若是但愿建立的内存表能被别的用户使用,保证多用户并发操做的安全,必须共享内存表。4种类型的内存表都可共享。在DolphinDB中,咱们使用share命令将内存表共享。

t=table(1..10 as id,rand(100,10) as val)
share t as st
//或者share(t,`st)

上面的代码将表t共享为表st。

使用undef函数能够删除共享表。

undef(`st,SHARED)

2.1 保证对全部会话可见

内存表仅在当前会话可见,在其余会话中不可见。共享以后,其余会话能够经过访问共享变量来访问内存表。例如,咱们在当前会话中把表t共享为表st。

t=table(1..10 as id,rand(100,10) as val)
share t as st

咱们能够在其余会话中访问变量st。例如,往共享表st插入一条数据。

insert into st values(11,200)
select * from st

id val
-- ---
1  1  
2  53 
3  13 
4  40 
5  61 
6  92 
7  36 
8  33 
9  46 
10 26 
11 200

切换到原来的会话,咱们能够发现,表t中也增长了一条记录。

select * from t

id val
-- ---
1  1  
2  53 
3  13 
4  40 
5  61 
6  92 
7  36 
8  33 
9  46 
10 26 
11 200

 

2.2 保证线程安全

在多线程的状况下,内存表中的数据很容易被破坏。共享则提供了一种保护机制,可以保证数据安全,但同时也会影响系统的性能。

常规内存表、流数据表和MVCC内存表都支持多版本模型,容许多读一写。具体说,读写互不阻塞,写的时候能够读,读的时候能够写。读数据时不上锁,容许多个线程同时读取数据,读数据时采用快照隔离(snapshot isolation)。写数据时必须加锁,同时只容许一个线程修改内存表。写操做包括添加,删除或更新。添加记录一概在内存表的末尾追加,不管内存使用仍是CPU使用均很是高效。常规内存表和MVCC内存表支持更新和删除,且采用了copy-on-write技术,也就是先复制一份数据(构成一个新的版本),而后在新版本上进行删除和修改。因而可知删除和更新操做不管内存和CPU消耗都比较高。当删除和更新操做很频繁,读操做又比较耗时(不能快速释放旧的版本),容易致使OOM异常。

键值内存表写入时需维护内部索引,读取时也须要根据索引获取数据。所以键值内存表共享采用了不一样的方法,不管读写都必须加锁。写线程和读线程,多个写线程之间,多个读线程之间都是互斥的。对键值内存表尽可能避免耗时的查询或计算,不然会使其它线程长时间处于等待状态。

 

3. 分区内存表

当内存表数据量较大时,咱们能够对内存表进行分区。分区后一个大表有多个子表(tablet)构成,大表不使用全局锁,锁由每一个子表独立管理,这样能够大大增长读写并发能力。DolphinDB支持对内存表进行值分区、范围分区、哈希分区和列表分区,不支持组合分区。在DolphinDB中,咱们使用函数createPartitionedTable建立内存分区表。

  • 建立分区常规内存表
t=table(1:0,`id`val,[INT,INT]) 
db=database("",RANGE,0 101 201 301) 
pt=db.createPartitionedTable(t,`pt,`id)
  • 建立分区键值内存表
kt=keyedTable(1:0,`id`val,[INT,INT]) 
db=database("",RANGE,0 101 201 301) 
pkt=db.createPartitionedTable(t,`pkt,`id)
  • 建立分区流数据表

建立分区流数据表时,须要传入多个流数据表做为模板,每一个流数据表对应一个分区。写入数据时,直接往这些流表中写入;而查询数据时,须要查询分区表。

st1=streamTable(1:0,`id`val,[INT,INT]) 
st2=streamTable(1:0,`id`val,[INT,INT]) 
st3=streamTable(1:0,`id`val,[INT,INT]) 
db=database("",RANGE,1 101 201 301) pst=db.createPartitionedTable([st1,st2,st3],`pst,`id)  
st1.append!(table(1..100 as id,rand(100,100) as val)) 
st2.append!(table(101..200 as id,rand(100,100) as val)) 
st3.append!(table(201..300 as id,rand(100,100) as val))  
select * from pst
  • 建立分区MVCC内存表

与建立分区流数据表同样,建立分区MVCC内存表,须要传入多个MVCC内存表做为模板。每一个表对应一个分区。写入数据时,直接往这些表中写入;而查询数据时,须要查询分区表。

mt1=mvccTable(1:0,`id`val,[INT,INT])
mt2=mvccTable(1:0,`id`val,[INT,INT])
mt3=mvccTable(1:0,`id`val,[INT,INT])
db=database("",RANGE,1 101 201 301)
pmt=db.createPartitionedTable([mt1,mt2,mt3],`pst,`id)

mt1.append!(table(1..100 as id,rand(100,100) as val))
mt2.append!(table(101..200 as id,rand(100,100) as val))
mt3.append!(table(201..300 as id,rand(100,100) as val))

select * from pmt

因为分区内存表不使用全局锁,建立之后不能再动态增删子表。


3.1 增长查询的并发性

分区表增长查询的并发性有三层含义:(1)键值表在查询时也须要加锁,分区表由子表独立管理锁,至关于把锁的粒度变细了,所以能够增长读的并发性;(2)批量计算时分区表能够并行处理每一个子表;(3)若是SQL查询的过滤指定了分区字段,那么能够缩小分区范围,避免全表扫描。

以键值内存表为例,咱们对比在分区和不分区的状况下,并发查询的性能。首先,建立模拟数据集,一共包含500万行数据。

n=5000000
id=shuffle(1..n)
qty=rand(1000,n)
price=rand(1000.0,n)
kt=keyedTable(`id,id,qty,price)
share kt as skt

id_range=cutPoints(1..n,20)
db=database("",RANGE,id_range)
pkt=db.createPartitionedTable(kt,`pkt,`id).append!(kt)
share pkt as spkt

咱们在另一台服务器上模拟10个客户端同时查询键值内存表。每一个客户端查询10万次,每次查询一条数据,统计每一个客户端查询10万次的总耗时。

def queryKeyedTable(tableName,id){
	for(i in id){
		select * from objByName(tableName) where id=i
	}
}
conn=xdb("192.168.1.135",18102,"admin","123456")
n=5000000

jobid1=array(STRING,0)
for(i in 1..10){
	rid=rand(1..n,100000)
	s=conn(submitJob,"evalQueryUnPartitionTimer"+string(i),"",evalTimer,queryKeyedTable{`skt,rid})
	jobid1.append!(s)
}
time1=array(DOUBLE,0)
for(j in jobid1){
	time1.append!(conn(getJobReturn,j,true))
}

jobid2=array(STRING,0)
for(i in 1..10){
	rid=rand(1..n,100000)
	s=conn(submitJob,"evalQueryPartitionTimer"+string(i),"",evalTimer,queryKeyedTable{`spkt,rid})
	jobid2.append!(s)
}
time2=array(DOUBLE,0)
for(j in jobid2){
	time2.append!(conn(getJobReturn,j,true))
}

time1是10个客户端查询未分区键值内存表的耗时,time2是10个客户端查询分区键值内存表的耗时,单位是毫秒。

time1
[6719.266848,7160.349678,7271.465094,7346.452625,7371.821485,7363.87979,7357.024299,7332.747157,7298.920972,7255.876976]

time2
[2382.154581,2456.586709,2560.380315,2577.602019,2599.724927,2611.944367,2590.131679,2587.706832,2564.305815,2498.027042]

能够看到,每一个客户端查询分区键值内存表的耗时要低于查询未分区内存表的耗时。

查询未分区的内存表,能够保证快照隔离。但查询一个分区内存表,再也不保证快照隔离。如前面所说分区内存表的读写不使用全局锁,一个线程在查询时,可能另外一个线程正在写入并且涉及多个子表,从而可能读到一部分写入的数据。


3.2 增长写入的并发性

以分区的常规内存表为例,咱们能够同时往不一样的分区写入数据。

t=table(1:0,`id`val,[INT,INT])
db=database("",RANGE,1 101 201 301)
pt=db.createPartitionedTable(t,`pt,`id)

def writeData(mutable t,id,batchSize,n){
	for(i in 1..n){
		idv=take(id,batchSize)
		valv=rand(100,batchSize)
		tmp=table(idv,valv)
		t.append!(tmp)
	}
}

job1=submitJob("write1","",writeData,pt,1..100,1000,1000)
job2=submitJob("write2","",writeData,pt,101..200,1000,1000)
job3=submitJob("write3","",writeData,pt,201..300,1000,1000)

上面的代码中,同时有3个线程对pt的3个不一样的分区进行写入。须要注意的是,咱们要避免同时对相同分区进行写入。例如,下面的代码可能会致使系统崩溃。

job1=submitJob("write1","",writeData,pt,1..300,1000,1000)
job2=submitJob("write2","",writeData,pt,1..300,1000,1000)

上面的代码定义了两个写入线程,而且写入的分区相同,这样会破坏内存。为了保证每一个分区数据的安全性和一致性,咱们可将分区内存表共享。这样便可定义多个线程同时对相同分区分入。

share pt as spt
job1=submitJob("write1","",writeData,spt,1..300,1000,1000)
job2=submitJob("write2","",writeData,spt,1..300,1000,1000)


4. 数据操做比较


4.1 增删改查

下表总结了4种类型内存表在共享/分区的状况下支持的增删改查操做。

说明:

  • 常规内存表、键值内存表、MVCC内存表都支持增删改查操做,流数据表仅支持增长数据和查询,不支持删除和更新操做。
  • 对于键值内存表,若是查询的过滤条件中包含主键,查询的性能会获得明显提高。
  • 对于分区内存表,若是查询的过滤条件中包含分区列,系统可以缩小要扫描的分区范围,从而提高查询的性能。


4.2 并发性

在没有写入的状况下,全部内存表都容许多个线程同时查询。在有写入的状况下,4种内存表的并发性有所差别。下表总结了4种内存表在共享/分区的状况下支持的并发读写状况。

说明:

  • 共享表容许并发读写。
  • 对于没有共享的分区表,不容许多线程对相同分区同时写入的。


4.3 持久化

  • 常规内存表和键值内存表不支持数据持久化。一旦节点重启,内存中的数据将所有丢失。
  • 只有空的流数据表才支持数据持久化。要对流数据表进行持久化,首先要配置流数据持久化的目录persistenceDir,再使用enableTableShareAndPersistence使用将流数据表共享,并持久化到磁盘上。例如,将流数据表t共享并持久化到磁盘上。
t=streamTable(1:0,`id`val,[INT,INT])
enableTableShareAndPersistence(t,`st)

流数据表启用了持久化后,内存中仍然会保留流数据表中部分最新的记录。默认状况下,内存会保留最新的10万条记录。咱们也能够根据须要调整这个值。

流数据表持久化能够设定采用异步/同步、压缩/不压缩的方式。一般状况下,异步模式可以实现更高的吞吐量。

系统重启后,再次执行enableTableShareAndPersistence函数,会将磁盘中的全部数据加载到内存。

  • MVCC内存表支持持久化。在建立MVCC内存表时,咱们能够指定持久化的路径。例如,建立持久化的MVCC内存表。
t=mvccTable(1:0,`id`val,[INT,INT],"/home/user/DolphinDB/mvccTable")
t.append!(table(1..10 as id,rand(100,10) as val))

系统重启后,咱们可使用loadMvccTable函数将磁盘中的数据加载到内存中。例如:

t=loadMvccTable("/home/user/DolphinDB/mvccTable","t")

5. 表结构操做比较

内存表的结构操做包括新增列、删除列、修改列(内容和数据类型)以及调整列的顺序。下表总结了4种类型内存表在共享/分区的状况下支持的结构操做。

说明:

  • 分区表以及MVCC内存表不能经过addColumn函数新增列。
  • 分区表能够经过update语句来新增列,可是流数据表不容许修改,所以流数据表不能经过update语句来新增列。


6. 小结

DolphinDB支持4种类型内存表,还引入了共享和分区的概念,基本可以知足内存计算和流计算的各类需求。

相关文章
相关标签/搜索