这篇文章翻译自 http://nathanmarz.com/blog/new-cascalog-features-outer-joins-combiners-sorting-and-more.html。以下进入正文。
在 第一篇教程(译文)中,我讲了 Cascalog 的很多强大的功能:连接(join),累计(aggregates),子查询(subqueries),自定义操作(custom operations),等等。从几周前 Cascalog release 以后,我又加了很多新的特性,这些新特性使 Cascalog 更富有表现力、更强大,但丝毫不影响原有的简洁和灵活。
跟第一篇教程一样,先通过以下命令把 playground 加载进来:
lein compile-java && lein compile
lein repl
user=> (use 'cascalog.playground) (bootstrap)
我们在第一篇教程中就已经看到,你可以用 Cascalog 把来自多个数据源的数据 join 在一起,只要在多个数据源中使用相同的变量名。例如,在 age 和 gender 数据源中,我们运行这一段代码来得到每个人的年龄和性别:
user=> (?<- (stdout) [?person ?age ?gender]
(age ?person ?age) (gender ?person ?gender))
这其实是一个 inner join,只有在两个数据集中都出现的人才会出现在结果中。我们可以这样做 outer join :
user=> (?<- (stdout) [?person !!age !!gender]
(age ?person !!age) (gender ?person !!gender))
这个查询的结果对于没有在 age 或 gender 数据集中没有出现的人,用空值替代不存在的值。
Cascalog 的 outer join 由以 !!
开头的变量名触发,这些变量称为 “未决变量”(原文叫 “ungrounding variales”),包含未决变量的谓词叫做 “未决谓词”(原文叫 “unground predicate”),不包含未决变量的谓词称作 “已决谓词”(原文叫 “ground predicate”)。对两个未决谓词做 join 是一个 full outer join,对一个已决谓词和一个未决谓词做 join 是一个 left join。
这里有个 left join 的例子,获取 person 数据集中所有人的 follow 关系,如果没有 follow 关系则用空值代替:
user=> (?<- (stdout) [?person1 !!person2]
(person ?person1) (follows ?person1 !!person2))
要拿到所有没有 follow 关系的人,可以这样
user=> (?<- (stdout) [?person]
(person ?person) (follows ?person !!p2) (nil? !!p2))
(nil? !!p2)
谓语是在 join 之后执行的,这是 Cascalog 的 outer join 语义中很重要的一部分。
现在我们要数一下每个人 follow 人的数量,正常的 count
累计器不会生效,因为那会计算所有元组的数量,但不会区分 follows 元组是否为空。在这种情况下,我们需要对空的 follow 元组计数为 0,对非空的计数为 1,Cascalog 的 !count
累计器可以做这件事:
user=> (?<- (stdout) [?person ?count]
(person ?person) (follows ?person !!p2) (c/!count !!p2 :> ?count))
没有 follow 关系的人会被计数为 0。
一个未决变量只能在一个查询中出现一次,其他时候都跟正常变量一样。
常规的累计器会把一个分组的所有元组都传到一台机器上去做运算,但有很多累计器如 count
,sum
,min
和 max
,可以被并行计算。例如,要计算总和,可以吧元组分成几个子集,计算各个子集的和,然后再把子集的和汇总来得到最后的结果,很多累计器都可以这样计算。
Cascalog 现在可以允许你定义 “并行累计”,这样可以尽可能的在 map 阶段完成这些计算,而不用全拖到 reducer 里去完成。map 端的累计器叫做 “combiners”。Cascalog 甚至可以在运行多个并行累计的时候用到 combiners。下面的例子就是用了 combiners:
user=> (?<- (stdout) [?count ?sum]
(integer ?n) (c/sum ?n :> ?sum) (c/count ?count))
Cascalog 会在可能的情况下自动插入 combiners,你不需要做任何事就能利用这个优化。
如果你想把通过 defaggregateop
或 defbufferop
定义的常规累计器用于并行累计,Cascalog 就不能自动用 combiners,所有的累计都会发生在 reducer 任务中。比如,下面这个用自定义累计器的查询就会把所有的累计放到 reducer 阶段:
user=> (defaggregateop product
([] 1)
([total val] (* total val))
([total] [total]))
user=> (?<- (stdout) [?prod ?count]
(integer ?n) (product ?n :> ?prod) (c/count ?count))
并行累计器可以通过 defparallelagg
来定义。具体例子可以去 cascalog.ops
里去看。
在分组很少,比如计算全局数量的时候,你会看到这个特性在速度上会有很大的提升。
“隐式等式约束” 特性是指定等式约束的比较优雅的方法,这个特性还是通过例子来讲比较好。在 playground 里定义了一个 “integer” 的数据集,里面包含了个数字的集合,如果要的到所有平方后和本身相等的数字,可以这样搞:
user=> (?<- (stdout) [?n] (integer ?n) (* ?n ?n :> ?n))
Cascalog 检测到我们尝试给 ?n 变量重新绑定值,会自动的过滤掉 *
谓词的输出和输入不相等的元组。
还有一些其他的情况你可以用这个特性。比如,要找到 “num-pair” 数据源里前后相等的两个数字对,可以这样:
user=> (?<- (stdout) [?n] (num-pair ?n ?n))
如果想拿到第二个数字是第一个数字两倍的数字对:
user=> (?<- (stdout) [?n1 ?n2]
(num-pair ?n1 ?n2) (* 2 ?n1 :> ?n2))
这没什么可多说的,很直观。
默认情况下,累计器接收到的元组是任意顺序的,Cascalog 现在有 :sort
和 :reverse
谓词来控制元组到达累计器的顺序。举个例子,我们可以这样找出一个人 follow 的年龄最小的人:
user=> (defbufferop first-tuple [tuples] (take 1 tuples))
user=> (?<- (stdout) [?person ?youngest] (follows ?person ?p2)
(age ?p2 ?age) (:sort ?age) (first-tuple ?p2 :> ?youngest))
要找到 follow 的年纪最大的人,只要再加上 :reverse
谓词:
user=> (?<- (stdout) [?person ?youngest] (follows ?person ?p2)
(age ?p2 ?age) (:sort ?age) (:reverse true)
(first-tuple ?p2 :> ?youngest))
如果你的查询中没有累计器,Cascalog 默认会在 reduce 这步去掉所有重复的元组,你可以通过 :distinct
谓词来控制这个行为。比较下面两个查询:
user=> (?<- (stdout) [?a] (age _ ?a))
user=> (?<- (stdout) [?a] (age _ ?a) (:distinct false))
第二个查询的输出中会有重复。这个功能的用例是用一个子查询来对输入做一些预处理的时候。