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