@ -1,6 +1,6 @@
< img src = "static/logo_c.png" width = "200" height = "175" alt = "Logo" >
[![docs ](https://img.shields.io/readthedocs/cozo/latest )](https://docs.cozodb.org/)
[![docs ](https://img.shields.io/readthedocs/cozo/latest )](https://docs.cozodb.org/zh_CN/latest/ )
[![cozo-node ](https://img.shields.io/npm/v/cozo-node )](https://www.npmjs.com/package/cozo-node)
[![npm (web) ](https://img.shields.io/npm/v/cozo-lib-wasm?label=browser )](https://www.npmjs.com/package/cozo-lib-wasm)
[![Crates.io ](https://img.shields.io/crates/v/cozo )](https://crates.io/crates/cozo)
@ -21,74 +21,74 @@
[ 中文文档 | [English ](./README.md ) ]
Cozo是一个通用事务性 关系型数据库:
Cozo是一个事务型 关系型数据库:
* 是 一个**可嵌入**数据库;
* 使用**Datalog**作为查询语句;
* 专注于**图数据、图算法**;
* 可进行**时光穿梭**查询 ;
* 支持**高性能、高并发**。
* 一个 ** 可嵌入** 的 数据库;
* 一个 使用 **Datalog** 作为查询语句的数据库 ;
* 一个 专注于 ** 图数据、图算法** 的数据库 ;
* 一个可进行 ** 历史穿梭** 查询的数据库 ;
* 一个 支持 ** 高性能、高并发** 的数据库 。
### “可嵌入”是什么意思?
如果你能在不联网的手机上使用某个数据库,那它大概率就是嵌入式的。 SQLite是嵌入式数据库。MySQL、Postgres、Oracle 是客户端—服务器( CS) 架构的数据库。
如果某个数据库能在不联网的手机上使用, 那它大概就是嵌入式的。举例来说, SQLite 是嵌入式的,而 MySQL、Postgres、Oracle 等不是(它们 是客户端—服务器( CS) 架构的数据库) 。
> 如果一个 数据库与你的主程序在同一进程中运行,那么它就是 _嵌入式_ 数据库。与此相对,在使用 _客户端—服务器_ 架构的数据库时,主程序通过数据库客户库连接到数据库服务器(可能运行在一个独立的机器上)。嵌入式数据库通常不需要额外设置, 可以在更广泛的环境中使用。
> 如果数据库与你的主程序在同一进程中运行,那么它就是 _嵌入式_ 数据库。与此相对,在使用 _客户端—服务器_ 架构的数据库时,主程序需要通过特定的接口(通常是网络接口)访问数据库,而数据库也可能运行在另一台机器或独立的集群上。嵌入式数据库使用简单,资源占用少,并 可以在更广泛的环境中使用。
>
> 因为Cozo同时也支持客户端—服务器模式运行, 所以我们说它是 _可嵌入_ 数据库而不是仅仅是 _嵌入式_ 数据库。在客户端—服务器模式下,服务器资源可以得到更好的运用,并支持比嵌入式模式更多的并发 性能。
> Cozo 同时也支持以客户端—服务器模式运行。因此, Cozo 是一个 _可嵌入_ 而不是仅仅是 _嵌入式_ 的数据库。在客户端—服务器模式下, Cozo 可以更充分地发挥服务器的 性能。
### “图数据”有什么用?
数据在本质上是相互关联、自关联的,这种关联的数学表达便是 _图_ 。只有将这些关联性考虑进去 ,才能更深入地洞察数据背后的逻辑。
从本质上来说,数据一定是相互关联、自关联的,而这种关联的数学表达便是 _图_ (也叫 _网络_ )。只有考虑这些关联 ,才能更深入地洞察数据背后的逻辑。
> 大多数现有的 _图数据库_ 强制要求按照属性图( property graph) 的范式存储数据。与此相对, Cozo的存储范式是传统的关系数据模型。关系数据模型的实现具有存储简单、功能强劲等优点,并且处理图数据也毫无问题。对于数据的数据洞察常常需要挖掘隐含在数据中内 关联,而关系数据模型作为关系 _代数_ ( relational algebra) 可以很好地处理此类问题。比较而言, 属性图模型处理此类问题较为吃力, 因为其不构成一个代数,可组合性弱。
> 大多数现有的 _图数据库_ 强制要求按照属性图( property graph) 的范式存储数据。与此相对, Cozo 使用传统的关系数据模型。关系数据模型有存储逻辑简单、功能强劲等优点,并且处理图数据也毫无问题。更重要的是,数据的洞察常常需要挖掘隐含的 关联,而关系数据模型作为关系 _代数_ ( relational algebra) 可以很好地处理此类问题。比较而言, 因为其不构成一个代数, 属性图模型仅仅能够将显性的图关系作为图数据处理, 可组合性很 弱。
### “Datalog”好在哪儿?
Datalog可表达所有的 _关系型查询_ 。_递归_ 的表达是 Datalog 的强项, 且通常比相应的SQL查询中运行得更快。Datalog的组合性、模块性都很优秀, 你可以一层一层地清晰地表达你 的查询。
Datalog 1977年便出现了, 它可表达所有的 _关系型查询_ ,而它与 SQL 比起来的优势在于其对 _递归_ 的表达。由于执行逻辑不同, Datalog 对于递归的运行,通常比相应的 SQL 查询更快。Datalog 的可组合性、模块性都很优秀,使用它,你可以逐层、清晰地表达所需 的查询。
> 递归对于图查询尤其重要。Cozo的Datalog方言CozoScript允许在含有( 安全的) 聚合查询规则中使用递归, 进一步增强了Datalog的递归查询 能力。同时, Cozo内置了图分析中常用的一些递归 算法( 如PageRank等) 的高性能实现 , 可以简便的直接 调用。
> 递归对于图查询尤其重要。Cozo 使用的 Datalog 方言 叫做 CozoScript, 其允许在一定条件下混合使用聚合查询与递归, 从而进一步增强了 Datalog 的表达 能力。同时, Cozo内置了图分析中常用的一些算法( 如 PageRank 等),调用简单 。
>
> 当你对Datalog有进一步了解以后, 你就会发现Datalog的 _规则_ 就像编程语言中的函数。规则的特点就是其可组合性: 将一个查询分解成多个渐进的规则可使它更加清晰、更易维护, 且也不会有效率上的损失。与此相对, 复杂的SQL查询语句通常表现为多层嵌套的“select-from-where”的形式, 可读性 不高。
> 对 Datalog 有进一步了解以后,你会发现 Datalog 的 _规则_ 类似于编程语言中的函数。规则的一大特点是其可组合性:将一个查询分解为多个渐进的规则可使查询更清晰、易维护,且不会有效率上的损失。与此相对的,复杂的 SQL 查询语句通常表达为多层嵌套的“select-from-where”, 可读性、可维护性都 不高。
### 时光 穿梭?
### 历史 穿梭?
在数据库中,“时光穿梭”意味着跟踪数据随时间的变化,并允许针对在特定时间的数据快照执行查询以获得数据的历史视图 。
在数据库中,“历史穿梭”的意思是记录数据的一切变化,以允许针对某一时刻的数据进行执行查询,用来窥探历史 。
> 在某种意义上,这使得你的数据库变得 _不可变_ ,因为没有数据会被真正删除。
> 在某种意义上,这使数据库成为 _不可变_ 数据库 ,因为没有数据会被真正删除。
>
> 在Cozo中, 不是所有数据都自动支持时间旅行: 某个表是否有这个能力需要你来决定。这是因为每一项额外的功能都有其代价, 而如果你不使用这个功能, 其代价你就不必承担 。
> 每一项额外的功能都有其代价。如果不使用某个功能,理想的状态是不必为这个功能的代价埋单。在 Cozo 中,不是所有数据表都自动支持历史穿梭,这就把是否需要此功能、是否愿意支付代价的选择权交到了用户手里 。
>
> 这里有一个关于时光穿梭的[小故事](https://docs.cozodb.org/en/latest/releases/v0.4.html),可以帮助你了解其一些 应用场景。
> [这个 ](https://docs.cozodb.org/zh_CN/latest/releases/v0.4.html )关于历史穿梭的小故事可能启发出一些历史穿梭的 应用场景。
### “高性能、高并发”,有多高?
我们在一台2020年的Mac Mini上, 使用RocksDB持久性存储引擎( Cozo支持许 多存储引擎)做了一些 性能测试:
我们在一台 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秒内完成 。
* 对一个有 160 万行的表进行查询:读、写、改的混合 事务性查询可达到每秒 10 万次,而只读查询可达到每秒 25 万次。在此过程中,数据库使用的内存峰值仅 为50MB。
* 备份数据的速度为每秒约 100 万行,恢复速度为每秒约 40 万行。备份、恢复的速度不随表单数据增长而变慢 。
* 分析查询:扫描一个有 160 万行的表大约需要 1 秒(根据具体查询语句大约有上下 2 倍以内的差异)。查询所需时间与查询所涉及的行数大致成比例,而内存使用主要决定于返回集合的大小 。
* 对于一个有 160 万个顶点, 3100 万条边的图数据表,“两跳”图查询(如查询某人的朋友的朋友都有谁)可在 1 毫秒内完成。
* Pagerank 算法速度: 1 万个顶点, 12 万条边: 50 毫秒以内; 10 个万顶点, 170 万条边: 1 秒以内; 160 万个顶点, 3100 万条边: 30秒以内 。
更多的细节请看[此文章](https://docs.cozodb.org/en /latest/releases/v0.3.html)。
更多的细节参见[此文](https://docs.cozodb.org/zh_CN /latest/releases/v0.3.html)。
## 学习
## 学习 Cozo
一般来说, 你得先安装数据库才能学习怎么使用它。但Cozo是“嵌入式”的, 所以它可以直接在浏览器里通过WASM运行, 省去了安装的麻烦, 而大多数操作的速度也和原生的差不多。打开[WASM里面跑的Cozo 页面](https://www.cozodb.org/wasm-demo/),然后就可以开始学了 :
你得先安装一个数据库才能开始学, 对吧? 不一定: Cozo 是“嵌入式”的,所以我们直接把它通过 WASM 嵌入到浏览器里了!打开[这个 页面](https://www.cozodb.org/wasm-demo/),然后:
* [Cozo 辅导课](https://docs.cozodb.org/en/latest/tutorial.html )——学习基础知识
* [Cozo 入门教程](https://docs.cozodb.org/zh_CN/latest/tutorial.html )
当然你也可以先翻到后面了解如何在你熟悉的环境里安装原生Cozo数据库, 再通过以上资料 学习。
当然也可以一步到位:先翻到后面了解如何在熟悉的环境里安装原生 Cozo 数据库,再开始 学习。
### 一些示例
以下给出一些示例, 可以在正式学习之前了解一下Cozo的查询长什么样 。
通过以下示例,可在正式开始学习之前对 Cozo 的查询先有个感性认识 。
假设我们有个表叫做`*route`,含有两列,名称叫做`fr`和`to`,存的都是机场的代码(比如`FRA`就是法兰克福机场的代码),而每行数据表示一个 航线。
假设有个表,名为`*route`,含有两列,名为`fr`和`to`,其中数据为机场代码(如`FRA`是法兰克福机场的代码),且每行数据表示一个飞行 航线。
从`FRA`可以直接飞到 多少个机场:
从`FRA`可以不转机到达 多少个机场:
```
?[count_unique(to)] := *route{fr: 'FRA', to}
```
@ -97,8 +97,7 @@ Datalog可表达所有的 _关系型查询_。_递归_ 的表达是 Datalog 的
|------------------|
| 310 |
从`FRA`出发,经停一次,可以飞到多少个机场:
从`FRA`出发,转机一次,可以到达多少个机场:
```
?[count_unique(to)] := *route{fr: 'FRA', to: 'stop},
*route{fr: stop, to}
@ -108,7 +107,7 @@ Datalog可表达所有的 _关系型查询_。_递归_ 的表达是 Datalog 的
|------------------|
| 2222 |
从`FRA`出发,经停任意次数 ,可以到达多少个机场:
从`FRA`出发,转机任意次 ,可以到达多少个机场:
```
reachable[to] := *route{fr: 'FRA', to}
reachable[to] := reachable[stop], *route{fr: stop, to}
@ -119,7 +118,7 @@ reachable[to] := reachable[stop], *route{fr: stop, to}
|------------------|
| 3462 |
从`FRA`出发,按所需的最少经停次数计算,给出最难到达的两个机场 :
从`FRA`出发,按所需的最少转机次数排序,到达哪两个机场需要最多的转机次数 :
```
shortest_paths[to, shortest(path)] := *route{fr: 'FRA', to},
path = ['FRA', to]
@ -137,7 +136,7 @@ shortest_paths[to, shortest(path)] := shortest_paths[stop, prev_path],
| YPO | `["FRA","YYZ","YTS","YMO","YFA","ZKE","YAT","YPO"]` | 8 |
| BVI | `["FRA","AUH","BNE","ISA","BQL","BEU","BVI"]` | 7 |
按实际路程计算,给出`FRA`和`YPO`这两个机场之间最短的路径 :
`FRA` 和`YPO`这两个机场之间最短的路径以及其实际飞行里程是多少:
```
start[] < - [ [ ' FRA ' ] ]
end[] < - [ [ ' YPO ] ]
@ -148,7 +147,7 @@ end[] <- [['YPO]]
|-----|-----|----------|--------------------------------------------------------|
| FRA | YPO | 4544.0 | `["FRA","YUL","YVO","YKQ","YMO","YFA","ZKE","YAT","YPO"]` |
如果查询语句有错误, Cozo会尝试提供明确、 有用的错误信息:
当查询语句有错时, Cozo 会提供明确 有用的错误信息:
```
?[x, Y] := x = 1, y = x + 1
```
@ -165,9 +164,9 @@ end[] <- [['YPO]]
## 安装
建议先[试用Cozo](#学习),再安装。当然反过来也可以 。
建议先[学习](#学习),再安装。当然反过来我们也不反对 。
如何安装Cozo取决于所使用的语言与环境, 如下表 :
Cozo 可以安装在一大堆不同的语言与环境中 :
| 语言/环境 | 官方支持的平台 | 存储引擎 |
|-------------------------------------------------------|------------------------------------------------------------------------------------------------------|-------|
@ -191,22 +190,22 @@ end[] <- [['YPO]]
* S: 基于 [Sled ](https://github.com/spacejam/sled ) 的存储引擎
* T: 基于 [TiKV ](https://tikv.org/ ) 的分布式存储引擎
在Cozo的[Rust文档](https://docs.rs/cozo/)里有一些额外的选择存储 的建议。
Cozo 的 [Rust API 文档 ](https://docs.rs/cozo/ )(英文)中有一些额外的关于存储选择 的建议。
即使你的语言、平台、存储引擎不被官方支持,你也可以尝试自己编译(也许需要在代码中做一些调整) 。
你也可以尝试为其它平台、语言、引擎自行编译 Cozo。可能需要调整一些代码, 但总体来说不难 。
### 为Cozo 优化RocksDB存储引擎
### 优化基于 RocksDB 的 存储引擎
RocksDB本身就有非常多的选项, 调整这些选项可以在特定的工作负载下达到更好的性能。当然Cozo“开箱”的设置就已经相当快了, 所以对95%的用户来说,优化引擎本身是不必要的 。
RocksDB 有五花八门的选项以供用户进行性能调优。但是调优这个问题太复杂了,就连 RocksDB 他们自己也搞不定,所以实际生产中他们用的是强化学习来自动调优。对于 95% 的用户来说,费这个劲根本不值得,尤其是 Cozo “开箱”的设置就已经相当快、足够快了 。
如果你是剩下的那5%: 当你用RocksDB引擎创建CozoDB实例时, 你需要提供一个存储数据的目录的路径( 如果不存在将被创建) 。你可以 在这个目录里创建一个名为`options`的文件,这时 RocksDB引擎会将其解读为[RocksDB选项文件](https://github.com/facebook/rocksdb/wiki/RocksDB-Options-File)
并应用其中的设置。如果你 使用的是独立的`cozoserver`程序,此功能被激活 时会有一条日志信息提示 。
如果你坚信你是那剩下的 5% 里面的:当你用 RocksDB 引擎创建 CozoDB 实例时,你提供了一个存储数据的目录的路径。如果 在这个目录里创建一个名为`options`的文件, RocksDB 引擎便 会将其解读为 [ RocksDB选项文件 ]( https://github.com/facebook/rocksdb/wiki/RocksDB-Options-File )
并应用其中的设置。如果使用的是独立的`cozoserver`程序,激活 此功能时会有一条提示 日志。
设置文件的内容相当繁杂, 乱设置可能会造成数据库的各种问题。每次运行RocksDB引擎的数据库时, 目录下的`data/OPTIONS-XXXXXX`文件会记录当前的设置, 你可以将这些文件作为优化设置的基础。如果你不是RocksDB方面的专家, 建议只改动那些你至少大概知道什么意思的数字型选项 。
每次 RocksDB 引擎启动时,存储目录下的`data/OPTIONS-XXXXXX`文件会记录当前应用设置。你可以把这个文件拷贝出来,在其基础上修改。如果你不是 RocksDB 的专家,建议只改动那些你大概知道什么意思的数字型选项。设置不当可能会搞乱、搞坏数据库 。
## 架构
Cozo数据库由三个垒起来的组成部分组成,其中每部分只调用下面那 部分的接口。
Cozo 数据库有三个上下游部分组成,其中每部分只调用下游 部分的接口。
< table >
< tbody >
@ -220,7 +219,7 @@ Cozo数据库由三个垒起来的组成部分组成, 其中每部分只调用
### 存储引擎
存储引擎定义了一个存储接口( Rust中的`trait`),需要能够支持二进制数据的键值存储及范围扫描。目前官方的具体实现如下 :
在存储引擎这一部分里, Cozo 定义了一个存储接口( Rust 中的 `trait` ),这个接口的功能是对二进制数据的键值进行存储及范围扫描。目前这个接口有以下官方实现 :
* 基于内存的非持久性存储引擎
* 基于 [SQLite ](https://www.sqlite.org/ ) 的存储引擎
@ -228,36 +227,36 @@ Cozo数据库由三个垒起来的组成部分组成, 其中每部分只调用
* 基于 [Sled ](https://github.com/spacejam/sled ) 的存储引擎
* 基于 [TiKV ](https://tikv.org/ ) 的分布式存储引擎
编译好的版本并不包含所有的引擎。这里面SQLite引擎有特殊地位: 它也同时也是Cozo的备份文件格式, 可以用来在不同引擎的Cozo数据库之间交换数据。Rust使用者也可以自己实现别的引擎 。
不是所有的二进制包都包含以上所有引擎。这些引擎中, SQLite 引擎具有特殊地位: Cozo 使用它的文件作为备份文件,用以在不同引擎的 Cozo 之间交换数据。Rust 使用者可以轻松实现自己的引擎(不是说写一个引擎很轻松,这里意思是把现有的引擎接入到 Cozo 里很轻松) 。
所有的存储引擎都使用相同的 _面向行的_ 二进制数据存储格式。实现具体的存储引擎并不需要了解这种格式。这种格式在存储键是使用的是一种叫做[memcomparable](https://github.com/facebook/mysql-5.6/wiki/MyRocks-record-format#memcomparable-format)的格式, 其好处为能够将数据行存储为一个字节数组, 直接对这些字节数组按照字节的顺序顺序排序就会得到正确的语义排序。当然, 这也意味着SQLite引擎中存储的数据直接用SQL查询得到的结果看起来是乱码 。
Cozo 使用 _面向行_ 而非 _面向列_ 的二进制存储格式。在这个格式中,对键的存储通过 [memcomparable ](https://github.com/facebook/mysql-5.6/wiki/MyRocks-record-format#memcomparable-format ) 的方法将复合键存储为一个字节数组,而直接对这些字节数组按照字节顺序排序就能得到正确的语义排序。这也意味着直接用 SQL 查询在 SQLite 引擎中存储的数据得到的结果看起来像是乱码。实现存储引擎本身的接口并不需要了解这个格式 。
### 查询引擎
查询引擎部分实现了以下功能:
* 函数、聚合算子、算法的定义
* 数据结构定义( schema)
* 数据库事务( transaction)
* 各种函数、聚合算子、算法的实现
* 表单 数据结构的 定义( schema)
* 数据库查询 事务( transaction)
* 查询语句的编译
* 查询的执行
Cozo中大部分代码都是在实现这些功能。CozoScript手册中[有一章](https://docs.cozodb.org/en/latest/execution.html)简要介绍了查询执行的一些细节 。
这部分包含 Cozo 项目的大部分代码。关于查询的执行,文档中[有一整章](https://docs.cozodb.org/zh_CN/latest/execution.html)来详细介绍 。
用户通过[Rust API](https://docs.rs/cozo/)来驱动查询引擎 。
Cozo 的 [Rust API ](https://docs.rs/cozo/ ) 实际上就是查询引擎的公共接口 。
### 语言、环境封装
除Rust之外的所有语言、环境, 都只是Rust API的进一步封装, 使其在相应的环境中更容易使用。例如, 在独立服务器( cozoserver) 中, Rust的API被封装为HTTP端点, 而在NodeJS中, 同步的Rust API被封装为基于JavaScript运行时的异步调用 。
Cozo 的 Rust 以外的所有语言、环境都只是对 Rust API 的进一步封装。例如, 在独立服务器( cozoserver) 中, Rust API 被封装为了 HTTP 端点,而在 Cozo-Node 中, 同步的Rust API 被封装为基于 JavaScript 运行时的异步 API 。
你也可以尝试自己封装Rust API, 使其可以用于其他语言。如果没有现成的目标语言与Rust之前的交互库, 你可以考虑包装Cozo提供的基于C语言的API。在官方支持的语言中, 只有Go直接封装了C语言的API 。
封装 Rust API 不难,如果你想让 Cozo 在其它语言上跑起来可以试试。Rust 有一些现成的库用来与其它语言交互。如果你想用某个语言而没有现成的交互库,我们建议你直接封装 Cozo 的 C 语言 API。官方支持的 Go 库就是这么实现的(通过 cgo) 。
## 项目进度
## 项目进程
Cozo是一个非常年轻的项目。欢迎任何反馈 。
Cozo 一开始预想的功能已经实现的不少了,但是项目仍然年轻得很。欢迎各界朋友使用并提出宝贵意见 。
1.0之前的版本不承诺语法、API的稳定性或存储兼容性。
Cozo 1.0 之前的版本不承诺语法、API 的稳定性或存储兼容性。
## 许可证和贡献
本项目以MPL-2.0或 更高版本授权。如果你有兴趣为该项目做 贡献,请看[这里](CONTRIBUTING.md)。
Cozo 以 MPL-2.0 或其 更高版本授权。如果你有兴趣为该项目贡献代码 ,请看[这里](CONTRIBUTING.md)。