SPL 的新关联计算

SPL 中的关联计算 - 内存篇》介绍了 SPL 对关联计算的分类,以及内存关联计算的编程方法。

SPL 中的关联计算 - 外存篇》介绍了外存关联计算的编程方法。

本篇继续介绍 SPL 的新关联计算方法,包括:用于外键连接的fjoin函数、组表游标关联过滤机制和用于主键连接的pjoin、new/news函数。

这些新方法在合适的场景下,性能要好于前两篇的方法。不过前面的方法概念更简单、容易理解,初学者可以先掌握这些方法。要在实践中遇到合适的场景,就会发现新关联方法能减少计算量,进一步提高性能。

阅读本篇前建议先读完前两篇,了解 SPL 对连接的分类,并熟悉针对不同连接类型选择计算方法的模式。这些知识本篇都会用到,不再详细介绍。

外键连接

1、 外键地址化与序号关联的统一

前两篇介绍过外键地址化,是将事实表的外键字段转换为维表记录的内存地址。外键序号化则是先将事实表外键字段转换为维表记录的位置序号,关联计算时再用序号取得维表记录。

但前面介绍的两个函数 switch/join 实现外键地址化和序号化关联的语法是不统一的,我们通过一个例子来回顾一下。

以订单(事实表)、员工(维表)为例,两表按照员工号连接。现在要按照员工姓名和订单日期来过滤订单数据。

为了区分清楚,将订单表员工号设为 o_eid,员工表的员工号为 eid。

用 switch 函数实现外键地址化的代码大致是这样:


A

B

1

=file("orders.btx").import@b()

=file("employee.btx").import@b().keys@i (eid)

2

>A1.switch(o_eid,B1)


3

=A1.select(o_eid.name=="Tom" && odate=…)

如果订单表中的字段 num_eid 存储的是员工表位置序号,那么用 switch 实现序号关联的代码大致是这样:


A

B

1

=file("orders.btx").import@b()

=file("employee.btx").import@b()

2

