性能优化案例课程 TPCH-Q20

select
    s_name,s_address
from
    supplier,nation
where
    s_suppkey in (
        select
            ps_suppkey
        from
            partsupp
        where
            ps_partkey in (
                select
                    p_partkey
                from
                    part
                where
                    p_name like 'bisque%'
            )
            and ps_availqty > (
                select
                    0.5 * sum(l_quantity)
                from
                    lineitem
                where
                    l_partkey = ps_partkey
                    and l_suppkey = ps_suppkey
                    and l_shipdate >= date '1995-01-01'
                    and l_shipdate < date '1995-01-01' + interval '1' year
            )
    )
    and s_nationkey = n_nationkey
    and n_name = 'CHINA'
order by
    s_name;

这个主查询很简单,但过滤条件很复杂,嵌套了好几层。

过滤条件主要是针对 supplier 表的 S_SUPPKEY 字段。这个表相对小,只要计算出符合条件的 SUPPKEY 集合,就容易得到 S_NAME,S_ADDRESS。可以分步骤来计算 SUPPKEY 集合,思路就清楚了。

原 SQL 是用 partsupp 过滤 supplier,调整为用 supplier 过滤 partsupp,得到的 PS_SUPPKEY 集合相同。partsupp 表较大,把 supplier 和 part 一起当成维表来过滤事实表 partsupp 更有利于性能。计算步骤为:

1. 按条件过滤 nation 表,结果再去过滤 supplier 表。

2. 按条件过滤 part 表。

3. 用 1 和 2 的结果当作外键表去匹配过滤 partsupp 表。

这三个步骤可以看做是一个视图 v1,写成 SQL 是这样的:

select
	ps_suppkey
from
	partsupp,
	(
		select
			p_partkey
		from
			part
		where
			p_name like 'bisque%'
	),
	(
		select
			s_suppkey
		from
			supplier,nation
		where
			s_nationkey = n_nationkey
			and n_name = 'CHINA'
	)
where
	ps_partkey=p_partkey
	and ps_suppkey=s_suppkey

有了 v1 后,原 SQL 变成这样:

select
    s_name,s_address
from
    supplier,
	(
	    select
            distinct ps_suppkey
        from
            v1
        where
            ps_availqty > (
                select
                    0.5 * sum(l_quantity)
                from
                    lineitem
                where
                    l_partkey = ps_partkey
                    and l_suppkey = ps_suppkey
                    and l_shipdate >= date '1995-01-01'
                    and l_shipdate < date '1995-01-01' + interval '1' year
            ) 
	)  q_suppkey
where s_suppkey=ps_suppkey;

观察子查询 q_suppkey,v1 和 lineitem 的计算属于子查询内用等值条件与主表关联的情况,可以用前面题目介绍的方法,转换成 v1 和 lineitem 的 JOIN 计算,转换后,这句 SQL 可以再变成这样:

select
    s_name,s_address
from
    supplier,
	(
		select
			distinct ps_suppkey
		from
			v1,
			(select 
				l_partkey,
				l_suppkey,
				0.5 * sum(l_quantity) quantity
			from 
				lineitem
			where
				l_shipdate >= date '1995-01-01'
				and l_shipdate < date '1995-01-01' + interval '1' year
			group by 
				l_partkey,
				l_suppkey
			) 
		where
			ps_partkey = l_partkey
			and ps_suppkey = l_suppkey
			and ps_availqty > quantity
	)  q_suppkey
where s_suppkey=ps_suppkey;

接下来,还要继续改造子查询 q_suppkey。lineitem 表很大,要先和 v1 连接,过滤掉不符合条件的记录,然后再分组汇总,最后过滤出 L_SUPPKEY 集合相同。SQL 再改写成:

select
    s_name,s_address
from
    supplier,
	(
		select
			distinct l_suppkey
		from
			(select 
				l_partkey,
				l_suppkey,
				ps_availqty,
				0.5 * sum(l_quantity) quantity
			from 
				lineitem,v1
			where
				ps_partkey = l_partkey
				and ps_suppkey = l_suppkey
				and l_shipdate >= date '1995-01-01'
				and l_shipdate < date '1995-01-01' + interval '1' year
			group by 
				l_partkey,
				l_suppkey,
				ps_availqty
			) 
		where
			ps_availqty > quantity
	) suppkey
where s_suppkey=l_suppkey;

1. 数据存储

参加计算的表都没有特殊的有序要求,按照主键有序存储。

继续使用题目 Q3 中的 lineitem.ctx 和 Q2 的 part.ctx、supplier.ctx、partsupp.ctx、nation.btx,复制到本题的主目录中。

2. 一般实现

计算代码:


A

1

=now()

2

1995-1-1

3

=elapse@y(A2,1)

4

>partname="bisque"

5

>nationname="CHINA"

6

=file("nation.btx").import@b().select@1(N_NAME==nationname).N_NATIONKEY

7

=file("part.ctx").open().cursor@m(P_PARTKEY;pos@h(P_NAME,partname)).fetch().keys@im(P_PARTKEY)

8

=file("supplier.ctx").open().cursor@m(S_SUPPKEY,S_NAME,S_ADDRESS;S_NATIONKEY==A6).fetch().keys@im(S_SUPPKEY)

9

=file("partsupp.ctx").open().cursor@m(PS_PARTKEY,PS_SUPPKEY,PS_AVAILQTY;A7.find(PS_PARTKEY),A8.find(PS_SUPPKEY)).fetch().keys@im(PS_PARTKEY,PS_SUPPKEY)

10

=file("lineitem.ctx").open().cursor@m(L_PARTKEY,L_SUPPKEY,L_QUANTITY;L_SHIPDATE>=A2 && L_SHIPDATE<A3)

