此包包含高性能,柱状内存存储引擎,支持快速查询,更新和迭代,具有零分配和位图索引。
一般思想是利用在阵列(SOA)结构中组织数据的缓存友好方式,否则以其他方式知道"柱状"存储在数据库设计中。这又允许我们非常有效地迭代和过滤列。最重要的是,此包还将位图索引添加到柱状存储,允许使用二进制文件和,而不是或XOR构建过滤器查询(请参阅与SIMD支持的Kelindar / Bitmap)。
为了将数据放入商店,您' ll需要首先通过调用newcollection()方法创建一个集合。每个集合都需要模式,可以通过调用CreateColumn()多次或通过调用CreateColumnSof()函数从对象自动推断出来手动指定。
在下面的示例中,我们'通过使用JSON.UNMARSHAL()和基于加载切片上的第一个元素的自动创建COLUM来加载一些JSON数据。完成此后,我们可以通过将对象逐一插入集合来加载我们的数据。这是通过重复调用集合本身的INSERT()方法来实现的。
数据:= loadfromjson(" players.json")//创建一个新的柱状集合播放器:=列。 newcollection()玩家。 CreateColumnSof(数据[0])//从我们加载的数据中插入每个项目for _,v:=范围数据{播放器。插入(v)}
现在,让' s说我们只想要添加特定列。我们可以通过手动调用CreateColumn()方法来完成此操作以创建所需列。
//使用预定义的列播放器创建一个新的colument集合:=列。 newcollection()玩家。 CreateColumn("姓名",列。forstring())玩家。 CreateColumn("类",列。forstring())玩家。 CreateColumn("余额",列。forfloat64())玩家。 CreateColumn("年龄",列。forint16())//从我们加载的数据中插入来自我们加载的数据的_,v:=范围loadfromjson("播放器。just#34;){玩家。插入(v)}
虽然前面的示例演示了如何插入许多对象,但它是一个接一个地执行它并且相当效率。这是由于每个插入()直接在集合上呼叫,发起一个单独的越野,并且在那里且'' s的小型性能成本。如果要执行批量插入并插入许多值,可以通过在事务上调用Insert()来完成,如下面的示例中所示。请注意,唯一的区别是通过调用Query()方法并在事务上调用Txn.insert()方法而代替集合上的差异。
玩家。查询(func(txn * txn)错误{for _,v:= loadfromjson(" players.json"){txn。插入(v)} return nil // commit})
该商店允许您根据某些属性的存在或其值查询数据。在下面的示例中,我们正在查询我们的集合并在事务上使用withValue()方法应用过滤操作。此方法扫描值并检查某个谓词是否评估为true。在这种情况下,我们'如果他们的班级等于" rogue"我们跳过他们的课程,扫描他们的课程,抬头看他们的课程。在最后,我们'重新调用count()方法,只计算结果集。
//此查询执行"类"列球员。查询(Func(TXN *列)错误{COUNT:= TXN。有value("类" func(v接口{})bool {return v ==" rogue"} )。count()返回nil})
现在,如果我们' ll需要经常做这个查询吗?可以简单地创建具有相同谓词的索引,并且每次(a)将对象插入到集合中并且(b)更新依赖列的值。看看下面的例子,我们'重新创建一个依赖于&#34的流氓指数的拳头;柱子。此索引适用于相同的谓词,如果类是"流氓" rogue"然后,我们可以通过简单地调用()方法并提供索引名称来查询这一点。
索引基本上类似于布尔列,因此您可以在技术上也可以选择它' QUENCEITY。现在,在这个例子中,查询将在10-100倍的速度左右才能更快地执行,因为它使用Bitmap索引为" Rogue"索引并在查询时对两个位图执行简单的逻辑和操作。这避免了在查询期间的整个扫描和应用谓词。
//创建索引" rogue"提前。 createIndex(" rogue""类" func(v接口{})bool {return v ==" rogue"})//返回相同的结果以前的查询,但玩家更快。查询(Func(TXN *列)错误{count:= txn。与("流氓")。count()返回nil})
查询可以进一步扩展,因为它允许索引交叉口,差异和联合操作。这允许您提出更复杂的集合问题。在下面的示例中,假设我们在类列中拥有一堆索引,我们希望提出不同的问题。
首先,让Let' s尝试通过应用同一命名的方法来合并两个查询。在这里,我们首先选择只选择盗贼,然后将它们与法师合并,导致包含盗贼和法师的选择。
接下来,让'统计每个人都没有一个流氓,因为我们可以使用一个没有()方法,该方法在集合上执行差异(即二进制而不是操作)。除了盗贼之外,这将导致集合中所有玩家的计数。
现在,您可以组合所有方法并保持更复杂的查询。当查询索引和非索引字段时,重要的是要知道,因为每次扫描都将仅适用于选择,加快查询。因此,如果您在特定索引上有过滤器,可选择50%的播放器,然后您对此进行扫描(例如,value()),它只扫描50%的用户,因此将更快地扫描50%。
//超过30岁的盗贼?玩家。查询(Func(txn * txn)错误{txn。与("流氓")。用float("年龄" func(v float64)bool {return v> = 30}) 。count()返回nil})
在以前的所有示例中,我们只做了计算结果集中元素数的计数()操作。在本节中,我们' ll看看我们如何迭代结果集。简而言之,有2个主要方法,让我们这样做:
range()方法以列名为参数,允许更快的get / set为该列的值。
SELECT()方法并未预选择任何特定列,因此它通常有点慢,它也不允许任何更新。
首先检查'首先检查范围()方法。在下面的示例中,我们通过使用Range()方法和提供"姓名"选择所有流氓并通过使用范围()方法来打印出他们的名称。列到它。包含光标的回调允许我们通过调用字符串()方法来快速获取列的值来检索字符串值。它还包含int(),uint(),float()或更多通用值()的方法,以提取不同类型的数据。
玩家。查询(Func(txn * txn)错误{txn。与("流氓")。范围("名称" func(v列。游标)bool {println(" Rogue name",v。字符串())//打印名称返回true})return nil}
现在,如果你需要两列怎么办?该范围仅允许您快速选择单个列,但您仍然可以在迭代期间通过其名称检索其他列。这可以通过相应的StringAt(),floatat(),Intat(),UIntat()或ValueAt()方法来完成,如下所示。
玩家。查询(Func(txn * txn)错误{txn。与("流氓")。范围("名称" func(v列。游标)bool {println(" Rogue名称",v。字符串())//打印名称println("流氓时代" v。Intat("年龄"))//打印年龄返回true})返回nil})
另一方面,SELECT()允许您执行提供选择光标的只读选择。此光标不允许任何更新,删除或插入,也不允许预选择任何特定列。在下面的示例中,我们使用选择器打印出所有流氓的所有盗贼的名称。
玩家。查询(Func(txn * txn)错误{txn。与("流氓")。选择(func(v列选择器)bool {println(" rogue name" v。 Stringat("名称"))//打印名称返回true})返回nil})
现在,如果您需要快速删除集合中的所有一些数据,该怎么办?在这种情况下,deleteAll()或deleteIf()方法派上用方便了。这些方法非常快(尤其是deleteAll()),并允许您快速删除适当的结果,同意。在下面的示例中,我们只需在事务中选择它们并调用DeleteAll()方法,从集合中删除所有盗贼。
为了更新集合中的某些项目,可以简单地调用范围()方法和相应的光标' s更新()或updateateate()方法,允许原子地更新某个列的值。如果商店支持交易,并且只有在已提交事务时,才会将更新直接反映,但才能将更新应用于集合。这允许隔离和回滚。
在下面的例子中,我们'重新选择所有流氓并更新它们的平衡和年龄,以某些值。事务返回nil,因此它将在query()方法返回时自动提交。
玩家。查询(Func(txn * txn)错误{txn。与("流氓")。范围("余额" func(v列。游标)bool {v。更新(10.0) //更新"余额"到10.0 v。Updateat("年龄" 50)//更新"年龄"到50 return true} //选择余额返回nil})
在某些情况下,您可能想要递归或减少数值。为了完成此操作,您可以使用光标或选择器的提供的添加()或addat()操作。注意,还将相应地更新索引,并且谓词以最新的值重新评估。在下面的例子中,我们'重新将所有盗贼的平衡归因于原子而像。
玩家。查询(Func(TXN * TXN)错误{TXN。与(" rogue")。范围("余额" func(v列。游标)bool {v。添加(500.0) //递增"余额" by 500返回true})返回nil})
有时,在不再需要它们时自动删除某些行是有用的。为此,库会自动将expire列添加到每个新集合中,并同时启动清除Goroutine,周期性地运行并清除已过期的对象。要设置此项,您可以简单地在集合上使用INSERTWITHTL()方法,允许使用定义的时间持续时间插入对象。
在下面的示例中,我们将对象插入集合并从当前时间将时间设置为5秒。在此之后,将自动从集合中自动驱逐对象,并且可以回收其空间。
玩家。 InsertWithttl(映射[String]接口{} {"姓名&#34 ;:" merlin&#34 ;,"类&#34 ;:"法师和#34 ;,"年龄&#34 ;:55,"余额":500,},5 *时间。第二)//时间到5秒
在一个有趣的节点上,由于自动添加到每个集合的到期列是一个实际的普通列,您可以查询甚至更新它。在下面的示例中,我们查询并有条理地更新到期列。示例加载时间,添加一小时并更新它,但在练习中,如果要执行此操作,则应使用可以原子执行的Add()方法。
玩家。查询(Func(TXN *列)错误{RETURN TXN。范围(" expire" func(v列)bool {oldexpire:= time。unix(0,v。int()) //将到期到期.Time.Time NewExt:= ExpiReat。添加(1 *时间。小时)。Unixnano()//添加一些时间v。更新(newext)return true})})
事务允许在两个并发操作之间隔离。实际上,所有批处理查询都必须通过此库中的事务进行。查询方法需要一个函数,该函数占据了列的列指针,该指针包含支持查询的各种辅助方法。在下面的例子中我们'重新尝试迭代所有玩家并通过将其设置为10.0来更新它们的余额。如果函数返回,则查询方法会自动调用txn.commit(),而没有任何错误。在翻盖方面,如果提供的函数返回错误,则查询将自动调用txn.rollback(),因此将应用任何更改。
//范围在所有玩家和更新(成功为平衡)玩家。查询(FUNC(TXN *列)错误{TXN。范围("余额" func(v列。游标)bool {v。更新(10.0)//更新"余额&# 34;至10.0返回true})//否错误,txn.commit()将被称为return nil})
现在,在此示例中,我们尝试更新余额,但查询回调返回错误,在这种情况下,均未在底层集合中反映出更新。
//范围在所有玩家和更新(成功为平衡)玩家。查询(FUNC(TXN *列)错误{TXN。范围("余额" func(v列。游标)bool {v。更新(10.0)//更新"余额&# 34;至10.0返回true})//返回错误,txn.rollback()将被称为return fmt。errorf(" bug")})
您可以(但是probablety won' t需要)手动调用comment()或滚动(),根据需要多次。这可以方便地进行部分更新,但是调用它们通常会在您的应用程序上击中绩效。
//范围在所有玩家和更新(成功为平衡)玩家。查询(FUNC(TXN *列)错误{TXN。范围("余额" func(v列。游标)bool {v。更新(10.0)//更新"余额&# 34;至10.0返回true})txn。commit()//手动提交所有更改返回nil //这将再次调用txn.commit(),但将是一个no-op})
此库还支持将所有交易始终拨出的流式传输,因为它们发生。这允许您实现自己的更改数据捕获(CDC)侦听器,将数据流入Kafka或远程数据库以进行耐用。要启用它,您可以简单地在创建集合期间提供Commit.Writer接口的实现。
在下面的示例中,我们利用Commit.Whannel实现了Commit.Writer,其简单地将提交发布到Go频道中。在这里,我们创建了一个缓冲的通道并继续使用单独的Goroutine消耗提交,允许我们在商店中发生时查看事务。
//创建一个新的提交编写器(简单频道)和一个新的集合编写器:= make(提交。频道,1024)播放器:= newCollection(列。选项{writer:writer,})//读取来自频道的更改func(){for commit:= writer {println(" commit"提交。键入。string())}}()// ...插入,更新或删除
在单独的备注上,保证此更改流是一致和序列化的。这意味着您还可以复制其他数据库上的这些更改并同步两者。实际上,此库还在集合上提供允许执行此操作的重播()方法。在下面的示例中,我们使用重播()方法与更改流一起创建两个集合主和副本和asucronouse将所有提交从主到副本复制到副本。
//创建一个p rimary集合编写器:= make(提交。频道,1024)primary:=列。 newcollection(专栏。选项{writer:& writer,})主要。具有相同架构副本的CreateColumnSof(Object)//副本:=列。 newcollection()副本。 CreateColumnSof(对象)// in sync go func(){for change:= Range Writer {副本。重播(更改)}}()
func main(){//创建一个新的柱状集合播放器:=列。 newcollection()//人类玩家的索引。 createIndex("人和#34 ;,#34;赛跑" func(v interface {})bool {return v ==" mages玩家的索引。 CreateIndex("法师""类" func(v接口{})bool {return v =="旧玩家的索引。 createIndex("旧""年龄" func(v interface {})bool {return v。(float64)> = 30})//将项目加载到加载的集合中:= loadfixture(" players.json")玩家。 CreateColumnSof(加载[0])对于_,v:= loaded {播放器。插入(v)} //这在3个不同的列上执行全扫描,并将它们与指定的谓词进行比较。这不是索引,但是柱状扫描是//缓存友好的。玩家。查询(Func(TXN *列)错误{println(txn。withstring(" func(v string)bool {return v =="人和#34;})。有了("类",func(v string)bool {return v =="法师"})。用float("年龄" func(v float64)bool {返回v> = 30})。count())//打印计数返回nil})//这执行了cound,而不是通过整个数据集扫描,而不是通过预构建的索引扫描//使用逻辑和操作。结果//将与上面的查询相同,但查询的性能取决于底层数据的大小,更快地为10x-100x //。玩家。查询(Func(TXN *列)错误{Println(TXN。与("人和#34;"法师""旧")。count()) //打印计数返回nil})//相同的条件如上所述,但我们还选择//播放器的实际名称并迭代它们。玩家。查询(Func(TXN *列)错误{TXN。与("人和#34;"法师","旧")。范围("名称",func(v列。光标)bool {println(v.ring string())//打印名称返回true})//列选择return nil})}
下面的基准在包含十几个列的500项的集合上运行。随意探索基准,但我强烈建议在实际数据集上测试它。
CPU:英特尔(R)核心(TM)I7-9700K CPU @ 3.60GHzBenchmarkCollection / Insert-8 5013795 239.9 NS / OP 27 B / OP 0 Allocs / OpbenchmarkCollection / Fetch-8 23730796 50.63 NS / OP 0 B / OP 0 Allocs / OpbenchMarkCollection / Scan-8 234990 4743 NS / OP 0 B / OP 0 Allocs / OpbenchmarkColection / Count-8 7965873 152.7 NS / OP 0 B / OP 0 Allocs / OpbenchmarkCollection / Range-8 1512513 799.9 NS / OP 0 B / OP 0 Allocs / OpbenchMarkCollection / Update-AT-8 5409420 224.7 NS / OP 0 B / OP 0 Allocs / OpbenchmarkCollection / Update-All-8 196626 6099 NS / OP 0 B / OP 0 Allocs / OpbenchmarkCollection / Delete-8 2006052 594.9 NS / OP 0 B / OP 0 Allocs / OpbenchmarkCollection / Delete-All-8 1889685 643.2 NS / OP 0 B / OP 0 Allocs / Op
当测试更大的集合时,我添加了一个小示例(请参阅示例文件夹)并使用2000万行插入,每个条目都有12列,需要计算4个索引,以及一些查询和扫描周围的扫描。
运行插入刀片为20000000行.... - >插入率为52.8255618SRunning全扫描时代> = 30 ... - >结果= 10200000->全部SCA.
......