多维分析后台实践 7:布尔维度和二值维度

【摘要】
用实例、分步骤,详细讲解多维分析(OLAP)的实现。点击了解多维分析后台实践 7:布尔维度和二值维度

实践目标

本期目标,是在完成前几期优化的基础上(特别是上期的分支机构维度序号化),实现布尔维度和二值维度,进一步提升计算速度。

实践的步骤:

1、 准备客户表:修改前几期的代码,将客户表分支机构、职业类型转化为布尔维度,将八个标志位转化为二值维度。

2、 访问客户表:修改查询代码,提高计算速度。

验证布尔维的多维分析计算需求,用存储在 customerEnum.ctx 中的客户数据,如下图:

..

city 字段存储客户居住城市,取值范围是 1 到 660。

计算需求用下面 Oracle 的 SQL1 表示:

select department_id,job_id,to_char(begin_date,'yyyymm') begin_month ,sum(balance) sum,count(customer_id) count

from customer

where city in (36,37,38,39,40,60,61,62,63,64,65,66,67,68,69,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660)

group by department_id,job_id,to_char(begin_date,'yyyymm') begin_month

验证二值维度的多维分析计算需求,用 SQL2 表示:

select department_id,job_id,to_char(begin_date,'yyyymm') begin_month ,sum(balance) sum,count(customer_id) count

from customer

where flag1=1 and flag2=1 and flag3=1 and flag5=1 and flag6=1 and flag8=1

group by department_id,job_id,to_char(begin_date,'yyyymm') begin_month

准备数据

布尔维度直接采用 customerEnum.ctx 存储方式即可。

二值维度要将 flag1 到 flag8 这 8 个标志位转换为一个整数的 8 个 bit 位来表示。续写 etl.dfx,将上期的 customerDept.ctx 转化后,生成组表文件 customerFlag.ctx。代码示例如下:


A

1

=file("data/customerDept.ctx").open().cursor()  

2

=A1.new(department_id,job_num,employee_id,begin_date,customer_id,first_name,last_name,phone_number,job_title,balance,department_name,vip_id,credit_grade,bits(flag8,flag7,flag6,flag5,flag4,flag3,flag2,flag1):flag)

3

