性能优化案例课程 TPCH-Q1

select
    l_returnflag,
    l_linestatus,
    sum(l_quantity) as sum_qty,
    sum(l_extendedprice) as sum_base_price,
    sum(l_extendedprice * (1 - l_discount)) as sum_disc_price,
    sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) as sum_charge,
    avg(l_quantity) as avg_qty,
    avg(l_extendedprice) as avg_price,
    avg(l_discount) as avg_disc,
    count(*) as count_order
from
     lineitem
where
     l_shipdate <= date '1995-12-01' - interval '90' day(3)
group by
     l_returnflag,
     l_linestatus
order by
     l_returnflag,
     l_linestatus;

计算不涉及表间关联,针对 LINEITEM 表做条件过滤、分组汇总和结果排序。

1. 集文件

LINEITEM 是大表,需要外存,先尝试存成行式的集文件。数据转换代码:


A

1

=file("lineitem.tbl").cursor(;,"|").new(_1:L_ORDERKEY,_4:L_LINENUMBER,_2:L_PARTKEY,_3:L_SUPPKEY,_5:L_QUANTITY,_6:L_EXTENDEDPRICE,_7:L_DISCOUNT,_8:L_TAX,_9:L_RETURNFLAG,_10:L_LINESTATUS,_11:L_SHIPDATE,_12:L_COMMITDATE,_13:L_RECEIPTDATE,_14:L_SHIPINSTRUCT, _15:L_SHIPMODE, _16:L_COMMENT)

2

=file("lineitem.btx").export@b(A1)

字段 L_RETURNFLAG 和 L_LINESTATUS 都是长度为 1 的字符串,用来记录标志和状态,实际取值分别是 2 个和 3 个。

这是一个小结果集分组,用 groups 计算。计算代码:


A

1

=now()

2

1995-12-01

3

=A2-90

4

=file("lineitem.btx").cursor@b(L_SHIPDATE,L_QUANTITY, L_EXTENDEDPRICE,L_DISCOUNT,L_TAX,L_RETURNFLAG,L_LINESTATUS).select(L_SHIPDATE<=A3)

5

=A4.groups(L_RETURNFLAG,L_LINESTATUS;sum(L_QUANTITY):sum_qty,sum(L_EXTENDEDPRICE):sum_base_price, sum(L_EXTENDEDPRICE*(1-L_DISCOUNT)):sum_disc_price, sum(L_EXTENDEDPRICE*(1-L_DISCOUNT)*(1+L_TAX)):sum_charge,avg(L_QUANTITY):avg_qty,avg(L_EXTENDEDPRICE):avg_price,avg(L_DISCOUNT):avg_disc, count(1):count_order)

6

=interval@s(A1,now())

测试结果:

测试项目

执行时间(秒)

集文件

108

这里的测试目的是,对比不同手段的提速效果。具体的执行时间不重要,主要是观察应用不同的性能优化手段后,执行时间是否提升,以及提升的幅度。

2. 并行计算

并行计算用多线程同时遍历,提高性能:


A

1

=now()

2

1995-12-01

3

=A2-90

4

=file("lineitem.btx").cursor@mb(L_SHIPDATE,L_QUANTITY, L_EXTENDEDPRICE,L_DISCOUNT,L_TAX,L_RETURNFLAG,L_LINESTATUS).select(L_SHIPDATE<=A3)

5

=A4.groups(L_RETURNFLAG, L_LINESTATUS; sum(L_QUANTITY):sum_qty, sum(L_EXTENDEDPRICE):sum_base_price, sum(L_EXTENDEDPRICE*(1-L_DISCOUNT)):sum_disc_price, sum(L_EXTENDEDPRICE*(1-L_DISCOUNT)*(1+L_TAX)):sum_charge, avg(L_QUANTITY):avg_qty, avg(L_EXTENDEDPRICE):avg_price, avg(L_DISCOUNT):avg_disc, count(1):count_order)

6

=interval@s(A1,now())

