性能优化案例课程 TPCH-Q13

select
    c_count,
    count(*) as custdist
from (
    select
        c_custkey,
        count(o_orderkey) c_count
    from
        customer left outer join orders on
            c_custkey = o_custkey
            and o_comment not like '%special%accounts%'
    group by
        c_custkey
) c_orders
group by
    c_count
order by
    custdist desc,
c_count desc;

这个查询简单看是对 orders 做两轮常规分组,第一轮按 custkey 分组计算出每个顾客的下单数,第二轮再按下单数分组计算出每种下单数各有多少顾客。

1. 数据存储

Orders 和 customer 表没有什么特殊要求,按照主键有序存储。

继续使用题目 Q3 中的 orders.ctx、customer.ctx。

将这些表复制到本题的主目录中。

2. 一般实现

注意到原 SQL 中有个左连接(也就是左外连接),需要将没有下单过的顾客(下单数为 0)也统计在内,而基于 orders 针对 O_CUSTKEY 做分组汇总时会遗漏掉这些数据,需要事后再统计出来再补一条记录,即下单数为 0 的顾客数量。


A
1 =now()
2 >filter="*special*accounts*"
3 =file("orders.ctx").open().cursor@m(O_CUSTKEY;!like(O_COMMENT,filter))
4 =A3.groups@u(O_CUSTKEY;count(1):c_count)
5 =A4.len()
6 =A4.groups@um(c_count;count(1):custdist)
7 =file("customer.ctx").open().cursor@m().skip()
8 =A6.insert(0,0,A7-A5)
9 =A6.sort@z(custdist,c_count)
10 =interval@ms(A1,now())

代码中用到了前面讲到过的游标前过滤、多线程并行等优化方法。

A4 做第一轮分组,A6 做第二轮。A7 计算所有客户数,A8 中用 A7 减去已下单的客户数 A5,得到没下单的客户数,补充到 A6 上再一起排序。

A4 中用 groups@u 分组,@u 表示分组后不用按分组字段 O_CUSTKEY 排序。

测试结果:

测试项目 执行时间(秒)
一般实现 22

3. 序号分组

上节的 groups 函数在分组时,需要 hash 计算。如果分组字段是序号,那么可以用 @n 选项直接定位,避免 hash 计算。

这里使用前面介绍的维表主键序号化方法,customer 中的 C_CUSTKEY 和 orders 中的 O_CUSTKEY 都已在前面的例子中转换过了。

可以直接使用以前的题目 Q3 转换的 orders_5.ctx。customer 使用 Q5 中的 customer_3.ctx。

将这些表复制到本题的主目录中。

计算代码:


A
1 =now()
2 >filter="*special*accounts*"
3 =file("customer_3.ctx").open().cursor().skip()
4 =file("orders_5.ctx").open().cursor@m(O_CUSTKEY;!like(O_COMMENT,filter))
5 =A4.groups@n0(O_CUSTKEY;count(1):c_count;A3)
6 =A5.len()
7 =A5.groups@um(c_count;count(1):custdist)
8 =A7.insert(0,0,A3-A6)
9 =A7.sort@z(custdist,c_count)
10 =interval@ms(A1,now())

A5 中 groups@n0 表示直接按 O_CUSTKEY 的值定位到第几组,@0 表示去掉空组。

测试结果:

测试项目 执行时间(秒)
一般实现 22
序号分组 17

4. 序号化后用序列计算

上一节序号化分组的结果集是序表,分组计算的时候要向序表聚合,占用内存比较大。可以在序号化后直接使用序列来进行分组统计,减少内存占用,提高性能。

计算代码:


A B
1 =now()
2 >filter="*special*accounts*"
3 =file("customer_3.ctx").open().cursor().skip()
4 =file("orders_5.ctx").open().cursor@m(O_CUSTKEY;!like(O_COMMENT,filter))
5 fork A4 =A3.(0)
6
=A5.run(B5(O_CUSTKEY)+=1).skip()
7
return B5
8 =transpose(A5).(~.sum()).groups@m(~:c_count;count(1):custdist)
9 =A8.sort@z(custdist,c_count)
10 =interval@ms(A1,now())

A3 求出 customer 表的记录数 (为便于下文描述,记为 n)。

A5 对 A4 游标进行多线程分段计算,线程数 m 取决于 A4 的 cursor@m,这里是 SPL 设置的多路游标缺省路数。

每个线程在 B5 中定义一个有 n 个成员的序列,赋初值为 0。在 B6 中循环当前线程对应的 orders 游标分段,让序列的第 O_CUSTKEY 个成员值加 1。多线程运行完后,会在 A5 得到 m 个长度为 n 的序列,分别表示各分段中 O_CUSTKEY 的下单数。

A8 对 A5 进行行列转置,结果是 n 个长度为 m 的序列,每个序列对应一个客户。将每行的 m 个成员加起来,就得到每个客户的下单总数。然后用 groups 计算第二轮分组,得到每种下单数对应的顾客数。

这个过程中都是对序列计算,相比序表节省内存,性能更高。

测试结果:

测试项目 执行时间(秒)
一般实现 22
序号分组 17
序号化后用序列 11

5. 列式计算


A
1 =now()
2 >filter="*special*accounts*"
3 =file("orders_5.ctx").open().cursor@mv(O_CUSTKEY;!like(O_COMMENT,filter))
4 =A3.groups@uz(O_CUSTKEY;count(1):c_count)
5 =A4.len()
6 =A4.groups@um(c_count;count(1):custdist).o()
7 =file("customer_3.ctx").open().cursor().skip()
8 =A6.insert(0,0,A7-A5)
9 =A6.sort@z(custdist,c_count)
10 =interval@ms(A1,now())

A4 中 groups 加选项 @z,表示多路并行分组时各路共享结果集。选项 @z 在分组结果集很大时 (一般百万组以上) 使用。

列式序表不能插入数据,A6 中的 o() 函数将列式序表转换成常规序表,以便在 A8 中调用 insert 方法。

列式计算还未提供 groups@n,不能使用序号分组方法。

测试结果:

测试项目 执行时间(秒)
一般实现 22
序号分组 17
序号化后用序列 11
列式计算 5