=file("data/customerFlag.ctx").create@y(#department_id,#job_num,#employee_id,#begin_date,#customer_id,first_name,last_name,phone_number,job_title,balance,department_name,vip_id,credit_grade,flag)  

4

=A3.append(A2)

A1:打开 customerDept.ctx 组表文件,建立游标。

A2:生成新游标,用 bits 函数,将原有 8 个字段用一个整数的 8 个 bit 位来表示。例如:flag1 到 flag8 为 1,0,1,1,0,1,1,1,转化为十进制整数是 237。用二进制表示就是 1110 1101,8 个 bit 位从右到左依次对应 flag1 到 flag8。

 

数据量为一亿,导出组表文件和前期的组表文件比较如下:

期数

文件大小

说明

备注

第六期

2.6GB

未做二值维度优化


第七期

2.4GB

完成二值维度优化


从上表可以看出,完成数据类型优化之后,文件大小减少了0.2GB。二值维度的个数越多,优化之后文件变小的越显著。文件变小,能减少磁盘读取数据量,一定程度的提高性能。列存文件,计算时需要读取的字段数量减少,对提升性能帮助较大。

多维分析计算

一、布尔维度

SPL 代码由 olap.dfx 和 customerEnum.dfx 组成。前者是调用的入口,传入参数是 arg_table、arg_json,后者用来解析 arg_json。

arg_table 的值为 customerEnum。

arg_json 的值为:

{

       aggregate:

              [

                     {func:"sum",field:"balance",alias:"sum"},

                 {func:"count",field:"customer_id",alias:"count"}

              ],

       group:

              ["department_id","job_id","begin_yearmonth"],

       slice:

              [

                     {dim:"city",

                     value:[36,37,38,39,40,60,61,62,63,64,65,66,67,68,69,640,641,642,

                      643,644,645,646,647,648,649,650,651,652,653,654,655,656,

                            657,658,659,660]

                     }

               ]

}

 

customerEnum.dfx 代码如下:


A

B

C

1

func



2


if A1.bool!=null

return   string(A1.bool)/"("/A1.dim/")"

3


else if   A1.value==null

return "between("/A1.dim/","/A1.interval(1)/":"/A1.interval(2)/")"

4


else if ifa(A1.value)

return   string(A1.value)/".contain("/A1.dim/")"

5


else if   ifstring(A1.value)

return   A1.dim/"==\""/A1.value/"\""

6


else

return   A1.dim/"=="/A1.value

7

=json(arg_json)


=date("2000-01-01")

8

=A7.aggregate.(~.func/"("/~.field/"):"/~.alias).concat@c()

9

=A7.group.(if(~=="job_id","job_num",~))

10

=A9.(if(~=="begin_yearmonth","begin_date\\100:begin_yearmonth",~)).concat@c()

11

=A7.aggregate.(field)


=A9.(if(~=="begin_yearmonth","begin_date",~))

12

=(A11|C11).id().concat@c()


13

=[]



14

for   A7.slice.derive(null:bool)

if A14.dim=="begin_date"   && A14.value!=null

>A14.value=int(interval@m(C7,eval(A14.value))*100+day(eval(A14.value)))

15


else if   A14.dim=="begin_date" && A14.value==null

=A14.interval.(~=int(interval@m(C7,eval(~))*100+day(eval(~))))

16


else if A14.dim=="job_id"

>A14.dim="job_num"

17



>A14.value=A14.value.(job.pos@b(~))

18


else if   A14.dim=="city"

=to(660)

19



=C18.(A14.value.contain(#))

20



=A14.bool=C19

21


else if like(A14.dim,   "flag?")

>A14.value=int(A14.value)

22


=[func(A1,A14)]

>A13|=B22

23

=A7.group|A7.aggregate.(alias)

=A23(A23.pos("job_id"))="job(job_num):job_id"

24

=A23(A23.pos("begin_yearmonth"))="month@y(elapse@m(date(\""/C7/"\"),begin_yearmonth)):begin_yearmonth"

25

=A23(A23.pos("begin_date"))="elapse@m(B4,begin_date\\100)+(begin_date%100-1):begin_date"

26

return   A12,A8,A10,A13.concat("&&"),A23.concat@c()

A1 到 C6 是子程序,在调用的时候才会执行。为了方便说明,我们按照执行的顺序来讲解。

A7:将 arg_json 解析成序表。解析的结果是多层嵌套的序表,如下图:

..

其中的 aggregate 为:

..

其中的 group 为:

..

其中的 slice 为:

..

C7:定义起点日期 2000-01-01,用于参数和结果中的日期值转换。

A8:先将 aggregate 计算成冒号相连的字符串序列,如下图:

..

再将序列用逗号连接成一个字符串:sum(balance):sum,count(customer_id):count,也就是聚合表达式。

A9:将 group 中的 job_id 替换为 job_num。

A10:将 A8 中的 begin_yearmonth 替换成表达式 begin_date\100:begin_yearmonth。再把 A8 成员用逗号连接成一个字符串 department_id,job_num,begin_date\100:begin_yearmonth,也就是分组表达式。

A11:取得 aggregate 中的 field 字段,也就是聚合表达式用到的所有字段名。

C11:将 group 中的 begin_yearmonth 替换成 begin_date,结果就是分组表达式所用到的所有字段名。

A12:将 A10、C10 合并后,用逗号连接成字符串,求得本次计算所需要的所有字段名:balance,begin_date,customer_id,department_id,job_num。

A13:定义一个空序列,准备存放切片(过滤条件)表达式序列。

A14:A7.slice 增加一个 bool 字段后循环计算,循环体是 B14 到 C22。其中:B14 到 C21 是对 slice 的 value 或者 interval 做性能优化的转换。

B14:如果 A14(当前 slice)的 dim 是 begin_date,并且 value 不为空,也就是 begin_date 等于指定日期的情况,例如:begin_date==date("2010-11-01")。此时 C14 计算出 date("2010-11-01") 的转换后的整数值,赋值给 A14 的 value。

B15:如果 A14 的 dim 是 begin_date,并且 value 为空,也就是 begin_date 在两个日期之间的情况,例如:begin_date 在 date("2002-01-01") 和 date("2020-12-31") 之间。此时 C15 计算出两个日期的转换后整数值,赋值给 A13 的 interval 的两个成员。

B16:如果 A14 的 dim 是 job_id,也就是 job_id 取枚举值的情况。例如:["AD_VP","FI_MGR","AC_MGR","SA_MAN","SA_REP"].contain(job_id)。此时 C16 要将 A14 的 dim 改为 job_num。C17 要将 A14 的 value 枚举值转换为在全局变量 job 序列中的位置,也就是 job_num 整数序列,例如:[5,7,2,15,16]。

B18:如果 A14 的 dim 是 city,C18 生成一个长度为 city 维度个数的序列,C19 利用 A14 的 value(city 枚举值)将 C19 的相应成员赋值为 true,其他为 false。C19 就是布尔维序列,计算结果如下:

..

C20:将 C19 赋值给 A14 的 bool 字段。

B21:如果 A14 的 dim 是 flag1、flag2…flag8,也就是标志位等于 "1" 或者 "0" 的情况。此时 C21 要将 A14 的 value 值从字符串转化为整数。

B22:用 B14 到 C21 对 slice 的 bool、value 或者 interval 做性能优化的转换的结果作为参数,调用子程序 A1。

子程序 A1(B2 到 B6),和第二篇的 func 代码基本相同,只是增加了 B2、C2。B2 判断 bool 字段是否为空,如果不为空,那么 C2 就生成并返回布尔维度表达式。例如:[false,true,false…](city),作用是:用 city 字段的值作为序号,在布尔维序列中找对应位置的成员,如果是 true 就是符合枚举条件的记录,否则就是不符合枚举条件的记录。

 

C22:func A1 的返回结果追加到 A13 中。继续 A14 中的循环,到循环结束,就准备好了切片表达式的序列。

A23:从这里开始准备结果集显示值转换的表达式。将 A6.group 和 A6.aggregate.alias 序列合并,如下图:

..

C23:将 A23 中的 job_id 替换成转换语句。语句的作用是:将结果集中的 job_num 转换为 job_id。

A24:将 A23 中的 begin_yearmonth,替换为转换语句,作用是:将分组字段中的整数值 begin_yearmonth 从整数转化为 yyyymm。

A25:将 A23 中的 begin_date,替换为转换语句,作用是:将分组字段中的整数化日期值转换为日期型。此时 A23 就是准备好的结果集显示值转换表达式:

..

A26:返回 A12,A8,A10,A13.concat("&&"),A23.concat@c(),依次是:

本次计算用到的字段名:balance,begin_date,customer_id,department_id,job_num

聚合表达式:sum(balance):sum,count(customer_id):count

分组表达式:department_id,job_num,begin_date\100:begin_yearmonth

切片表达式:

[false,false,false,false,false,false,false,false,false,false,… false,false,true,true,true,true,

true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true](city)

结果集显示值转换表达式:

department_id,job(job_num):job_id,month@y(elapse@m(date("2000-01-01"),begin_yearmonth)):begin_yearmonth,sum,count

 

布尔维优化对 olap.dfx 没有特殊的要求,代码如下:


A

1

=call(arg_table/".dfx",arg_json)

2

=file("data/"/arg_table/".ctx").open()

3

=A2.cursor@m(${A1(1)};${A1(4)};2)

4

=A3.groups(${A1(3)};${A1(2)})

5

=A4.new(${A1(5)})

6

return A5

A3:实际执行的是布尔维优化之后的代码:

=A2.cursor@m(balance,begin_date,customer_id,department_id,job_num; [false,false,false,false,false,false,false,false,false,false,… false,false,true,true,true,true,

true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true](city)

;2) 作用是用 city 字段的值作为序号,在布尔维序列中找对应位置的成员,如果是 true 就是符合枚举条件的记录,否则就是不符合枚举条件的记录。

 

用来调用 SPL 代码的 Java 代码和上期相比没有变化,只要调整调用参数即可。

Java 代码加上后台计算返回结果总的执行时间如下:

计算方法

单线程

二线程并行

备注

无布尔维优化

11

6


布尔维优化

6 秒

4 秒


通过上表的对比可以看出,布尔维优化可以提高计算性能。

 

二、二值维度

SPL 代码由 olap.dfx 和 customerFlag.dfx 组成。前者是调用的入口,传入参数是 arg_table、arg_json,后者用来解析 arg_json。

arg_table 的值为 customerFlag。

arg_json 的值为:

{

       aggregate:

              [

                     {func:"sum",field:"balance",alias:"sum"},

                 {func:"count",field:"customer_id",alias:"count"}

              ],

       group:

              ["department_id","job_id","begin_yearmonth"],

       slice:

              [

                     {dim:"flag1",value:"1"},

                     {dim:"flag2",value:"1"},

                     {dim:"flag3",value:"1"},

                     {dim:"flag5",value:"1"},

                     {dim:"flag6",value:"1"},

                     {dim:"flag8",value:"1"}

               ]

}

 

customerFlag.dfx 代码如下:


A

B

C

1

func



2


if A1.bool!=null

return   string(A1.bool)/"("/A1.dim/")"

3


else if   A1.value==null

return   "between("/A1.dim/","/A1.interval(1)/":"/A1.interval(2)/")"

4


else if ifa(A1.value)

return string(A1.value)/".contain("/A1.dim/")"

5


else if   ifstring(A1.value)

return   A1.dim/"==\""/A1.value/"\""

6


else

return   A1.dim/"=="/A1.value

7

=json(arg_json)


=date("2000-01-01")

8

=A7.aggregate.(~.func/"("/~.field/"):"/~.alias).concat@c()

9

=A7.group.(if(~=="job_id","job_num",~))

10

=A9.(if(~=="begin_yearmonth","begin_date\\100:begin_yearmonth",~)).concat@c()

11

=A7.aggregate.(field)


=A9.(if(~=="begin_yearmonth","begin_date",~))

12

=(A11|C11).id().concat@c()


13

=A7.slice.select(like(dim,"flag?"))

=A13.derive(int(right(dim,1)):num)

=to(8).("0")

14

=B13.(C13(num)=value)

=bits(C13.rvs())

="and(flag,"/B14/")=="/B14

15

=[]

=A7.slice\A13


16

for B15

if   A16.dim=="begin_date" && A16.value!=null

>A16.value=int(interval@m(C7,eval(A16.value))*100+day(eval(A16.value)))

17


else if   A16.dim=="begin_date" && A16.value==null

=A16.interval.(~=int(interval@m(C7,eval(~))*100+day(eval(~))))

18


else if   A16.dim=="job_id"

>A16.dim="job_num"

19



=A16.value.(job.pos@b(~))

20


=[func(A1,A16)]

>A15|=B20

21

=A7.group|A7.aggregate.(alias)


=A21(A21.pos("job_id"))="job(job_num):job_id"

22

=A21(A21.pos("begin_yearmonth"))="month@y(elapse@m(date(\""/C7/"\"),begin_yearmonth)):begin_yearmonth"

23

=A21(A21.pos("begin_date"))="elapse@m(B4,begin_date\\100)+(begin_date%100-1):begin_date"

24

return   A12,A8,A10,(A15|C14).concat("&&"),A21.concat@c()

A1 到 A12 与 customerEnum.dfx 相比没有变化。

A13:将切片过滤条件 A7.slice 中 flag 开头的 dim 过滤出来,如下图:

..

B13:A13 增加一列 num,取 dim 的最后一个数字,转为整数:

..

C13:创建一个长度为 8 的序列,每一个成员都是字符串 "0"。

A14:对 B13 循环计算,将 C13 对应位置设置成 value 的值 "1":

..

B14:将 C13 成员顺序反转,再用 bits 函数计算对应的整数值:237,写成二进制数就是:1011 0111。从右向左对应原有的 flag1-flag8 的值。

C14:用 B14 做字符串拼接,生成按位与计算的表达式:and(flag,237)==237。后续将作为过滤表达式的一部分。

A15:定义一个空序列,准备存放切片(过滤条件)表达式序列。

B15:A7.slice 中去掉 A13,得到 flag1-flag8 之外的切片条件。

A16:对 B15 开始循环,循环中处理 flag1-flag8 之外的切片条件。

A16-A23:代码和 customerEnum.dfx 没有差别。

A24:返回 A12,A8,A10,A13.concat("&&"),A23.concat@c(),依次是:

本次计算用到的字段名:balance,begin_date,customer_id,department_id,job_num

聚合表达式:sum(balance):sum,count(customer_id):count

分组表达式:department_id,job_num,begin_date\100:begin_yearmonth

切片表达式:and(flag,237)==237

结果集显示值转换表达式:

department_id,job(job_num):job_id,month@y(elapse@m(date("2000-01-01"),begin_yearmonth)):begin_yearmonth,sum,count

 

二值维优化对 olap.dfx 也没有特殊的要求,代码如下:


A

1

=call(arg_table/".dfx",arg_json)

2

=file("data/"/arg_table/".ctx").open()

3

=A2.cursor@m(${A1(1)};${A1(4)};2)

4

=A3.groups(${A1(3)};${A1(2)})

5

=A4.new(${A1(5)})

6

return A5

A3:实际执行的是二值维优化之后的代码:

=A2.cursor@m(balance,begin_date,customer_id,department_id,job_num;and(flag,237)==237;2) 作用是用 flag 字段的值与 237 按位与,如果结果是 237 就是符合枚举条件的记录,否则就是不符合二值维条件的记录。

 

用来调用 SPL 代码的 Java 代码和上期相比没有变化,只要调整调用参数即可。

Java 代码加上后台计算返回结果总的执行时间如下:

计算方法

单线程

二线程并行

备注

无二值维优化

16

9


二值维优化

7 秒

4 秒


通过上表的对比可以看出,二值维优化也可以提高计算性能。