SPL 计算性能系列测试:分组累计

1、测试任务

本任务原型为某大型电器企业ERP系统中的产品库龄统计。

原始数据是如下的一对主子表:

库存表(store)

字段名

数据类型

字段含义

store

字符串

仓库ID

product

整数

产品ID

total

整数

目前的库存量

入库表(inbound)

字段名

数据类型

字段含义

store

字符串

仓库ID

product

整数

产品ID

indate

日期

入库日期

num

整数

当次入库数量

现在要计算入库表中指定截止日期前一个时间段(通常是90天)内入库产品的库龄(入库日期距离截止日期的天数):

库龄表(inbound_age)

字段名

数据类型

字段含义

store

字符串

仓库ID

product

整数

产品ID

indate

日期

入库日期

num

整数

当次入库数量

age

整数

库龄

原始数据以及计算过程有4个细节(以90天时间段为例):

1、按照入库时间顺序出库,早入库,早出库;
2、近90天入库总量少于实际库存量时,那多出来的产品的库龄都归为“90天以上”;

3、近90天入库总量多于实际库存量时,这90天已经出库了一部分,出库产品要删除掉,不再计算库龄;

4、某天入库的某产品m个,可能出库了一部分n个,那么库龄记录里个数要记为m-n个。

下面通过实际的示例数据展示计算细节:

库存表:

store

product

total

store_id_000001

1

700

store_id_000002

1

1346

store_id_000003

1

850

入库表:

store

product

indate

num

store_id_000001

1

2016-09-07

100

store_id_000001

1

2016-08-13

200

store_id_000003

1

2016-09-23

300

store_id_000003

1

2016-09-15

200

store_id_000003

1

2016-08-25

400

store_id_000003

1

2016-08-07

100

截止日期为2016-9-30,时间段为90天。计算出的库龄表如下:

store

product

indate

num

age

store_id_000001

1

2016-09-07

100

23

store_id_000001

1

2016-08-13

200

48

store_id_000001

1

2016-07-01

400

91

store_id_000003

1

2016-09-23

300

7

store_id_000003

1

2016-09-15

200

15

store_id_000003

1

2016-08-25

350

36

其中有一条indate2016-07-01的记录,库龄填成91,表示当前库存中库龄在90天以上产品。

看两组数据的计算过程:

1store=store_id_000001product=1的产品,库存是700个,有2条入库信息:

2016-09-07入库,库龄是23天,入库100个;当时库存是700 – 100 = 600

2016-08-13入库,库龄是48天,入库200个;当时库存是600 – 200 = 400

400个库存是2016-07-01以前入库的,库龄是90天以上。

2 store=store_id_000003product=1的产品,库存是850个,有4条入库信息:

2016-09-23入库,库龄是7天,入库300个;当时库存是850 – 300 = 550

2016-09-15入库,库龄是15天,入库200个;当时库存是550 – 200 = 350

2016-08-25入库,库龄是36天,入库400个,出库了50个,个数记为库里的350个。

2016-08-07入库的100个,全部出库了,所以也就不再计算库龄。

2、对标技术及测试环境

我们选用了两种关系数据库产品用作对标技术:

1. Oracle常常用于数据库性能测试的标杆,本次任务测试版本是19

2. Starrocks宣称是更快的OLAP数据库,测试版本 2.5.2

SPL20230528社区版。

测试环境为单台物理服务器,配置:

2Intel3014,主频1.7G,共12CPU64G内存,SSD固态硬盘

为考查计算技术对CPU和内存的敏感性,我们在在物理机上安装两套虚拟机环境分别测试:

VM18CPU32G内存

VM24CPU16G内存

Starrocks 至少要安装两个节点 BE FE,将承担计算任务的 BE 安装在虚拟机上,管理节点 FE 安装在物理机上,这样不会影响测试效果。SPL Oracle 都只要在虚拟机下安装就可以了。

3、数据准备

generateData.splx生成库存表store、入库表inbound


A

B

C

D

E

1

1000

100

1500



2

200000000

20

60

10

100

3


=date(2015,10,1)


 =date(2016,9,30)


4






5

=create(store,product,total)

6