A4 中 cursor 增加了 m 选项,表示并行。

测试用的机器是 4 核虚拟机,集算器设置了并行线程数是 4。

测试结果:

测试项目

执行时间(秒)

集文件

108

集文件并行

37

3. 列存组表

对于这种遍历计算,用组表列存方式,性能应好于集文件的行存方式。数据转换代码:


A

1

=file("lineitem.btx").cursor@b()

2

=file("lineitem3.ctx").create(L_ORDERKEY,L_LINENUMBER,L_PARTKEY, L_SUPPKEY, L_QUANTITY, L_EXTENDEDPRICE,L_DISCOUNT, L_TAX, L_RETURNFLAG, L_LINESTATUS, L_SHIPDATE, L_COMMITDATE, L_RECEIPTDATE,L_SHIPINSTRUCT, L_SHIPMODE, L_COMMENT)

3

>A2.append(A1)

组表文件命名为 lineitem3,表示是在第 3 节生成的,主要是为了区分后续生成的组表文件。

计算代码:


A

1

=now()

2

1995-12-01

3

=A2-90

4

=file("lineitem3.ctx").open().cursor@m(L_SHIPDATE,L_QUANTITY, L_EXTENDEDPRICE,L_DISCOUNT,L_TAX,L_RETURNFLAG,L_LINESTATUS;L_SHIPDATE<=A3)

5

=A4.groups(L_RETURNFLAG, L_LINESTATUS; sum(L_QUANTITY):sum_qty, sum(L_EXTENDEDPRICE):sum_base_price, sum(L_EXTENDEDPRICE*(1-L_DISCOUNT)):sum_disc_price, sum(L_EXTENDEDPRICE*(1-L_DISCOUNT)*(1+L_TAX)):sum_charge, avg(L_QUANTITY):avg_qty, avg(L_EXTENDEDPRICE):avg_price, avg(L_DISCOUNT):avg_disc, count(1):count_order)

6

=interval@s(A1,now())

A4 把过滤条件写在组表游标中,可以避免不满足条件的记录被生成,减少硬盘读入内存的数据量。

测试结果:

测试项目

执行时间(秒)

集文件

108

集文件并行

37

组表列存

15

4. 枚举型字符串字段数字化

字符串的运算相对于整数来说更费时,如果数据表行数很多,某字符串字段的不同取值较少,可以把这些不同取值构成一个列表,把字段值替换成串值在列表中对应的序号,从而变成整数型字段以提高运算的效率。

L_RETURNFLAG 和 L_LINESTATUS 是字符串字段,它们的取值个数分别为 3 个和 2 个,可以转换成整数字段。在 Q1 中,两个字段恰好是分组字段,实际上,不是分组字段的枚举型字符串字段也可以数字化。

数据转换代码:


A

1

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

2

=A1.cursor(L_RETURNFLAG, L_LINESTATUS).id(L_RETURNFLAG, L_LINESTATUS).(~.sort())

3

=file("l_returnflag.txt").export(A2(1))

4

=file("l_linestatus.txt").export(A2(2))

5

=A1.cursor().run(L_RETURNFLAG =A2(1).pos@b(L_RETURNFLAG), L_LINESTATUS =A2(2).pos@b(L_LINESTATUS))

6

=file("lineitem4.ctx").create(L_ORDERKEY,L_LINENUMBER,L_PARTKEY,L_SUPPKEY,L_QUANTITY,L_EXTENDEDPRICE,L_DISCOUNT,L_TAX,L_RETURNFLAG,L_LINESTATUS,L_SHIPDATE,L_COMMITDATE,L_RECEIPTDATE,L_SHIPINSTRUCT,L_SHIPMODE,L_COMMENT)

7

>A6.append(A5)

A2 计算两个字符串字段的取值列表并排序。

A5 用 pos@b 查找相应字符串在列表中的序号。

计算代码:


A

1

>l_linestatus=file("l_linestatus.txt").import@si()

2

>l_returnflag=file("l_returnflag.txt").import@si()