>A1.switch(num_eid,B1:#)


3

=A1.select(num_eid.name=="Tom" && odate=…)

A2:switch 中使用了 #代表序号关联。也就是说 switch 要借助特殊的符号才可以实现序号关联,造成序号关联与外键地址化的语法并不一致。

再来看 join 函数实现外键地址化的代码:


A

B

1

=file("orders.btx").import@b()

=file("employee.btx").import@b().keys@i(eid)

2

=A1.join(o_eid,B1,~:fk_eid,name)

3

=A2.select(name=="Tom" && odate=…)

A2:join 新建了一个表。除了事实表字段外,还拼接了一个新字段 fk_eid 存储维表记录地址,完成了外键地址化。~ 代表关联上的维表记录。

实际上 join 更常见的用法是将维表字段 name 和事实表拼接在一起,后续计算就直接使用 name 字段,~:fk_eid 可以省去。

join 函数实现序号化关联代码是这样的:


A

B

1

=file("orders.btx").import@b()

=file("employee.btx").import@b()

2

=A1.join(num_eid,B1:#,name)


3

=A2.select(name=="Tom" && odate=…)

A2 中 join 函数也使用 #,同样是特殊的符号。即 join 实现外键地址化关联和序号关联的语法也是不统一的。这个例子就省去了 ~:fk_eid,直接拼接 name 字段。

fjoin 函数可以实现外键地址化和序号化关联在语法上的统一。先看 fjoin 实现外键地址化的代码,大致是下面这样:


A

B

1

=file("orders.btx").import@b()

=file("employee.btx").import@b().keys@i(eid)

2

=A1.fjoin(B1.find(o_eid),~:fk_eid,name)

3

=A2.select(name=="Tom" && odate=…)

A1:将订单表读入内存。B1:将员工表读入内存,并定义带索引的主键为 eid。

A2:在 A1 的基础上新建一个表,对 A1 的每一行都执行表达式B1.find(o_eid),返回员工表的记录地址。~:fk_eid,name 是将员工表记录地址和 name 字段拼接到新表上。~ 代表表达式返回值。

和 join 函数一样,fjoin 推荐把维表数据拼接到事实表上,所以 ~:fk_eid 也可以省略。

再看 fjoin 实现序号化关联的代码,大致是下面这样:


A

B

1

=file("orders.btx").import@b()

=file("employee.btx").import@b()

2

=A1.fjoin(B1(num_eid),~:fk_eid,name)

3

=A2.select(name=="Tom" && odate=…)

A2 中除了表达式换成按照序号位置取维表记录外,其他语法和地址化完全一样。

从上面的例子可以看到,fjoin 覆盖了 join 的功能,可以直接将维表记录拼接到事实表上。这是 fjoin 比较常见的用法。

fjoin 也可以覆盖 switch 功能,直接给外键字段赋值实现地址化或者序号化关联。先看地址化的写法:


A

B

1

=file("orders.btx").import@b()

=file("employee.btx").import@b().keys@i(eid)

2

=A1.fjoin(o_eid=B1.find(o_eid))

3

=A2.select(o_eid.name=="Tom" && odate=…)

A2:表达式按照主键找到维表记录地址,赋值给事实表的外键字段,实现了外键地址化。

再看序号化关联:


A

B

1

=file("orders.btx").import@b()

=file("employee.btx").import@b()

2

=A1.fjoin(num_eid=B1(num_eid))

3

=A2.select(num_eid.name=="Tom" && odate=…)

A2:表达式按照序号找到维表记录地址,赋值给事实表的外键字段,实现了序号关联。

从上述例子可以看到,fjoin 不仅从语法上统一了地址化和序号化关联,还覆盖了 switch/join 函数的功能。

但是,fjoin 和 switch/join 也有一些不同之处。

首先,switch 会自动判断维表的主键是否有索引,如果有则直接使用,没有则建立且自行管理。而 fjoin 本质上是使用 find 函数在做赋值计算,没有这个机制,所以维表需要自行建立主键的索引。

其次,是 switch 直接修改原表,fjoin 则是生成新表。

第三,fjoin 支持多线程并行计算,这些例子中 fjoin 直接加上 @m 就可以了。switch 和 join 不支持多线程并行。

2、表达式的灵活运用。

fjoin 最大的优势是用表达式实现关联,表达式的使用非常灵活。

比如运用 fjoin 的表达式,很容易完成多字段外键关联,只要将表达式改成多字段就可以了。

假设班级表主键包括两个字段:专业号 mid 和班号 cid,现在要用学生表中的 s_mid、s_cid 两个字段和班级表的主键关联,代码大致是这样:


A

1

=file("class.btx").import@b(mid,cid,teacher).keys@i(mid,cid)

2

=file("student.btx").import@b(s_mid,s_cid,…).fjoin(A1.find(s_mid,s_cid),teacher)

利用表达式,还能实现极致灵活的外键关联。

假设班级表的主键 classid 是专业号 mid 和班级号 cid 两个整数组成的。最后两位整数是 cid,前面是 mid,也就是 classid=mid*100+cid。而学生表中则用两个字段分别存储 s_mid 和 s_cid。

现在要实现这两个表的外键关联,代码大致是这样:


A

1

=file("class.btx").import@b(classid,teacher).keys@i(classid)

2

=file("student.btx").import@b(s_mid,s_cid,…).fjoin(A1.find(s_mid*100+s_cid),teacher)

A2 中,使用 fjoin 的表达式很容易实现这样复杂的外键关联。

3、fjoin 的 @i 选项

前两篇介绍过 switch/join 函数的 @i 选项,只保留事实表能关联上的记录。@d 选项则相反,是去掉事实表关联上的记录。

fjoin 也有 @i 选项,如果表达式返回值是 false/null,@i 选项就会把相应记录过滤掉。比如订单表和员工、客户两个维表关联且去掉关联不上的订单,现在要按照客户城市 city 和员工部门 dept 分组汇总订单数和金额,代码大致是下面这样:


A

B

1

=file("employee.btx").import@b(eid,dept).keys@i(eid)

=file("customer.btx").import@b(cid,city).keys@i(cid)

2

=file("orders.ctx").open().cursor(o_cid,o_eid,amount)

3

=A2.fjoin@i(B1.find(o_cid),city;A1.find(o_eid),dept)

4

=A3.groups(city,dept;count(1),sum(amount))

A3:fjoin 加上 @i,且有用分号分开的两组表达式。

第一组表达式在客户表中找客户号对应记录,关联不上的返回值为 null。

第二组表达式在员工表中找员工号对应记录,关联不上的返回值为 null。

这两组表达式是“并且”关系,也就是说都不为 null 的时候,最终的值是 true,否则为 false。

fjoin 保留最终值是 true 的订单记录,同时拼接上两个表的 city 和 dept 字段。

A4:按照需求分组汇总。

如果需要得到类似 @d 的结果,只要在表达式前加“!”就可以了,所以 fjoin 不需要 @d 选项。

仍以订单和员工、客户的关联为例,现在要仅保留两个表都关联不上的订单,代码大致是这样:


A

B

1

=file("employee.btx").import@b(eid).keys@i(eid)

=file("customer.btx").import@b(cid).keys@i(cid)

2

=file("orders.ctx").open().cursor(o_cid,o_eid,odate,amount)

3

=A2.fjoin@i(!B1.find(o_cid);!A1.find(o_eid))

4

=A3.groups(odate;count(1),sum(amount))

A1、B1:员工和客户表仅用于过滤订单表,不需要的字段不再读出。

A3:fjoin 还是用分号分开的两组表达式。

第一组表达式在客户表中找客户号对应记录,加! 后,能关联上的返回值为 null。

第二组表达式在员工表中找员工号对应记录,加! 后,能关联上的返回值为 null。

这两组表达式是“并且”关系,也就是说都不为 null 的时候,最终的值是 true,否则为 false。

fjoin 保留最终值是 true 的订单记录。

A4:这时候保留的都是关联不上的订单,所以后续计算仅能使用订单表自身的字段。

当事实表关联多个维表,很可能有些维表需要 @i,有些则需要 @d。switch/join 两个函数同时只能有一个 @i 或 @d 选项,无法做到有些 @i 有些 @d。

fjoin 表达式的写法非常灵活,很容易实现这样的需求。

还是看订单表和员工、客户两个维表关联,现在要找能关联上客户,且关联不上员工的订单。


A

B

1

=file("employee.btx").import@b(eid).keys@i(eid)

=file("customer.btx").import@b(cid,city).keys@i(cid)

2

=file("orders.ctx").open().cursor(o_cid,o_eid,amount)

3

=A2.fjoin@i(B1.find(o_cid),city;!A1.find(o_eid))

4

=A3.groups(city;count(1),sum(amount))

A3:fjoin 仍用分号分开的两组表达式。

第一组表达式在客户表中找客户号对应记录,关联不上的返回值为 null。

第二组表达式在员工表中找员工号对应记录,加! 后,能关联上的返回值为 null。

这两组表达式是“并且”关系,也就是说都不为 null 的时候,最终的值是 true,否则为 false。

fjoin 保留最终值是 true 的订单记录。

上面的例子中,多个关联过滤条件之间是“且”的关系,任何一个不匹配就过滤掉了。但有时候也会需要“或”的关系。

假设要用订单表中三个字段和三个不同的客户表做关联。三个表的主键字段依次是:客户号 cid,客户名 cname、联系人 ccontact。如果某订单记录在三个客户表中都关联不上,就要被舍弃。

使用 fjoin 实现这个需求的代码大致是这样:


A

1

=file("customer1.btx").import@b().keys@i(cid)

2

=file("customer2.btx").import@b().keys@i(cname)

3

=file("customer3.btx").import@b().keys@i(ccontact)

4

=file("orders.ctx").open().cursor(o_cid,o_cname,o_ccontact,amount)

5

=A4.fjoin@i((o_cid=A1.find(o_cid),o_cname=A2.find(o_cname),o_ccontact=A3.find(o_ccontact),o_cid||o_cname||o_ccontact))

6

=A5.groups(ifn(o_cid,o_cname,o_ccontact).city;sum(amount))

A5:前三个表达式负责关联赋值,最后一个表达式是过滤条件,其中的“或”计算实现了这个例子的需求。

如果用 switch 或 join,就要将三个外键都关联出来,然后再用 select 过滤掉一个维表都没有关联上的订单记录。

4、 组表游标关联过滤

fjoin@i 意味着事实表不仅关联还要过滤。如果事实表是组表游标,可以在游标前过滤时实现关联。这种方法性能更好,称为组表游标关联过滤机制。

用这个机制来实现上一节“有些维表 @i 有些 @d”例子,代码大致是这样的:


A

B

1

=file("employee.btx").import@b(eid).keys@i(eid)

=file("customer.btx").import@b(cid,city).keys@i(cid)

2

=file("orders.ctx").open().cursor(o_cid,amount;o_cid:B1,o_eid:A1:null)

3

=A2.groups(o_cid.city;count(1),sum(amount))

A2:原来 fjoin@i 中的表达式,放到了游标前过滤时执行。

游标前关联过滤机制相当于 fjoin 强制加上了 @i。如果希望只关联不过滤,一般还是要使用不带选项的 fjoin。

上节三个客户表的例子也可以用组表游标关联过滤机制来实现:


A

1

=file("customer1.btx").import@b().keys@i(cid)

2

=file("customer2.btx").import@b().keys@i(cname)

3

=file("customer3.btx").import@b().keys@i(ccontact)

4

=file("orders.ctx").open().cursor(o_cid,o_cname,o_ccontact,amount;(o_cid=A1.find(o_cid),o_cname=A2.find(o_cname),o_ccontact=A3.find(o_ccontact),o_cid||o_cname||o_ccontact))

5

=A4.groups(ifn(o_cid,o_cname,o_ccontact).city;sum(amount))

A4 将原来写在 fjoin@i 中的表达式写在游标前过滤条件中。

组表游标关联过滤机制在写法上和 fjoin@i 一致,但计算性能比 fjoin@i 好。这是由于,写在组表游标前过滤条件中的表达式相当于有强制的 @i 选项,其计算发生在游标生成记录之前,若返回值是 null/false 则订单记录将不会被生成。这样可以减少游标读取数据、生成记录的时间,提高性能。

而直接用 fjoin,那么关联过滤发生在游标记录生成之后,即使这条订单记录不满足条件,也已经生成了。

因此我们可以说,组表游标关联过滤是大数据外键关联最常见也是最推荐的方法,游标上的 fjoin@i 反而会较少被用到。

需要注意的是,游标关联过滤机制仅用于组表。其他类型游标(集文件、文本文件、数据库等)不支持游标前过滤,无法实现这个机制。

主键连接

1、 pjoin 函数可实现 join/joinx 的基本功能

比如员工表和经理表通过各自主键做连接,求薪酬和津贴之和的代码大致是这样:


A

B

1

=file("employee.ctx").open().cursor(eid,name,salary)

=file("manager.ctx").open().cursor(mid,allowance)

2

=A1.pjoin(eid,eid:id,name,salary;B1,mid,allowance)

3

=A2.derive(salary+allowance:income)

A2:pjoin 必须有个基准表,这里是 A1 员工表。

如果 pjoin 参与关联的表中有游标,那么要求参与计算的表都对关联字段有序。这与 joinx 是一致的。

和 join/joinx 类似,pjoin 也支持内连接、左连接和全连接。

上面例子中得到的是内连接结果。还有很多时候需要左连接,代码写法大致是这样:


A

B

1

=file("employee.btx").import@b()

=file("manager.btx").import@b()

2

=A1.pjoin(eid,eid:id,name,salary;B1:null,mid,allowance)

3

=A2.derive(salary+allowance:income)

A2 中的 B1:null 是指:对于员工关联不上经理表的记录,在结果中经理表的字段都为 null。

如果只保留关联不上的员工记录,是这样写:


A

B

1

=file("employee.btx").import@b()

=file("manager.btx").import@b()

2

=A1.pjoin(eid,eid:id,name,salary;B1:null,mid)

3

=A2.sum(salary)

A2 中的 B1:null 后边只有主键字段,这时候结果中仅保留关联不上的员工记录。结果集不会出现经理表的字段。

pjoin 不直接支持右连接,假设员工和经理右连接需要转成经理和员工左连接,再用 pjoin 实现。

pjoin 实现全连接的代码大致是这样的:


A

B

1

=file("employee.btx").import@b()

=file("manager.btx").import@b()

2

=A1.pjoin@f(eid,eid:id,name,salary;B1,mid,allowance)

3

=A2.derive(salary+allowance:income)

两个表记录都会出现在结果中,不管是否能关联上。能关联上的,结果记录包含两个表的字段;一个表关联不上另一个表的,另一个表的字段都为 null。

这时即使写成 B1:null,null 也会忽略掉,仍然得到全连接的结果。

pjoin 和 join/joinx 有两个主要区别。

第一个是 join/joinx 在计算左连接时会以第一个表为基准,但是内连接和全连接则没有基准表,参与的各个表地位都一样。

而 pjoin 实现这三种连接方式时,都必须有基准表。

第二个区别是 join/joinx 的结果是各个表对应记录组成的记录。

而 pjoin 则更提倡用各个表的字段拼出结果。比如 A2 的计算会得到 id、name、salary、allowance 四个字段组成的结果集,前三个来自于员工表,最后一个是经理表字段。

pjoin 这样设计可以有效提高性能,后面会有详细介绍。

2、多个表主键关联,有左连接也有内连接

假设在员工表、经理表之外增加一个工资表 salary,保存员工号 sid 和工资 salary。现在要计算工资和津贴之和,只有员工和工资表可以关联上的记录才参加计算,且员工和经理表关联时以员工表为准。

也就是说 employee 和 salary 是内连接,employee 和 manager 是左连接。代码大致是下面这样:


A

1

=file("employee.btx").import@b(eid,name)

2

=file("manager.btx").import@b(mid,allowance)

3

=file("salary.btx").import@b(sid,salary)

4

=A1.pjoin(eid,eid:id,name;A3,sid,salary;A2:null,mid,allowance)

5

=A4.derive(salary+allowance:income)

join/joinx 函数则写不出 A4 这样的运算。

3、用组表的new/news函数实现主键连接

new 函数可以实现同维关联,以员工表和经理表为例。


A

B

1

=file("employee.ctx").open()

=file("manager.ctx").open().cursor(mid,allowance)

2

=A1.new(B1,mid,allowance)

3

=A2.derive(salary+allowance:income)

A1:打开组表,注意这里不是组表游标。B1 打开经理表,建立游标。

A2:用员工表的主键 eid 对应经理表的第一个字段 mid 关联。两个表都要对关联字段有序。

new 函数也能用于主表关联子表。以订单和订单明细为例,订单的主键是订单号 oid,订单明细的主键是 d_oid 和产品号 pid。关联代码大致是这样:


A

B

1

=file("orders.ctx").open()

=file("order_detail.btx").cursor(d_oid,price,quantity;price<100)

2

=A1.new(B1,cid,odate,sum(price*quantity):amt;odate>=date(2020,1,1))

3

=A2.groups(cid,odate;sum(amt))

A2:用 A1 的主键 oid 去对应 B1 的第一个字段 d_oid。两个表都要对关联字段有序。

由于 A2 中一个订单会对应多个订单明细记录,new 函数要求这些订单明细记录要做聚合运算。

B1 是游标时,new 函数返回游标;B1 是序表时,返回序表。

new 函数可以加 @r 选项,这时订单明细表不聚合,订单表的记录会按照对应订单明细记录数被复制成多条。

对于子表关联主表的情况,可以使用 news 函数,代码大致是这样的:


A

B

1

=file("orders.ctx").open().cursor(oid,cid,odate;odate==date(2020,1,1))

=file("orders_detail.ctx").open()

2

=B1.news(A1:oid,cid,odate;price>100)

A2:news 以 B1 的主键 d_oid 去对应订单的第一个字段 oid。两个表都要对关联字段有序。

订单明细有多条记录和一条订单记录对应,news 会复制订单记录。

news 函数可以加 @r 选项,这时订单明细表需要聚合运算后再关联,订单表的记录不会被复制。

A1 是游标时,news 函数返回游标;A1 是序表时,返回序表。

new/news 函数实现主键关联具有性能优势:这两个函数计算时,组表会自动随着游标(或序表)跳过一些数据块。

比如在上面 new 函数的例子中,先取一批订单明细记录,再根据这些记录中 d_oid 的取值范围去取订单记录,这样相对于对订单表加了个过滤条件,而组表对主键有序,在涉及主键过滤时会自动跳块(某数据块中没有满足条件的主键时将被整个跳过),从而提高计算性能。特别是订单明细(过滤之后)变得很少的时候,跳块效果更强,性能提升更明显。

4、pjoin 支持对子表做聚合、复制主表记录

pjoin 覆盖并扩展了组表的 new/news 函数应用范围。除了组表之外,也可以应用于序表或其他类型游标。

先看主表关联子表的情况,子表要使用聚合函数,将多条记录聚合后和主表连接。

例如订单表和订单明细表连接,计算每个订单的金额,用 pjoin 实现的代码大致是这样的:


A

B

1

=file("orders.btx").cursor@b(oid,odate)

=file("order_detail.btx").import@b(d_oid,price,quantity).sort(d_oid)

2

=A1.pjoin(oid;B1,d_oid,sum(price*quantity):amt)

3

=A2.groups(cid,odate;sum(amt))

A2:pjoin 用订单表 A1 的主键 oid 去关联订单明细表的部分主键 d_oid。由于其中一个表是游标,所以需要所有参与的表都对关联字段有序。

订单明细每条记录的价格乘以数量,按照 d_oid 分组聚合后,再和订单表关联并拼接字段。

pjoin 也支持用子表关联主表。比如明细表中的各条记录都要找到对应的客户号,代码大致是这样的:


A

B

1

=file("orders.btx").import@b(oid,cid)

=file("order_detail.btx").import@b(d_oid,pid,price,quantity)

2

=B1.pjoin(d_oid,price,quantity;A1,oid,cid))

A2:用订单明细表的部分主键 d_oid 对应订单表的第一个字段 oid。

由于多条订单明细对应一条订单,pjoin 会将订单记录复制成多条,再和明细字段拼接在一起。

5、pjoin 跟随跳块

如果参与 pjoin 计算的表有组表游标,那么也可以实现类似 new/news 的跟随跳块机制,有效提高性能。假设订单和订单明细都是组表游标:


A

B

1

=file("orders.btx").open().cursor(oid,odate;odate==date(2020,1,1))

=file("order_detail.ctx").open().cursor(d_oid,quantity,price)

2

=A1.pjoin(oid;B1,d_oid,sum(price*quantity):amt)

3

=A2.groups(cid,odate;sum(amt))

pjoin 计算连接时,B1 订单明细游标会自动随着订单游标 A1 跳过一些数据块,和前面介绍的 new/news 跟随跳块机制是一样的。

A1 中的订单表也可以是序表,pjoin 计算时订单明细游标仍可以做到跟随跳块。

有些情况下可能订单明细表(过滤后)比订单表小,需要订单组表游标跟随明细表跳数据块,pjoin 代码大致是这样:


A

B

1

=file("orders.ctx").open().cursor(oid,cid,odate)

=file("order_detail.btx").open().cursor(d_oid,quantity,price;price<100)

2

=A1.pjoin@r(oid;B1,d_oid,sum(price*quantity):amt)

3

=A2.groups(cid,odate;sum(amt))

A2 中使用 @r 选项,A1 的游标会跟随 B1 跳块。这个例子中 B1 是游标,在实际应用中也可以是序表。

注意全连接 @f 的时候,pjoin 不会跟随跳块。

6、列式游标

pjoin 支持 SPL 企业版纯序表列式游标机制,代码是这样的:


A

B

1

=file("orders.ctx").open().cursor@v(oid,cid,odate)

=file("order_detail.btx").open().cursor@v(d_oid,quantity,price)

2

=A1.pjoin(oid;B1,d_oid,sum(price*quantity):amt)

3

=A2.groups(cid,odate;sum(amt))

A1、B1 中游标加 @v 选项,就是使用列式游标。

由于 join/joinx 不一定有基准表,难以确定结果是不是用列式的。前边介绍过,pjoin 必须有基准表,所以计算时可以根据基准表决定是不是采用列式计算。

join/joinx 用各表的记录组成结果记录的做法,在行式计算的时候是有性能优势的。列式计算时这样做反而会影响性能,所以 pjoin 才会像前面介绍的那样,提倡用各表的字段拼出结果记录。


以下是广告时间

对润乾产品感兴趣的小伙伴,一定要知道软件还能这样卖哟性价比还不过瘾? 欢迎加入好多乾计划。
这里可以低价购买软件产品,让已经亲民的价格更加便宜!
这里可以销售产品获取佣金,赚满钱包成为土豪不再是梦!
这里还可以推荐分享抢红包,每次都是好几块钱的巨款哟!
来吧,现在就加入,拿起手机扫码,开始乾包之旅



嗯,还不太了解好多乾?
猛戳这里
玩转好多乾