性能优化案例课程 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 |
英文版