查询技巧#

空值的问题#

Cozo 的类型检查很严格:下面的查询

?[a] := *rel[a, b], b > 0

b 为空值时会报错,因为只能对相同类型的值做大小比较。解决方法是使空值代表某一个具体的非空值:

?[a] := *rel[a, b], (b ~ -1) > 0

这里的 ~ 是“聚凝”算符。括号其实不是必须的,但是可以让这个查询更易读一些。

你也可以显式检查空值:

?[a] := *rel[a, b], if(is_null(b), false, b > 0)

你也可以使用 cond 结构。

如何连接表#

假设我们有如下存储表:

:create friend {fr, to}

我们想知道 Alice 的朋友的朋友的朋友的朋友的朋友们都是谁。我们可以这么写:

?[who] := *friends{fr: 'Alice', to: f1},
          *friends{fr: f1, to: f2},
          *friends{fr: f2, to: f3},
          *friends{fr: f3, to: f4},
          *friends{fr: f4, to: who}

也可以这么写:

f1[who] := *friends{fr: 'Alice', to: who}
f2[who] := f1[fr], *friends{fr, to: who}
f3[who] := f2[fr], *friends{fr, to: who}
f4[who] := f3[fr], *friends{fr, to: who}
?[who] := f4[fr], *friends{fr, to: who}

这两种写法的返回值是相同的。但是,在真实的社交网络数据中,第二种写法比第一种写法快得多(指数级别地快),其原因在于 Cozo 的表遵循集合语义,因此第二种写法中每层朋友都被去重了。与此相对的,第一种写法只有在返回时朋友们才会被去重,而这其中生成的中间结果就太多了。实际上,即使每个规则的返回值中没有重复,第二种写法也会更快,因为 Cozo 的各个规则在语义资源允许的情况下是并行计算的。

因此,我们在写查询语句的时候,应该尽量将查询分为多个小规则。这不但让查询更易读,也会让查询运行得更快(这一点和其他一些数据库正好相反)。当然,上面的例子其实用递归查询更好:

f_n[who, min(layer)] := *friends{fr: 'Alice', to: who}, layer = 1
f_n[who, min(layer)] := f_n[fr, last_layer], *friends{fr, to: who}, layer = last_layer + 1, layer <= 5
?[who] := f_n[who, 5]

这里我们用了一个表达式原子 layer <= 5 来保证返回结果不是无穷大集。

第一种写法也有其作用,比如:

?[who] := *friends{fr: 'Alice', to: f1},
          *friends{fr: f1, to: f2},
          *friends{fr: f2, to: f3},
          *friends{fr: f3, to: f4},
          *friends{fr: f4, to: who}
:limit 1

因为我们要求数据库至多返回一行结果,“早停法”的机能会被激活。在这种情况下,这样写会比拆成多个规则稍微快那么一点。

另外,在聚合数据时:

?[count(who)] := *friends{fr: 'Alice', to: f1},
                 *friends{fr: f1, to: f2},
                 *friends{fr: f2, to: f3},
                 *friends{fr: f3, to: f4},
                 *friends{fr: f4, to: who}

合起来写与拆成多个规则写返回的结果是不同的。对于这个具体的查询,如果你想知道的是 Alice 到她第五层朋友们不同 路径 的数目,那只有上面这种写法是对的。另外,虽然不同路径的数量很大,以上查询执行时使用的内存非常小,因为 Cozo 在这里执行流式计算,中间结果不需要存在任何表中。