性能优化案例课程 TPCH-Q8
select
o_year,
sum(case
when nation = 'CHINA' then volume
else 0
end) / sum(volume) as mkt_share
from
(
select
extract(year from o_orderdate) as o_year,
l_extendedprice * (1 - l_discount) as volume,
n2.n_name as nation
from
part,
supplier,
lineitem,
orders,
customer,
nation n1,
nation n2,
region
where
p_partkey = l_partkey
and s_suppkey = l_suppkey
and l_orderkey = o_orderkey
and o_custkey = c_custkey
and c_nationkey = n1.n_nationkey
and n1.n_regionkey = r_regionkey
and r_name = 'ASIA'
and s_nationkey = n2.n_nationkey
and o_orderdate between date '1995-01-01' and date '1996-12-31'
and p_type = 'STANDARD POLISHED BRASS'
) all_nations
group by
o_year
order by
o_year;
这就是多表关联过滤后的分组聚合运算,其中包括主子表 orders 和 lineitem 关联。
1. 数据存储
大表 orders、lineitem 按照主键 orderkey 有序存储,连接的时候可以有序归并。其他表没有特殊的排序要求,就按照主键有序存储。
这样,就可以继续使用题目 Q3 中的 orders.ctx、lineitem.ctx、customer.ctx。
part、supplier 表直接使用题目 Q2 的 part.ctx、supplier.ctx。
region、nation 表直接使用题目 Q2 的 regin.btx,nation.btx。
将这些表复制到本题的主目录中。
2. 一般实现
A |
B |
|
1 |
=now() |
|
2 |
1995-01-01 |
1996-12-31 |
3 |
>nation="CHINA" |
>name="ASIA" |
4 |
>type="STANDARD POLISHED BRASS" |
|
5 |
=file("region.btx").import@b().select@1(R_NAME==name).R_REGIONKEY |
|
6 |
=file("nation.btx").import@b(N_NATIONKEY,N_NAME,N_REGIONKEY).keys@i(N_NATIONKEY) |
|
7 |
=A6.select@1(N_NAME==nation).N_NATIONKEY |
|
8 |
=A6.select(N_REGIONKEY==A5).derive@o().keys(N_NATIONKEY) |
|
9 |
=file("supplier.ctx").open().cursor(S_SUPPKEY,S_NATIONKEY;S_NATIONKEY:A6).fetch().keys@im(S_SUPPKEY) |
|
10 |
=file("part.ctx").open().cursor@m(P_PARTKEY;P_TYPE==type).fetch().keys@im(P_PARTKEY) |
|
11 |
=file("customer.ctx").open().cursor@m(C_CUSTKEY;A8.find(C_NATIONKEY)).fetch().keys@im(C_CUSTKEY) |
|
12 |
=file("orders.ctx").open().cursor@m(O_ORDERKEY,O_ORDERDATE;O_ORDERDATE>=A2 && O_ORDERDATE <=B2 && A11.find(O_CUSTKEY)) |
|
13 |
=file("lineitem.ctx").open().news(A12,L_ORDERKEY,L_SUPPKEY,L_EXTENDEDPRICE,L_DISCOUNT,O_ORDERDATE;A10.find(L_PARTKEY),L_SUPPKEY:A9) |
|
14 |
=A13.run(L_EXTENDEDPRICE*=(1-L_DISCOUNT)) |
|
15 |
=A14.groups(year(O_ORDERDATE):o_year;sum(if(L_SUPPKEY.S_NATIONKEY.N_NAME==nation,L_EXTENDEDPRICE,0)):s1,sum(L_EXTENDEDPRICE):s2) |
|
16 |
=A15.new(o_year,s1/s2:mkt_share) |
|
17 |
=interval@ms(A1,now()) |
这里使用了前面题目中说过的游标建立时过滤、主子表有序关联和将关联字段转换成外键表指针的技巧。
不过,A15 中的 L_SUPPKEY.S_NATIONKEY.N_NAME 并不是前面讲过,在 join 或者 where 中出现的维表过滤,而是出现在条件求和表达式中。这里的实现方式是 A9 中 supplier 表连接 nation 表,A13 中 lineitem 表连接 supplier 表,在 A15 中 sum(if) 的时候,外键字段作为条件出现。
测试项目 |
执行时间(秒) |
一般实现 |
18 |
3. 维表过滤用在计算式中
上一节,A15 中的 L_SUPPKEY.S_NATIONKEY.N_NAME 关联的表比较多,而 sum(case) 的本质是要根据 l_suppkey 是否符合条件,对 volume 做有条件求和。
对于这个计算需求,如果先过滤 supplier,得到的 s_suppkey 集合作为 l_suppkey 的条件,实现有条件的求和。这样可以避免 supplier 参与后面的关联计算。
用 SQL 写出来是这样的:
select
o_year,
sum(case
when l_suppkey in(
select s_suppkey
from supplier,nation
where
s_nationkey=n_nationkey
and n_name='CHINA'
)
then volume
else 0
end) / sum(volume) as mkt_share
from
(
select
extract(year from o_orderdate) as o_year,
l_extendedprice * (1 - l_discount) as volume,
l_suppkey
from
part,
lineitem,
orders,
customer,
nation n1,
region
where
p_partkey = l_partkey
and s_suppkey = l_suppkey
and l_orderkey = o_orderkey
and o_custkey = c_custkey
and c_nationkey = n1.n_nationkey
and n1.n_regionkey = r_regionkey
and r_name = 'ASIA'
and o_orderdate between date '1995-01-01' and date '1996-12-31'
and p_type = 'STANDARD POLISHED BRASS'
) all_nations
group by
o_year
order by
o_year;
改写后,sum(case) 的条件变成了一个 in 子句,其中的子查询是个有 join 和 where 的语句,SPL 中可以再用维表过滤的办法优化。这样,后面关联表中就可以没有 supplier 及其维表 nation(n2) 了。
计算代码:
A |
B |
|
1 |
=now() |
|
2 |
1995-01-01 |
1996-12-31 |
3 |
>nation="CHINA" |
>name="ASIA" |
4 |
>type="STANDARD POLISHED BRASS" |
|
5 |
=file("region.btx").import@b().select@1(R_NAME==name).R_REGIONKEY |
|
6 |
=file("nation.btx").import@b() |
|
7 |
=A6.select@1(N_NAME==nation).N_NATIONKEY |
|
8 |
=A6.select(N_REGIONKEY==A5).derive@o().keys(N_NATIONKEY) |
|
9 |
=file("supplier.ctx").open().cursor@m(S_SUPPKEY;S_NATIONKEY==A7).fetch().keys@im(S_SUPPKEY) |
|
10 |
=file("part.ctx").open().cursor@m(P_PARTKEY;P_TYPE==type).fetch().keys@im(P_PARTKEY) |
|
11 |
=file("customer.ctx").open().cursor@m(C_CUSTKEY;A8.find(C_NATIONKEY)).fetch().keys@im(C_CUSTKEY) |
|
12 |
=file("orders.ctx").open().cursor@m(O_ORDERKEY,O_ORDERDATE;O_ORDERDATE>=A2 && O_ORDERDATE <=B2 && A11.find(O_CUSTKEY)) |
|
13 |
=file("lineitem.ctx").open().news(A12,L_SUPPKEY,L_EXTENDEDPRICE,L_DISCOUNT,O_ORDERDATE;A10.find(L_PARTKEY)) |
|
14 |
=A13.run(L_EXTENDEDPRICE*=(1-L_DISCOUNT)) |
|
15 |
=A14.groups(year(O_ORDERDATE):o_year;sum(if(A9.find(L_SUPPKEY),L_EXTENDEDPRICE,0)):s1,sum(L_EXTENDEDPRICE):s2) |
|
16 |
=A15.new(o_year,s1/s2:mkt_share) |
|
17 |
=interval@ms(A1,now()) |
A15 采用了前面说的计算方法,用 L_SUPPKEY 查找 A9 的序表主键 S_SUPPKEY,作为求和的条件。这样,A9 中 supplier 不需要连接 nation,A13 中 lineitem 也不再关联 supplier 了。
测试结果:
测试项目 |
执行时间(秒) |
一般实现 |
18 |
维表过滤用在计算式中 |
16 |
4. 数据变换
数据变换要采用前面题目提到的维表主键序号化、日期整数化手段。
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。
将这些表复制到本题的主目录中。
数据转换代码:
A |
|
1 |
=file("part.ctx").open().import(P_PARTKEY).keys@im(P_PARTKEY) |
2 |
=file("lineitem.ctx").open().cursor() |
3 |
=A2.run(L_PARTKEY=A1.pfind(L_PARTKEY)) |
4 |
=file("lineitem_4.ctx").create@p(#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) |
5 |
>A4.append(A3) |
计算代码:
A |
B |
|
1 |
=now() |
|
2 |
=days@o(date("1995-01-01")) |
=days@o(date("1996-12-31")) |
3 |
>nationName="CHINA" |
>name="ASIA" |
4 |
=p_type=file("p_type.btx").import@b().(_1) |
|
5 |
=nation= file("nation_2.btx").import@b() |
|
6 |
>type=p_type.pos@b("STANDARD POLISHED BRASS") |
|
7 |
=file("region_2.btx").import@b().pselect(R_NAME==name) |
|
8 |
=nation.pselect(N_NAME==nationName) |
=nation.(N_REGIONKEY==A7) |
9 |
=file("supplier_2.ctx").open() |
=A9.cursor@m().skip().(false) |
10 |
=A9.cursor@m(S_SUPPKEY;S_NATIONKEY==A8).fetch().(B9(S_SUPPKEY)=true) |
|
11 |
=file("part_2.ctx").open() |
=A11.cursor@m().skip().(false) |
12 |
=A11.cursor@m(P_PARTKEY;P_TYPE==type).fetch().(B11(P_PARTKEY)=true) |
|
13 |
=file("customer_3.ctx").open() |
=A13.cursor@m().skip().(false) |
14 |
=A13.cursor@m(C_CUSTKEY;B8(C_NATIONKEY)).fetch().(B13(C_CUSTKEY)=true) |
|
15 |
=file("orders_5.ctx").open().cursor@m(O_ORDERKEY,O_ORDERDATE;O_ORDERDATE>=A2 && O_ORDERDATE <=B2 && B13(O_CUSTKEY)) |
|
16 |
=file("lineitem_4.ctx").open().news(A15,L_SUPPKEY,L_EXTENDEDPRICE,L_DISCOUNT,O_ORDERDATE;B11(L_PARTKEY)) |
|
17 |
=A16.run(L_EXTENDEDPRICE*=(1-L_DISCOUNT)) |
|
18 |
=A17.groups(year(O_ORDERDATE):o_year;sum(if(B9(L_SUPPKEY),L_EXTENDEDPRICE,0)):s1,sum(L_EXTENDEDPRICE):s2) |
|
19 |
=A18.new(o_year,s1/s2:mkt_share) |
|
20 |
=interval@ms(A1,now()) |
B8、B9、B11、B13 是对位序列。
A18 中采用对位序列 B9 来判断 L_SUPPKEY 是否符合求和条件,性能比前面的主键查找更好。
测试结果:
测试项目 |
执行时间(秒) |
一般实现 |
18 |
维表过滤用在计算式中 |
16 |
数据变换 |
10 |
5. 列式计算
A |
B |
|
1 |
=now() |
|
2 |
=days@o(date("1995-01-01")) |
=days@o(date("1996-12-31")) |
3 |
>nationName="CHINA" |
>name="ASIA" |
4 |
=p_type=file("p_type.btx").import@b().(_1) |
|
5 |
=nation= file("nation_2.btx").import@b() |
|
6 |
>type=p_type.pos@b("STANDARD POLISHED BRASS") |
|
7 |
=file("region_2.btx").import@b().pselect(R_NAME==name) |
|
8 |
=nation.pselect(N_NAME==nationName) |
=nation.(N_REGIONKEY==A7) |
9 |
=file("supplier_2.ctx").open() |
=A9.cursor@m().skip().(false) |
10 |
=A9.cursor@mv(S_SUPPKEY;S_NATIONKEY==A8).fetch().(B9(S_SUPPKEY)=true) |
|
11 |
=file("part_2.ctx").open() |
=A11.cursor@m().skip().(false) |
12 |
=A11.cursor@mv(P_PARTKEY;P_TYPE==type).fetch().(B11(P_PARTKEY)=true) |
|
13 |
=file("customer_3.ctx").open() |
=A13.cursor@m().skip().(false) |
14 |
=A13.cursor@mv(C_CUSTKEY;B8(C_NATIONKEY)).fetch().(B13(C_CUSTKEY)=true) |
|
15 |
=file("orders_5.ctx").open().cursor@mv(O_ORDERKEY,O_ORDERDATE;O_ORDERDATE>=A2 && O_ORDERDATE<=B2 && B13(O_CUSTKEY)) |
|
=file("lineitem_4.ctx").open().cursor@v(L_ORDERKEY,L_SUPPKEY,L_EXTENDEDPRICE,L_DISCOUNT;B11(L_PARTKEY);A15) |
||
16 |
=A16.joinx@im(L_ORDERKEY,A15:O_ORDERKEY,O_ORDERDATE) |
|
17 |
=A17.derive@o(L_EXTENDEDPRICE*(1-L_DISCOUNT):dp) |
|
18 |
=A18.groups(year(O_ORDERDATE):o_year;sum(if(B9(L_SUPPKEY),dp,0)):s1,sum(dp):s2) |
|
19 |
=A19.new(o_year,s1/s2:mkt_share) |
|
20 |
=interval@ms(A1,now()) |
|
21 |
=now() |
这里 orders 与 lineitem 主子关联时没有使用 news,而是同步游标后用 joinx@im 进行有序归并连接。当然继续使用 news 也是可以的,实测中用 joinx 要略快一点。
A16 中用 derive@o 新增一列 dp,避免在 A18 中会 2 次计算 dp。在列式计算中,要尽量避免赋值操作,A16 中使用 derive 定义新的计算列,而不再使用 run 给原有列赋值。
测试结果:
测试项目 |
执行时间(秒) |
一般实现 |
18 |
维表过滤用在计算式中 |
16 |
数据变换 |
10 |
列式计算 |
4 |
英文版
陈虎老师,为啥很少看到用新关联计算函数 (fjoin,pjoin) 来写 SPL 语句, 这个系列基本上都是用 new 和 news 来解决。除了书写上语句的简洁之外,效率上也会有明显优势吗?比如此文第 2 部分中”一般实现“时的 A13 格子,目前用 news 这样写无疑是简洁高效的,但 A13 也可以改成与 A12 同步的组表游标,然后用 pjoin 进行关联,pjoin 在 A13、A12 都是组表游标的前提下也能跳块提速,就是多了一步生成组表游标,把 A13 拆成两步,其余步骤都一样,我测了一下结果是一样的,效率也可喜:😄
这个是基础课程,不讲这些新方法,有兴趣的同学可以自己练。