3

=now()

4

1995-12-01

5

=A4-90

6

=file("lineitem4.ctx").open().cursor@m(L_QUANTITY, L_EXTENDEDPRICE,L_DISCOUNT,L_TAX,L_RETURNFLAG,L_LINESTATUS;L_SHIPDATE<=A5)

7

=A6.groups(L_RETURNFLAG, L_LINESTATUS; sum(L_QUANTITY):sum_qty, sum(L_EXTENDEDPRICE):sum_base_price, sum(L_EXTENDEDPRICE*(1-L_DISCOUNT)):sum_disc_price, sum(L_EXTENDEDPRICE*(1-L_DISCOUNT)*(1+L_TAX)):sum_charge, avg(L_QUANTITY):avg_qty, avg(L_EXTENDEDPRICE):avg_price, avg(L_DISCOUNT):avg_disc, count(1):count_order)

8

=A7.run(L_RETURNFLAG=l_returnflag(L_RETURNFLAG),L_LINESTATUS=l_linestatus(L_LINESTATUS))

9

=interval@s(A3,now())

A8 把枚举型字符串字段重新转换回它们在列表中对应的字符串值。

测试结果:

测试项目

执行时间(秒)

集文件

108

集文件并行

37

组表列存

15

枚举型字符串字段数字化

12

5. 分组字段合并

分组过程中,分组字段的计算与对比是重要的计算量。多一个分组字段,在计算和比对时都会复杂很多,导致性能下降。也就是说,分组字段个数越少性能会越好。

我们发现,分组字段 L_ RETURNFLAG、L_LINESTATUS 的取值范围很小,可以合并计算出一个新的分组字段 gk,在 groups 函数中就可以把按 2 个字段分组变成按一个字段 gk 分组,可以加快分组计算的速度。

计算代码:


A

1

>l_linestatus=file("l_linestatus.txt").import@si()

2

>l_returnflag=file("l_returnflag.txt").import@si()

3

=now()

4

=l_linestatus.len()+1

5

1995-12-01

6

=A5-90

7

=file("lineitem4.ctx").open().cursor@m(L_QUANTITY, L_EXTENDEDPRICE,L_DISCOUNT,L_TAX,L_RETURNFLAG,L_LINESTATUS;L_SHIPDATE<=A6)

8

=A7.groups(L_RETURNFLAG*A4+L_LINESTATUS:gk ; sum(L_QUANTITY):sum_qty, sum(L_EXTENDEDPRICE):sum_base_price, sum(L_EXTENDEDPRICE*(1-L_DISCOUNT)):sum_disc_price,sum(L_EXTENDEDPRICE*(1-L_DISCOUNT)*(1+L_TAX)):sum_charge, avg(L_QUANTITY):avg_qty, avg(L_EXTENDEDPRICE):avg_price, avg(L_DISCOUNT):avg_disc, count(1):count_order)

9

=A8.run(L_RETURNFLAG =l_returnflag(gk\A4), L_LINESTATUS =l_linestatus(gk%A4))

10

=interval@s(A3,now())

A9 用 gk 反过来算出 L_ RETURNFLAG、L_LINESTATUS 的值,并换成它们在列表中对应的字符串值。

实际测试结果是这样的:

测试结果:

测试项目

执行时间(秒)

集文件

108

集文件并行

37

组表列存

15

枚举型字符串字段数字化

12

分组字段合并

11

6. 日期字段数字化

日期型数据的计算和比较也较为费时,把日期用 days@o 函数转换成小整数以提高性能。

L_SHIPDATE 字段是日期型数据,将它转换成一个小整数进行优化。


A

1

=file("lineitem4.ctx").open().cursor().run(L_SHIPDATE=days@o(L_SHIPDATE))

2

