Cozo 是事务型关系型数据库,使用 Datalog 作为查询语言的高性能·关系型·可嵌入式·图数据库。
- 一个 可嵌入 的数据库;
- 一个使用 Datalog 作为查询语句的数据库;
- 一个专注于 图数据、图算法 的数据库;
- 一个可进行 历史穿梭 查询的数据库;
- 一个支持 高性能、高并发 的数据库。
“可嵌入”是什么意思?
如果某个数据库能在不联网的手机上使用,那它大概就是嵌入式的。举例来说,SQLite 是嵌入式的,而 MySQL、Postgres、Oracle 等不是(它们是客户端—服务器(CS)架构的数据库)。
如果数据库与你的主程序在同一进程中运行,那么它就是 嵌入式 数据库。与此相对,在使用 客户端—服务器 架构的数据库时,主程序需要通过特定的接口(通常是网络接口)访问数据库,而数据库也可能运行在另一台机器或独立的集群上。嵌入式数据库使用简单,资源占用少,并可以在更广泛的环境中使用。
Cozo 同时也支持以客户端—服务器模式运行。因此,Cozo 是一个 可嵌入 而不是仅仅是 嵌入式 的数据库。在客户端—服务器模式下,Cozo 可以更充分地发挥服务器的性能。
“图数据”有什么用?
从本质上来说,数据一定是相互关联、自关联的,而这种关联的数学表达便是 图 (也叫 网络)。只有考虑这些关联,才能更深入地洞察数据背后的逻辑。
大多数现有的 图数据库 强制要求按照属性图(property graph)的范式存储数据。与此相对,Cozo 使用传统的关系数据模型。关系数据模型有存储逻辑简单、功能强劲等优点,并且处理图数据也毫无问题。更重要的是,数据的洞察常常需要挖掘隐含的关联,而关系数据模型作为关系 代数(relational algebra)可以很好地处理此类问题。比较而言,因为其不构成一个代数,属性图模型仅仅能够将显性的图关系作为图数据处理,可组合性很弱。
“Datalog”好在哪儿?
Datalog 1977 年便出现了,它可表达所有的 关系型查询,而它与 SQL 比起来的优势在于其对 递归 的表达。由于执行逻辑不同,Datalog 对于递归的运行,通常比相应的 SQL 查询更快。Datalog 的可组合性、模块性都很优秀,使用它,你可以逐层、清晰地表达所需的查询。
递归对于图查询尤其重要。Cozo 使用的 Datalog 方言 叫做 CozoScript,其允许在一定条件下混合使用聚合查询与递归,从而进一步增强了 Datalog 的表达能力。同时,Cozo内置了图分析中常用的一些算法(如 PageRank 等),调用简单。
对 Datalog 有进一步了解以后,你会发现 Datalog 的 规则 类似于编程语言中的函数。规则的一大特点是其可组合性:将一个查询分解为多个渐进的规则可使查询更清晰、易维护,且不会有效率上的损失。与此相对的,复杂的 SQL 查询语句通常表达为多层嵌套的“select-from-where”,可读性、可维护性都不高。
历史穿梭?
在数据库中,“历史穿梭”的意思是记录数据的一切变化,以允许针对某一时刻的数据进行执行查询,用来窥探历史。
在某种意义上,这使数据库成为 不可变 数据库,因为没有数据会被真正删除。
每一项额外的功能都有其代价。如果不使用某个功能,理想的状态是不必为这个功能的代价埋单。在 Cozo 中,不是所有数据表都自动支持历史穿梭,这就把是否需要此功能、是否愿意支付代价的选择权交到了用户手里。
这个关于历史穿梭的小故事可能启发出一些历史穿梭的应用场景。
“高性能、高并发”,有多高?
我们在一台 2020 年的 Mac Mini 上,使用 RocksDB 持久性存储引擎(Cozo 支持多种存储引擎)做了性能测试:
- 对一个有 160 万行的表进行查询:读、写、改的混合事务性查询可达到每秒 10 万次,而只读查询可达到每秒 25 万次。在此过程中,数据库使用的内存峰值仅为50MB。
- 备份数据的速度为每秒约 100 万行,恢复速度为每秒约 40 万行。备份、恢复的速度不随表单数据增长而变慢。
- 分析查询:扫描一个有 160 万行的表大约需要 1 秒(根据具体查询语句大约有上下 2 倍以内的差异)。查询所需时间与查询所涉及的行数大致成比例,而内存使用主要决定于返回集合的大小。
- 对于一个有 160 万个顶点,3100 万条边的图数据表,“两跳”图查询(如查询某人的朋友的朋友都有谁)可在 1 毫秒内完成。
- Pagerank 算法速度:1 万个顶点,12 万条边:50 毫秒以内;10 个万顶点,170 万条边:1 秒以内;160 万个顶点,3100 万条边:30秒以内。
更多的细节参见此文。
学习 Cozo
你得先安装一个数据库才能开始学,对吧?不一定:Cozo 是“嵌入式”的,所以我们直接把它通过 WASM 嵌入到浏览器里了!打开这个页面,然后:
当然也可以一步到位:先翻到后面了解如何在熟悉的环境里安装原生 Cozo 数据库,再开始学习。
一些示例
通过以下示例,可在正式开始学习之前对 Cozo 的查询先有个感性认识。
假设有个表,名为 *route
,含有两列,名为 fr
和 to
,其中数据为机场代码(如 FRA
是法兰克福机场的代码),且每行数据表示一个飞行航线。
从 FRA
可以不转机到达多少个机场:
?[count_unique(to)] := *route{fr: 'FRA', to}
count_unique(to) |
---|
310 |
从 FRA
出发,转机一次,可以到达多少个机场:
?[count_unique(to)] := *route{fr: 'FRA', to: 'stop},
*route{fr: stop, to}
count_unique(to) |
---|
2222 |
从 FRA
出发,转机任意次,可以到达多少个机场:
reachable[to] := *route{fr: 'FRA', to}
reachable[to] := reachable[stop], *route{fr: stop, to}
?[count_unique(to)] := reachable[to]
count_unique(to) |
---|
3462 |
从 FRA
出发,按所需的最少转机次数排序,到达哪两个机场需要最多的转机次数:
shortest_paths[to, shortest(path)] := *route{fr: 'FRA', to},
path = ['FRA', to]
shortest_paths[to, shortest(path)] := shortest_paths[stop, prev_path],
*route{fr: stop, to},
path = append(prev_path, to)
?[to, path, p_len] := shortest_paths[to, path], p_len = length(path)
:order -p_len
:limit 2
to | path | p_len |
---|---|---|
YPO | ["FRA","YYZ","YTS","YMO","YFA","ZKE","YAT","YPO"] |
8 |
BVI | ["FRA","AUH","BNE","ISA","BQL","BEU","BVI"] |
7 |
FRA
和 YPO
这两个机场之间最短的路径以及其实际飞行里程是多少:
start[] <- [['FRA']]
end[] <- [['YPO]]
?[src, dst, distance, path] <~ ShortestPathDijkstra(*route[], start[], end[])
src | dst | distance | path |
---|---|---|---|
FRA | YPO | 4544.0 | ["FRA","YUL","YVO","YKQ","YMO","YFA","ZKE","YAT","YPO"] |
当查询语句有错时,Cozo 会提供明确有用的错误信息:
?[x, Y] := x = 1, y = x + 1
eval::unbound_symb_in_head × Symbol 'Y' in rule head is unbound ╭──── 1 │ ?[x, Y] := x = 1, y = x + 1 · ─ ╰──── help: Note that symbols occurring only in negated positions are not considered bound
更多信息请参见软件网址及文档。
评论