性能优化案例课程 TPCH-Q9
select
nation,
o_year,
sum(amount) as sum_profit
from
(
select
n_name as nation,
extract(year from o_orderdate) as o_year,
l_extendedprice * (1 - l_discount) - ps_supplycost * l_quantity as amount
from
part,
supplier,
lineitem,
partsupp,
orders,
nation
where
s_suppkey = l_suppkey
and ps_suppkey = l_suppkey
and ps_partkey = l_partkey
and p_partkey = l_partkey
and o_orderkey = l_orderkey
and s_nationkey = n_nationkey
and p_name like '%chocolate%'
) profit
group by
nation,
o_year
order by
nation,
o_year desc;
这是多表关联过滤后的分组聚合运算,其中包括主子表 orders 和 lineitem 关联以及与双主键外键表 partsupp 的关联。
1. 数据存储
大表 orders、lineitem 按照主键 orderkey 有序存储,连接的时候可以有序归并。其他表没有特殊的排序要求,就按照主键有序存储。
这样,就可以继续使用题目 Q3 中的 orders.ctx、lineitem.ctx、customer.ctx。
part、supplier、partsupp 表直接使用题目 Q2 的 part.ctx、supplier.ctx、partsupp.ctx。
region、nation 表直接使用题目 Q2 的 regin.btx,nation.btx。
将这些表复制到本题的主目录中。
2. 一般实现
A |
|
1 |
=now() |
2 |
>name="chocolate" |
3 |
=file("nation.btx").import@b().keys@i(N_NATIONKEY) |
4 |
=file("part.ctx").open().cursor@m(P_PARTKEY;pos(P_NAME, name)).fetch().keys@im(P_PARTKEY) |
5 |
=file("supplier.ctx").open().cursor@m(S_SUPPKEY,S_NATIONKEY).fetch().keys@im(S_SUPPKEY) |
6 |
=file("partsupp.ctx").open().cursor@m(PS_PARTKEY,PS_SUPPKEY,PS_SUPPLYCOST;A4.find(PS_PARTKEY)).fetch().keys@im(PS_PARTKEY,PS_SUPPKEY) |
7 |
=file("lineitem.ctx").open().cursor@m(L_ORDERKEY,L_PARTKEY,L_SUPPKEY,L_QUANTITY,L_EXTENDEDPRICE,L_DISCOUNT;A4.find(L_PARTKEY)) |
8 |
=file("orders.ctx").open().new@r(A7,L_PARTKEY,L_SUPPKEY,L_QUANTITY,L_EXTENDEDPRICE,L_DISCOUNT,O_ORDERDATE) |
9 |
=A8.join@i(L_PARTKEY:L_SUPPKEY,A6,PS_SUPPLYCOST).switch(L_SUPPKEY,A5) |
10 |
=A9.groups(L_SUPPKEY.S_NATIONKEY:nationName,year(O_ORDERDATE):o_year;sum(L_EXTENDEDPRICE*(1-L_DISCOUNT)-PS_SUPPLYCOST*L_QUANTITY):profit) |
11 |
=A10.run(nationName=A3.find(nationName).N_NAME).sort(nationName,-o_year) |
12 |
=interval@ms(A1,now()) |
本例中 orders 无过滤,而 lineitem 有过滤,过滤后的数据行数远小于 orders 的行数,所以在主子表关联时与前面的例子不同。此时在 A7 先构造 lineitem 游标,A8 中用它去过滤主表 orders,从而大大减少读取的数据行数,提高运行效率。
A9 中用 join@i 方法把外键表的字段 PS_SUPPLYCOST 拼到游标上,这里是个双字段主键的外键表,不能再使用在游标建立时匹配并转换的方法。
A10 用 NATIONKEY 代替字符串 NAME 分组,A11 分组结果再转换成字符串。
测试项目 |
执行时间(秒) |
一般实现 |
37 |
3. 数据变换
数据变换要采用前面题目提到的维表主键序号化、日期整数化手段。
region、nation、supplier、part 表,可以直接使用以前的题目 Q2 转换的 region_2.btx,nation_2.btx、supplier_2.ctx、part_2.ctx。
part 表中的枚举型字符串字段 type 要完成数字化,可以用 Q2 的 p_type.btx。
customer 使用 Q5 中的 customer_3.ctx。
orders 表使用 Q3 中的 orders_5.ctx。
lineitem 表使用 Q8 中的 lineitem_4.ctx。
将这些表复制到本题的主目录中。
计算代码:
A |
B |
|
1 |
=now() |
|
2 |
>name="chocolate" |
|
3 |
=file("nation_2.btx").import@b() |
|
4 |
= file("part_2.ctx").open() |
=A4.cursor@m().skip().(false) |
5 |
=A4.cursor@m(P_PARTKEY;pos(P_NAME, name)).fetch().(B4(P_PARTKEY)=true) |
|
6 |
=file("supplier_2.ctx").open() |
|
7 |
=A6.cursor@m(S_NATIONKEY).fetch().(S_NATIONKEY) |
|
8 |
=file("partsupp_2.ctx").open().cursor@m(PS_PARTKEY,PS_SUPPKEY,PS_SUPPLYCOST;B4(PS_PARTKEY)).fetch().keys@im(PS_PARTKEY,PS_SUPPKEY) |
|
9 |
=file("lineitem_4.ctx").open().cursor@m(L_ORDERKEY,L_PARTKEY,L_SUPPKEY,L_QUANTITY,L_EXTENDEDPRICE,L_DISCOUNT;B4(L_PARTKEY)) |
|
10 |
=file("orders_5.ctx").open().new@r(A9,L_PARTKEY,L_SUPPKEY,L_QUANTITY,L_EXTENDEDPRICE,L_DISCOUNT,O_ORDERDATE) |
|
11 |
=A10.select(L_PARTKEY=A8.find(L_PARTKEY,L_SUPPKEY).PS_SUPPLYCOST).switch(L_SUPPKEY,A7:#) |
|
12 |
=A3.len()+1 |
|
13 |
=A11.groups(year(O_ORDERDATE)*A12+L_SUPPKEY:gk;sum(L_EXTENDEDPRICE*(1-L_DISCOUNT)-L_PARTKEY*L_QUANTITY):profit) |
|
14 |
=A13.new(A3(gk%A12).N_NAME:nationName,gk\A12:o_year,profit).sort(nationName,-o_year) |
|
15 |
=interval@ms(A1,now()) |
A11 中用 L_PARTKEY 和 L_SUPPKEY 在 A5 中按主键查找记录,找到后把 PS_SUPPLYCOST 赋值给 L_PARTKEY,如果没找到对应的记录则此值为 null,就会被 select 函数过滤掉。这个方法与前面介绍的 join@i 运算性能相当。
A13 中使用了 Q1 介绍的分组键技巧,把按 2 个字段分组变成按一个字段 gk 分组。
测试结果:
测试项目 |
执行时间(秒) |
一般实现 |
37 |
数据变换 |
29 |
4. 列式计算
A |
B |
|
1 |
=now() |
|
2 |
>name="chocolate" |
|
3 |
=file("nation_2.btx").import@b() |
|
4 |
=file("part_2.ctx").open() |
=A4.cursor@m().skip().(false) |
5 |
=A4.cursor@mv(P_PARTKEY;pos(P_NAME, name)).fetch().(B4(P_PARTKEY)=true) |
|
6 |
=file("supplier_2.ctx").open() |
|
7 |
=A6.cursor@m(S_NATIONKEY).fetch() |
|
8 |
=file("partsupp_2.ctx").open().cursor@mv(PS_PARTKEY,PS_SUPPKEY,PS_SUPPLYCOST;B4(PS_PARTKEY)).fetch().keys@im(PS_PARTKEY,PS_SUPPKEY) |
|
9 |
=file("lineitem_4.ctx").open().cursor@mv(L_ORDERKEY,L_PARTKEY,L_SUPPKEY,L_QUANTITY,L_EXTENDEDPRICE,L_DISCOUNT;B4(L_PARTKEY)) |
|
10 |
=file("orders_5.ctx").open().new@r(A9,L_PARTKEY,L_SUPPKEY,L_QUANTITY,L_EXTENDEDPRICE,L_DISCOUNT,O_ORDERDATE) |
|
11 |
=A10.join@i(L_PARTKEY:L_SUPPKEY,A8,PS_SUPPLYCOST;L_SUPPKEY,A7:#,S_NATIONKEY) |
|
12 |
=A3.len()+1 |
|
13 |
=A11.groups(year(O_ORDERDATE)*A12+S_NATIONKEY:gk;sum(L_EXTENDEDPRICE*(1-L_DISCOUNT)-PS_SUPPLYCOST*L_QUANTITY):profit) |
|
14 |
=A13.new(A3(gk%A12).N_NAME:nationName,gk\A12:o_year,profit).sort(nationName,-o_year) |
|
15 |
=interval@ms(A1,now()) |
|
=now() |
在列式计算中,要尽量避免 switch 操作,A11 中使用 join 拼接新列,而不再使用 switch 给原有列赋值。
测试结果:
测试项目 |
执行时间(秒) |
一般实现 |
37 |
数据变换 |
29 |
列式计算 |
12 |
英文版