=file("lineitem6.ctx").create(L_ORDERKEY,L_LINENUMBER,L_PARTKEY,L_SUPPKEY,L_QUANTITY,L_EXTENDEDPRICE,L_DISCOUNT,L_TAX,L_RETURNFLAG,L_LINESTATUS,L_SHIPDATE,L_COMMITDATE,L_RECEIPTDATE,L_SHIPINSTRUCT,L_SHIPMODE,L_COMMENT)

3

>A2.append(A1)

A1 把 L_SHIPDATE 字段的值用 days@o 变换为小整数。

计算代码:


A

1

>l_linestatus=file("l_linestatus.txt").import@si()

2

>l_returnflag=file("l_returnflag.txt").import@si()

3

=now()

4

=l_linestatus.len()+1

5

1995-12-01

6

=days@o(A5-90)

7

=file("lineitem6.ctx").open().cursor@m(L_QUANTITY, L_EXTENDEDPRICE,L_DISCOUNT,L_TAX,L_RETURNFLAG,L_LINESTATUS;L_SHIPDATE<=A6)

8

=A7.groups(L_RETURNFLAG*A4+L_LINESTATUS:gk ; sum(L_QUANTITY):sum_qty, sum(L_EXTENDEDPRICE):sum_base_price, sum(L_EXTENDEDPRICE*(1-L_DISCOUNT)):sum_disc_price,sum(L_EXTENDEDPRICE*(1-L_DISCOUNT)*(1+L_TAX)):sum_charge, avg(L_QUANTITY):avg_qty, avg(L_EXTENDEDPRICE):avg_price, avg(L_DISCOUNT):avg_disc, count(1):count_order)

9

=A8.run(L_RETURNFLAG =l_returnflag(gk\A4), L_LINESTATUS =l_linestatus(gk%A4))

10

=interval@s(A3,now())

A6 用 days@o 把日期过滤条件转换为数字。

测试结果:

测试项目

执行时间(秒)

集文件

108

集文件并行

37

组表列存

15

枚举型字符串字段数字化

12

分组字段合并

11

日期数字化

11

日期数字化效果不明显,是因为这里只是日期比较,没有计算年月日这些动作。日期型和小整数的计算差距相对于其它计算来讲,耗时占比太小了,体现不出来效果。

7. 避免重复计算

打折价格 L_EXTENDEDPRICE*(1-L_DISCOUNT) 算了两次,可以先算出来赋给变量 dp,避免重复计算。

avg_qty、avg_price、avg_disc 这些平均值不要在 groups 中计算,而是在其后再计算,可以减少分组统计中的计算量。

修改后代码:


A

1

>l_linestatus=file("l_linestatus.txt").import@si()

2

>l_returnflag=file("l_returnflag.txt").import@si()

3

=now()

4

=l_linestatus.len()+1

5

1995-12-01

6

=days@o(A5-90)

7

=file("lineitem6.ctx").open().cursor@m(L_QUANTITY, L_EXTENDEDPRICE,L_DISCOUNT,L_TAX,L_RETURNFLAG,L_LINESTATUS;L_SHIPDATE<=A6)

8

=A7.groups(L_RETURNFLAG*A4+L_LINESTATUS:gk; sum(L_QUANTITY):sum_qty, sum(L_EXTENDEDPRICE):sum_base_price, sum(dp=L_EXTENDEDPRICE*(1-L_DISCOUNT)):sum_disc_price, sum(dp*L_TAX):sum_charge,sum(L_DISCOUNT):sum_disc, count(1):count_order)

9

=A8.new(l_returnflag(gk\A4):L_RETURNFLAG,l_linestatus(gk%A4):L_LINESTATUS,sum_qty,sum_base_price,sum_disc_price,sum_charge+sum_disc_price:sum_charge,sum_qty/count_order:avg_qty,sum_base_price/count_order:avg_price,sum_disc/count_order:avg_disc,count_order)

10

=interval@ms(A3,now())

A10 用 sum 和 count 的结果计算平均值,可以减少分组汇总计算量。

而且,分组完成后再把价税加起来 sum_charge+sum_disc_price:sum_charge,减少加法次数(分组后的记录数要远少于原表记录数)。

