16 KiB
Cozo 数据库
简介
[ 中文文档 | English ]
Cozo是一个通用事务性关系型数据库:
- 是一个可嵌入数据库;
- 使用Datalog作为查询语句;
- 专注于图数据、图算法;
- 支持高性能、高并发。
“可嵌入”是什么意思?
如果你能在不联网的手机上使用某个数据库,那它大概率就是嵌入式的。 SQLite是嵌入式数据库。MySQL、Postgres、Oracle是客户端—服务器(CS)架构的数据库。
如果一个数据库与你的主程序在同一进程中运行,那么它就是 嵌入式 数据库。与此相对,在使用 客户端—服务器 架构的数据库时,主程序通过数据库客户库连接到数据库服务器(可能运行在一个独立的机器上)。嵌入式数据库通常不需要额外设置,可以在更广泛的环境中使用。
因为Cozo同时也支持客户端—服务器模式运行,所以我们说它是 可嵌入 数据库而不是仅仅是 嵌入式 数据库。在客户端—服务器模式下,服务器资源可以得到更好的运用,并支持比嵌入式模式更多的并发性能。
“图数据”有什么用?
数据在本质上是相互关联、自关联的,这种关联的数学表达便是 图 。只有将这些关联性考虑进去,才能更深入地洞察数据背后的逻辑。
大多数现有的 图数据库 强制要求按照属性图(property graph)的范式存储数据。与此相对,Cozo的存储范式是传统的关系数据模型。关系数据模型的实现具有存储简单、功能强劲等优点,并且处理图数据也毫无问题。对于数据的数据洞察常常需要挖掘隐含在数据中内关联,而关系数据模型作为关系 代数(relational algebra)可以很好地处理此类问题。比较而言,属性图模型处理此类问题较为吃力,因为其不构成一个代数,可组合性弱。
“Datalog”好在哪儿?
Datalog可表达所有的 关系型查询。递归 的表达是 Datalog 的强项,且通常比相应的SQL查询中运行得更快。Datalog的组合性、模块性都很优秀,你可以一层一层地清晰地表达你的查询。
递归对于图查询尤其重要。Cozo的Datalog方言CozoScript允许在含有(安全的)聚合查询规则中使用递归,进一步增强了Datalog的递归查询能力。同时,Cozo内置了图分析中常用的一些递归算法(如PageRank等)的高性能实现,可以简便的直接调用。
当你对Datalog有进一步了解以后,你就会发现Datalog的 规则 就像编程语言中的函数。规则的特点就是其可组合性:将一个查询分解成多个渐进的规则可使它更加清晰、更易维护,且也不会有效率上的损失。与此相对,复杂的SQL查询语句通常表现为多层嵌套的“select-from-where”的形式,可读性不高。
“高性能、高并发”,有多高?
我们在一台2020年的Mac Mini上,使用RocksDB持久性存储引擎(Cozo支持许多存储引擎)做了一些性能测试:
- 对一个有160万行的表进行OLTP查询:混合读、写、改的事务性查询可达到每秒10万次,而对于只读查询,可达到每秒25万次。在此过程中,数据库使用的内存峰值约为50MB。
- 备份速度约为每秒100万行,恢复速度约为每秒40万行。备份、恢复的速度不管表本身有多大都差不多。
- OLAP查询:扫描一个有160万行的表大约需要1秒(取决于具体操作略有不同,上下2倍以内)。查询所需的时间大致与查询所涉及的行数成比例,内存的使用主要由返回集的大小决定。
- 对于一个有3100万条边的图数据表,“两跳”图查询(如查询某人的朋友的朋友都有谁)可在1毫秒内完成。
- Pagerank算法速度。1万个顶点和12万条边:50毫秒内完成;10个万顶点和170万条边:大约在1秒内完成;160万个顶点和32万条边:大约在30秒内完成。
更多的细节请看此文章。
学习
一般来说,你得先安装数据库才能学习怎么使用它。但Cozo是“嵌入式”的,所以它可以直接在浏览器里通过WASM运行,省去了安装的麻烦,而大多数操作的速度也和原生的差不多。打开WASM里面跑的Cozo页面,然后就可以开始学了:
- Cozo辅导课——学习基础知识
- CozoScript手册——深入学习细节
当然你也可以先翻到后面了解如何在你熟悉的环境里安装原生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
安装
建议先试用Cozo,再安装。当然反过来也可以。
如何安装Cozo取决于所使用的语言与环境,如下表:
语言/环境 | 官方支持的平台 | 存储引擎 |
---|---|---|
Python | Linux (x86_64), Mac (ARM64, x86_64), Windows (x86_64) | MQR |
NodeJS | Linux (x86_64, ARM64), Mac (ARM64, x86_64), Windows (x86_64) | MQR |
浏览器 | 支持WASM的浏览器(较新的浏览器全都支持) | M |
Java (JVM) | Linux (x86_64, ARM64), Mac (ARM64, x86_64), Windows (x86_64) | MQR |
Clojure (JVM) | Linux (x86_64, ARM64), Mac (ARM64, x86_64), Windows (x86_64) | MQR |
安卓 | Android (ARM64, ARMv7, x86_64, x86) | MQ |
iOS/macOS (Swift) | iOS (ARM64, 模拟器), Mac (ARM64, x86_64) | MQ |
Rust | 任何支持std 的平台(源代码编译) |
MQRST |
Go | Linux (x86_64, ARM64), Mac (ARM64, x86_64), Windows (x86_64) | MQR |
C/C++/支持C FFI的语言 | Linux (x86_64, ARM64), Mac (ARM64, x86_64), Windows (x86_64) | MQR |
独立的HTTP服务 | Linux (x86_64, ARM64), Mac (ARM64, x86_64), Windows (x86_64) | MQRST |
“存储引擎”列中各个字母的含义:
在Cozo的Rust文档里有一些额外的选择存储的建议。
即使你的语言、平台、存储引擎不被官方支持,你也可以尝试自己编译(也许需要在代码中做一些调整)。
为Cozo优化RocksDB存储引擎
RocksDB本身就有非常多的选项,调整这些选项可以在特定的工作负载下达到更好的性能。当然Cozo“开箱”的设置就已经相当快了,所以对95%的用户来说,优化引擎本身是不必要的。
如果你是剩下的那5%:当你用RocksDB引擎创建CozoDB实例时,你需要提供一个存储数据的目录的路径(如果不存在将被创建)。你可以在这个目录里创建一个名为options
的文件,这时RocksDB引擎会将其解读为RocksDB选项文件
并应用其中的设置。如果你使用的是独立的cozoserver
程序,此功能被激活时会有一条日志信息提示。
设置文件的内容相当繁杂,乱设置可能会造成数据库的各种问题。每次运行RocksDB引擎的数据库时,目录下的data/OPTIONS-XXXXXX
文件会记录当前的设置,你可以将这些文件作为优化设置的基础。如果你不是RocksDB方面的专家,建议只改动那些你至少大概知道什么意思的数字型选项。
架构
Cozo数据库由三个垒起来的组成部分组成,其中每部分只调用下面那部分的接口。
(用户代码) |
语言/环境包装 |
查询引擎 |
存储引擎 |
(操作系统) |
存储引擎
存储引擎定义了一个存储接口(Rust中的trait
),需要能够支持二进制数据的键值存储及范围扫描。目前官方的具体实现如下:
编译好的版本并不包含所有的引擎。这里面SQLite引擎有特殊地位:它也同时也是Cozo的备份文件格式,可以用来在不同引擎的Cozo数据库之间交换数据。Rust使用者也可以自己实现别的引擎。
所有的存储引擎都使用相同的 面向行的 二进制数据存储格式。实现具体的存储引擎并不需要了解这种格式。这种格式在存储键是使用的是一种叫做memcomparable的格式,其好处为能够将数据行存储为一个字节数组,直接对这些字节数组按照字节的顺序顺序排序就会得到正确的语义排序。当然,这也意味着SQLite引擎中存储的数据直接用SQL查询得到的结果看起来是乱码。
查询引擎
查询引擎部分实现了以下功能:
- 函数、聚合算子、算法的定义
- 数据结构定义(schema)
- 数据库事务(transaction)
- 查询语句的编译
- 查询的执行
Cozo中大部分代码都是在实现这些功能。CozoScript手册中有一章简要介绍了查询执行的一些细节。
用户通过Rust API来驱动查询引擎。
语言、环境封装
除Rust之外的所有语言、环境,都只是Rust API的进一步封装,使其在相应的环境中更容易使用。例如,在独立服务器(cozoserver)中,Rust的API被封装为HTTP端点,而在NodeJS中,同步的Rust API被封装为基于JavaScript运行时的异步调用。
你也可以尝试自己封装Rust API,使其可以用于其他语言。如果没有现成的目标语言与Rust之前的交互库,你可以考虑包装Cozo提供的基于C语言的API。在官方支持的语言中,只有Go直接封装了C语言的API。
项目进度
Cozo是一个非常年轻的项目。欢迎任何反馈。
1.0之前的版本不承诺语法、API的稳定性或存储兼容性。
许可证和贡献
本项目以MPL-2.0或更高版本授权。如果你有兴趣为该项目做贡献,请看这里。