=file("e:/store.ctx").create@y(#store,#product,total)

7

=create(store,product,indate,num)

8

=file("e:/inbound.ctx").create@y(#store,#product,#indate,num)

9






10

func genOne (store,product)

11


="store_id_"+mid(string(store+1000000),2)



12


>A5.insert(0,B11,product,B1+rand(C1-B1))

13


=B2+rand(C2-B2)

=to(1000).(rand(interval(B3,D3))).id@u().m(to(B13)).sort()

 

14


for B13

=elapse(B3,C13(B14))

=D2+rand(E2-D2)


15



>A7.insert(0,B11,product,C14,D14)



16



>C3 = C3+1



17




18

for 1000000

for A1

=func(genOne,A18,B18)



19



if (C3 >= A2)

>A6.append(A5.cursor()), A8.append(A7.cursor())

20




break A18


21


>A6.append(A5.cursor())

>A5.reset()



22


>A8.append(A7.cursor())

>A7.reset()



23

>A6.close()

>A8.close()




24






25

=file("e:/store.csv").export@c(file("e:/store.ctx").open().cursor())

26

=file("e:/inbound.csv").export@c(file("e:/inbound.ctx").open().cursor())

3行是对两表数据规模、字段数据范围进行了定义:

A1是产品ID范围;

当前库存量在B1~C1范围内随机,A2为入库表条数;

随机B2~C2个入库日期的记录条数,每次入库数量在D2~E2之间随机产生;

定义起止日期,入库日期从B3~D3的起止日期内随机产生。

A5A6定义两表数据结构,A6A8定义两表数据的存储文件(组表格式)

A10定义一个genOne函数,传入storeproduct两个主键字段值,生成一条主表库存表的记录,以及这条记录关联的多条子表记录。

18行对三个主键字段叉乘组合后,调用genOne函数产生每一组主、子表数据。

A25A26把组表格式(ctx)的两表转存为文本格式(csv)

实际生成了这样规模的测试数据:

库存表506万条,组表格式文件33M,文本格式(csv)123M

入库表2亿条,1年的时间跨度,组表格式文件604M,文本格式(csv)6.8G

计算三个月数据 (2016-07-01 ~ 2016-09-30)的库龄,结果集为4500万条;

SPL基于组表文件计算库龄。文本格式数据会导入OracleStarrocks,用SQL计算库龄。

4、Oracle SQL测试

SQL

CREATE TABLE inbound_age AS

    WITH t1 AS (

        SELECT d.store,d.product,

            d.indate,

            d.num,

            SUM(d.num) OVER (PARTITION BY d.store,d.product ORDER BY d.indate DESC) AS accu_num,

            ROW_NUMBER()OVER(PARTITION BY d.store,d.product ORDER BY d.indate DESC) rn,

            c.total

        FROM inbound d JOIN store c ON d.store = c.store AND d.product = c.product

        WHERE d.indate>TO_DATE('2016-07-01','YYYY-MM-DD') AND d.indate<=TO_DATE('2016-09-30','YYYY-MM-DD')

    ),

    t11 AS (

        SELECT t1.* FROM

            t1 JOIN (

                SELECT store,product,MAX(rn) max_rn

                FROM t1 WHERE accu_num - num<total GROUP BY store,product

            )t2

            ON t1.store = t2.store AND t1.product = t2.product AND t1.rn = t2.max_rn

    ),

    t_result AS (

        SELECT t.*,

            CASE WHEN over90=0

                THEN ROUND(TO_NUMBER(TO_DATE('2016-09-30','YYYY-MM-DD') - indate))

                ELSE 91 END AS age

        FROM (

            SELECT store,product,indate,

                total-accu_num AS num,1 AS over90

            FROM t11 WHERE total>accu_num

            UNION

            SELECT store,product,indate,

                CASE WHEN total<accu_num

                THEN num-(accu_num-total) ELSE num END AS num,

                0 AS over90

            FROM t1 WHERE accu_num - num<total

        )t ORDER BY store,product,indate DESC

    )

    SELECT /*+ PARALLEL (t_result,8) */ * FROM t_result

CREATE TABLE AS SELECT……语句把计算结果存入inbound_age表,计算步骤有些复杂,因此使用WITH子语定义了三个中间表。最后的主查询设置了并行数8VM24CPU,测试时要把并行数改为4

不存结果

用三个月的入库数据算出库龄后,结果集t_result4500万条,这个大结果集写回硬盘会比较耗时,为了测出回写大结果集的时间,改写SQL,最后一行只对t_result计数,这样几乎就没有回写硬盘的动作了。

……

SELECT /*+ PARALLEL (t_result,8) */ COUNT(*) cnt FROM t_result

全量数据计算

全量入库记录数据是一整年的,共2亿条,上面测试时选择近3个月的,大概也就是选出5000万条进行计算,算出的结果集条数为4500万。

把计算的日期范围调整成全年(2015-10-01 ~ 2016-09-30),就能让全部的2亿条都参与计算,可以算出7700万条结果。修改SQL中的日期相关参数:

……

WHERE d.indate>TO_DATE('2015-10-01','YYYY-MM-DD')AND d.indate<=TO_DATE('2016-09-30','YYYY-MM-DD')

……

ELSE 365 END AS age

……

表示库龄为1年及以上。

5、Starrocks SQL测试

SQL

CREATE TABLE inbound_age AS

    WITH t1 AS (

        SELECT d.store,d.product,

            d.indate,

            d.num,

            SUM(d.num) OVER (PARTITION BY d.store,d.product ORDER BY d.indate DESC) AS accu_num,

            ROW_NUMBER()OVER(PARTITION BY d.store,d.product ORDER BY d.indate DESC) rn,

            c.total

        FROM inbound d JOIN store c ON d.store = c.store AND d.product = c.product

        WHERE d.indate> '2016-07-01' AND d.indate<= '2016-09-30'

    ),

    t11 AS (

        SELECT t1.* FROM

            t1 JOIN (

                SELECT store,product,MAX(rn) max_rn

                FROM t1 WHERE accu_num - num<total GROUP BY store,product

            )t2

            ON t1.store = t2.store AND t1.product = t2.product AND t1.rn = t2.max_rn

    ),

    t_result AS (

        SELECT t.*,

            CASE WHEN over90=0

                THEN DATEDIFF('2016-09-30', indate)

                ELSE 91 END AS age

        FROM (

            SELECT store,product, indate,

                total-accu_num AS num,1 AS over90

            FROM t11 WHERE total>accu_num

            UNION

            SELECT store,product,indate,

                CASE WHEN total<accu_num

                THEN num-(accu_num-total) ELSE num END AS num,

                0 AS over90

            FROM t1 WHERE accu_num - num<total

        ) t ORDER BY store,product,indate DESC

    )

    SELECT * FROM t_result

Starrocks不用设置并行数,它会自动控制。

不存结果

Oracle一样,改写SQL最后一行,改为计数即可:

……

SELECT COUNT(*) cnt FROM t_result

全量数据计算

改变SQL中日期相关的参数:

……

WHERE d.indate> '2015-10-01'AND d.indate<='2016-09-30'

……

ELSE 365 END AS age

……

表示库龄为1年及以上。

6、SPL测试

SPL脚本


A

B

1

=now()


2

=date(2016,7,1)


3

=date(2016,9,30)


4

=file("e:/store.ctx").open().cursor@m(;;8)


5

=file("e:/inbound.ctx").open().cursor(;indate>A2&&indate<=A3;A4)


6

=A5.joinx@m(store:product,A4:store:product,total,0:age, 0:accu)


7

=A6.group@o(store,product).(~.rvs())


8

=A7.(~.run(cum(num;store,product):accu,num-if(accu>total,(accu-total),0):num))

9

=A8.(last=~.m(-1),~=if(last.accu>=last.total,~.select(num>0),~.derive@o().insert(0,last.store,last.product,A2,last.total-last.accu,last.total,0)))

10

=A9.(~.run(interval(indate,A3):age))


11

=A8.conj().new(store,product,indate,num,age)


12

fork A11

=file("e:/age_"/#A12/".btx").export@b(A12)

13

=interval@ms(A1,now())


两表数据按照主键storeproduct有序存储在组表文件中,A4/A5从组表文件以游标方式加载数据,一边加载,一边进行后续的计算,A4中指定并行度为8(VM2CPU4核,并行度改为4)A5游标用过滤条件只加载待计算的三个月内的数据,并且跟随A4游标同步分段。

A6对两表进行有序归并关联,把主表库存量字段并入入库表。

A7形成计算库龄的小组,并且反转次序,按照入库日期降序;

A8在组内计算出累加量、仍然在库的有效数量;

A9根据累加量与库存量的比对,去掉多余的入库记录、增补90天以上的入库记录(入库日期设置为2016-07-01,计算出库龄为91天,代表90天以上的)

A10计算出库龄;

A11把各小组的小结果集conj()new()出最终的大结果集;

12行用并行代码块fork,把并行的多路游标存出为多个集文件,这些结果集文件仍然会保持有序。12行以前的计算操作都是定义在游标上,并不执行计算,第12行输出结果时才触发一系列的计算。

不存结果

OracleStarrocks一样,不输出明细数据,只计算结果条数,去掉12行的fork代码块(A12/B12),修改A12,用skip()对结果游标计数:

12

=A11.skip()

全量数据计算

修改A2的起始日期:

2

=date(2015,10,1)

7、测试结果

单位:秒


VM1

VM2

SPL

Oracle

Starrocks

SPL

Oracle

Starrocks

90天数据

存结果

10

181

92

22

252

溢出

只计数

9

92

63

15

167

溢出

全量数据

存结果

30

451

溢出

48

800

溢出

只计数

23

295

溢出

43

601

溢出

8、总结对比

1、 SPL可以采用更高性能的算法,避免关联,而SQL只能使用低效的JOIN运算,性能严重依赖于优化器,对些本例的复杂运算,优化器通常不起作用。同类测试,SPL能比Oracle SQL10倍以上。

2、 结果集比较大,向硬盘回写结果都明显多耗费一些时间,SPL写出集文件比较快,对性能影响比较小;OracleStarrocks性能降低明显。

3、 Starrocks作为专业的OLAP数据库,使用了CPU优化技术,计算性能要好于Oracle,但仍然要使用低效算法,计算性能和SPL相比仍然有相当大的差距。而且,CPU优化技术严重依赖内存计算,在较小内存或较大数据量时无法正常执行。