测试结果:

测试项目

执行时间(秒)

集文件

108

集文件并行

37

组表列存

15

枚举型字符串字段数字化

12

分组字段合并

11

日期数字化

11

避免重复计算

10

8. 列式计算

列式计算可以进一步提高性能,但是有些注意事项,在后面的代码中将会说明。

列式计算不需要做特殊的数据转换,继续使用 lineitem5.ctx。

计算代码:


A

1

>l_linestatus=file("l_linestatus.txt").import@si()

2

>l_returnflag=file("l_returnflag.txt").import@si()

3

=now()

4

=l_linestatus.len()+1

5

1995-12-01

6

=days@o(A5-90)

7

=file("lineitem5.ctx").open().cursor@mv(L_QUANTITY, L_EXTENDEDPRICE,L_DISCOUNT,L_TAX,L_RETURNFLAG,L_LINESTATUS;L_SHIPDATE<=A6)

8

=A7.derive@o(L_EXTENDEDPRICE*(1-L_DISCOUNT):dp,L_RETURNFLAG*A4+L_LINESTATUS:gk)

9

=A8.groups(gk;sum(L_QUANTITY):sum_qty, sum(L_EXTENDEDPRICE):sum_base_price,sum(dp):sum_disc_price,sum(dp*L_TAX):sum_charge, sum(L_DISCOUNT):sum_disc, count(1):count_order)

10

=A9.new(l_returnflag(gk\A4):L_RETURNFLAG,l_linestatus(gk%A4):L_LINESTATUS,sum_qty,sum_base_price,sum_disc_price,sum_disc_price+sum_charge:sum_charge,sum_qty/count_order:avg_qty,sum_base_price/count_order:avg_price,sum_disc/count_order:avg_disc,count_order)

11

=interval@s(A3,now())

A7 中选项 @v 表示使用列式游标。

列式游标要避免在计算过程中产生新的变量,要使用 derive 方法将新变量变成新的列,如 A8 中创建 dp 列,选项 @o 表示新列增加到原序表上,不产生新序表。

测试结果:

测试项目

执行时间(秒)

集文件

108

集文件并行

37

组表列存

15

枚举型字符串字段数字化

12

分组字段合并

11

日期数字化

11

避免重复计算

10

列式计算

4

9. 日期有序存储

过滤条件是日期范围,只考虑这一题的话,按照日期有序最有利于提高过滤计算性能。

数据转换代码:


A

1

=file("lineitem6.ctx").open().cursor(L_SHIPDATE,L_ORDERKEY,L_LINENUMBER,L_PARTKEY,L_SUPPKEY,L_QUANTITY,L_EXTENDEDPRICE,L_DISCOUNT,L_TAX,L_RETURNFLAG,L_LINESTATUS,L_COMMITDATE,L_RECEIPTDATE,L_SHIPINSTRUCT,L_SHIPMODE,L_COMMENT).sortx(L_SHIPDATE;1000000)

2

=file("lineitem9.ctx").create(#L_SHIPDATE,L_ORDERKEY,L_LINENUMBER,L_PARTKEY,L_SUPPKEY,L_QUANTITY,L_EXTENDEDPRICE,L_DISCOUNT,L_TAX,L_RETURNFLAG,L_LINESTATUS,L_COMMITDATE,L_RECEIPTDATE,L_SHIPINSTRUCT,L_SHIPMODE,L_COMMENT)

3

>A2.append(A1)

A2 定义组表的时候,第一个字段 L_SHIPDATE 前加 #号,表示这是有序的维字段。

计算时继续使用上一章的代码,把其中的组表名字改为 lineitem9.ctx 就可以了,其他不用修改。

SPL 引擎会自动利用组表的有序字段提高性能。

测试结果:

测试项目

执行时间(秒)

集文件

108

集文件并行

37

组表列存

15

枚举型字符串字段数字化

12

分组字段合并

11

日期数字化

11

避免重复计算

10

列式计算

4

日期有序存储

2