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) |
英文版