11

=A10.join@i(L_PARTKEY:L_SUPPKEY,A9:PS_PARTKEY:PS_SUPPKEY,PS_AVAILQTY)

12

=A11.groups@u(L_PARTKEY,L_SUPPKEY;PS_AVAILQTY,sum(L_QUANTITY):quantity)

13

=A12.select(PS_AVAILQTY*2>quantity).id(L_SUPPKEY)

14

=A8.join@im(S_SUPPKEY,A13:~)

15

=A14.new(S_NAME,S_ADDRESS).sort(S_NAME)

16

=interval@ms(A1,now())

A9 相当于计算出了前面说的视图 v1。

A8 取出 supplier 表的 S_SUPPKEY 的同时,也取出了 S_NAME,S_ADDRESS,可以避免 A14 再次读取。

A13 相当于计算出了子查询 q_suppkey。

A12 中使用了前面题目介绍的冗余分组键,PS_AVAILQTY 不再作为分组字段,可以提高性能。

A13 将 sum(L_QUANTITY)*0.5 改成 PS_AVAILQTY*2,L_QUANTITY、PS_AVAILQTY 都是整数,这样做可以将小数乘法变成整数乘法。

A14 中使用 join@im,选项 @i 表示删除关联不上的记录,选项 @m 表示 S_SUPPKEY 和 A13 都有序时,用有序归并连接加快速度。

测试结果:

测试项目

执行时间(秒)

一般实现

15

3. 数据转换

本题中要使用前面题目介绍的维表主键序号化方法和日期整数化优化方法,将 Q2 中的 part_2.ctx、supplier_2.ctx、partsupp_2.ctx、nation_2.btx 和 Q14 中的 lineitem_14_4.ctx 复制到本题的主目录中。

计算代码:


A

B

1

=now()

2

1995-1-1

3

=days@o(elapse@y(A2,1))

4

=days@o(A2)

5

>partname="bisque"

6

>nationname="CHINA"

7

=file("nation_2.btx").import@b().(N_NAME==nationname)

8

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

=A8.cursor().skip().(false)

9

=A8.cursor@m(P_PARTKEY;pos@h(P_NAME,partname)).fetch().run(B8(P_PARTKEY)=true)

10

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

=A10.cursor().skip().(null)

11

=A10.cursor@m(S_SUPPKEY,S_NAME,S_ADDRESS;A7(S_NATIONKEY)).fetch()

=A11.run(B10(S_SUPPKEY)=~)

12

=file("partsupp_2.ctx").open().cursor@m(PS_PARTKEY,PS_SUPPKEY,PS_AVAILQTY;B8(PS_PARTKEY) && B10(PS_SUPPKEY)).fetch().keys@im(PS_PARTKEY,PS_SUPPKEY)

13

=file("lineitem_14_4.ctx").open().cursor@m(L_PARTKEY,L_SUPPKEY,L_QUANTITY;L_SHIPDATE>=A4 && L_SHIPDATE<A3)

14

=A13.join@i(L_PARTKEY:L_SUPPKEY,A12:PS_PARTKEY:PS_SUPPKEY,PS_AVAILQTY)

15

=A14.groups@u(L_PARTKEY,L_SUPPKEY;PS_AVAILQTY,sum(L_QUANTITY):quantity)

16

=A15.select(PS_AVAILQTY*2>quantity).id(L_SUPPKEY)

17

=B10(A16)

18

=A17.new(S_NAME,S_ADDRESS).sort(S_NAME)

19

=interval@ms(A1,now())

A7、B8、B10、B11 都是对位序列。其中,A10 的值是 null 或者 supplier 的记录,这样在 A17 中就可以直接使用记录了,不必再次读取。

测试结果:

测试项目

执行时间(秒)

一般实现

15

数据转换

10

4. 列式计算

计算代码:


A

B

1

=now()

2

1995-1-1

3

=days@o(elapse@y(A2,1))

4

=days@o(A2)

5

>partname="bisque"

6

>nationname="CHINA"

7

=file("nation_2.btx").import@b().select@1(N_NAME==nationname).N_NATIONKEY

8

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

=A8.cursor@m().skip().(false)

9

=A8.cursor@mv(P_PARTKEY;pos@h(P_NAME,partname)).fetch().run(B8(P_PARTKEY)=true)

10

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

=A10.cursor@m().skip().(null)

11

=A10.cursor@mv(S_SUPPKEY,S_NAME,S_ADDRESS;S_NATIONKEY==A7).fetch()

=A11.run(B10(S_SUPPKEY)=~)

12

=file("partsupp_2.ctx").open().cursor@mv(PS_PARTKEY,PS_SUPPKEY,PS_AVAILQTY;B8(PS_PARTKEY) && B10(PS_SUPPKEY)).fetch().keys@im(PS_PARTKEY,PS_SUPPKEY)

13

=file("lineitem_14_4.ctx").open().cursor@mv(L_PARTKEY,L_SUPPKEY,L_QUANTITY;L_SHIPDATE>=A4 && L_SHIPDATE<A3)

14

=A13.join@i(L_PARTKEY:L_SUPPKEY,A12:PS_PARTKEY:PS_SUPPKEY,PS_AVAILQTY)

15

=A14.groups@u(L_PARTKEY,L_SUPPKEY;PS_AVAILQTY,sum(L_QUANTITY):quantity)

16

=A15.select@v(PS_AVAILQTY*2>quantity).id(L_SUPPKEY)

17

=B10(A16)

18

=A17.new(S_NAME,S_ADDRESS).sort(S_NAME)

19

=interval@ms(A1,now())

测试结果:

测试项目

执行时间(秒)

一般实现

15

数据转换

10

列式计算

6