SPL 列式计算

内存列式计算

什么是列式存储

内存中的序表,一般是采用行式存储。例如员工表包含字段 id、name、birthday,在内存中大致是这样存储的:

..

每行(也就是每条记录)存成一个 Object 数组,包括三个成员对象:[Integer,String,Date]。

一般来说,每一列(字段)数据的类型都是一样的。这时,SPL 可以按照列来存储。比如 id 列全部都是整数,就可以存成 int 数组。name 列全部都是字符串,则可以存成字符串数组 String[]。birthday 都是日期型,就可以存成日期数组 Date[]。大致如下图:

..

SPL 这种存储方式称为列式存储。要实现列式存储,序表的字段必须要保证在所有记录上的数据类型均相同,这样的字段称为纯字段。强制所有字段都是纯字段的序表为纯序表,使用列式存储,也称列表

相应的,强制成员数据类型相同的序列称为纯序列。

SPL 将符合上述条件的员工序表转换为纯序表的代码大致是这样的:


A

B

1

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

=ifpure(A1)

2

=A1.i()

=ifpure(A2)

3

=A2.o()

=ifpure(A3)

A1:将文件内的数据读入内存,生成普通序表。B1:判断 A1 是否纯序表,返回 false。

A2:用 i() 函数将普通序表 A1 转换为纯序表。如果 A1 中某个字段不是纯字段,则会报错,这种情况称为序表不纯

B2:判断 A2 是否纯序表,返回 true。

A3:用 o() 函数将纯序表 A2 转换为普通序表。B3:判断 A3 是否纯序表,返回 false。

需要注意的是:改变纯序表字段时,导致不纯则报错。

什么是列式计算

普通序表是一行一行计算的。比如要求出员工的大写姓名,并计算出员工的年龄。普通序表大致是这样计算的:

..

先计算第一行员工大写姓名和年龄,再计算第二行员工大写姓名和年龄,以此类推。

而 SPL 可以对纯序表一列一列计算,大致是这样的:

..

先计算 name 列的所有值,得到全部的大写姓名,再计算 birthday 列,达到所有的年龄。这种以列为单位的计算方式,称为列式计算

列式计算的代码大致是这样的:


A

B

1

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

2

=A1.i()

=A2.derive@o(upper(name):NAME,age(birthday):ages)

列式计算的性能优势

SPL 是基于 Java 开发的,对内存的占用比较敏感。

普通序表采用行式存储,在内存中每条记录是一个对象数组,每个字段值是对象数组中的一个对象。

纯序表采用列式存储,每一列是一个数组。由于列数往往要比行数少很多,所以列式存储可以减少生成对象的个数。

如果字段值都是简单数据类型,比如 int、long、double、boolean,列式存储就不需要生成对象。行存则简单数据类型也必须生成对象。

列式存储相比行式存储生成对象的数量少,可以减少内存占用、节省生成对象的时间。而且列式计算一次算一列,可复用很多上下文信息,总体上计算性能更有优势。

列式计算和字符串、日期类型

字符串和日期类型不是简单数据类型,即使在列式存储中,也需要生成对象。为了提高性能,需要想办法转换成简单数据类型。

字符串尽量采用序号化,转换为数值类型。比如:产品表中的产品类型字段如果是字符串,可以建立独立的外键表,将产品类型字符串转化为外键表的序号。

日期时间类型要尽量存储成 int 型。SPL 提供 days@o 函数,可以实现这种转换。在需要的时候再用 date@o 函数转成日期时间类型。

列式计算和 new、derive、run 函数

序表使用 new 和 derive 函数的时候,计算结果是将原来的序表复制一份。纯序表推荐使用 new@o 和 derive@o,是直接在原表的基础上增加列。对于列式存储来说,多加一列的计算成本很低,这一点和行式计算正好相反。行式计算中多一列会导致所有行重新生成,效率很低。

列式计算不推荐使用 run 函数,计算时如果用 run 给纯序表原字段赋值,会强迫改成行式计算。

列式计算和 switch、join 函数

switch 是将字段切换成记录(引用地址),记录是对象,不是简单变量。所以列式计算不推荐使用 switch 函数。比如下面这样的代码就不适合用于列式计算:


A

1

=ORDERS.switch(O_CUSTKEY,CUSTOMER:C_CUSTKEY)

join 函数可以将关联表字段拼接到当前表,更适合列式计算。这个代码要改成这样:


A

1

=ORDERS.join(O_CUSTKEY,CUSTOMER:C_CUSTKEY,NAME:CNAME)

列式计算和临时变量

列式计算不推荐使用临时变量。比如纯序表 T 有三个整数字段 f1、f2、f3,要算 f1 平方与 f2 的差、与 f3 的和。

下面的代码中使用了临时变量,不适合列式计算:


A

1

=T.new(t=f1*f1,t-f2:r1,t+f3:r2)

列式计算中简单数据类型没有对象,一个整数就是整数数组的一个成员,不是 Integer 对象。

临时变量 t 要存成一个对象,没办法做成整数数组,无法参与列式计算。这个代码要改成这样:


A

1

=T.new(f1*f1:t,t-f2:r1,t+f3:r2)

这种写法增加了一列 t,可以避免使用临时变量。列式计算临时多加一列的计算成本很低。

列式计算相关函数

纯序表用 new、derive、groups 函数仍然返回纯序表。而用 conj(),select(),sort(),group(),align(),j()

函数计算后,要加 @v 才会返回纯序表。

组表中用 T.import() 读入数据时,可以加 @v 生成纯序表。

组表或组表的游标用 T.memory() 读入数据生成内表时,也可以加 @v 生成纯序表的内表。

列式游标

什么是列式游标

游标是将数据库或者文件中的数据分批读取到内存中进行计算。对于数据库游标和组表游标来说,SPL 可以将数据分批读入内存形成纯序表进行列式存储和计算,这样的游标称为列式游标

以员工组表和数据库中的员工表为例,列式游标的代码写法大致是这样:


A

1

=file("employees.ctx").open().cursor@v(id,name;manager=="Tom")

2

=connect("db").cursor@v("select id,name from employees where manager='Tom'")

列式游标计算时,如果发现生成的序表不纯,则会报错。

我们也可以在生成组表的时候用 create@v 函数。这样,数据维护时 SPL 会对比列是否纯,

而且还会保存数据类型,适合用做列式游标。代码大致是这样的:


A

1

=file("employees.ctx").create@v(#id,name,address,manager,…)

列式游标的性能优势

列式游标计算时,数据分批读入内存生成纯序表,所以也有内存占用小,计算性能高的优势。

列式游标的使用

用作列式游标的组表尽量采用简单数据类型:int、long、double、boolean。这样做更有利于列式计算减少内存占用,提高性能。字符串要尽量序号化,日期时间类型尽量转成 int 或 long。

纯序表计算时如果用 new、run 函数给原字段赋值,会强迫改成行式计算。因此列式游标不要给原字段赋值。也不要使用临时变量。

列式游标做外键关联计算时,不推荐使用 switch 函数,建议使用 join 函数。

列式游标相关函数

列式游标使用 group 函数时,加上 @v 选项每组子集将复制成新的纯序表。要注意的是,序表和游标的 group@v 不一定比 group 性能更好,应用中需要实际测试决定。

select 函数加上 @v 选项计算结果仍是列式游标。cs.new 和 cs.derive 函数加上 @o 选项,也是在原纯序表基础上增加字段,不会复制整个纯序表。

列式游标和并行计算

组表的列式游标和普通游标一样支持并行,只需要加上 @mv 选项、和并行数参数即可。

多个列式游标并行归并时,也要注意同步分段。以订单表和在线商品表有序归并为例,代码大致是这样的:


A

1

=file("ORDERS.ctx").open().cursor@mv(O_ORDERKEY,O_ORDERDATE;;4)

2

=file("LINEITEM.ctx").open().cursor@v( L_ORDERKEY,L_PRICE,L_QUANTITY;;A1)

3

=A3.joinx@im(L_ORDERKEY,A2:O_ORDERKEY,O_ORDERDATE)

4

=A2.groups(O_ORDERDATE;sum(L_PRICE*L_QUANTITY):AMOUNT)