SQL 提速:TopN 和组内 TopN
【摘要】
从原理上分析 SQL 语句慢的原因,用代码示例给出提速办法。点击了解SQL 提速:TopN 和组内 TopN
问题描述
TopN 是指从数据中查找前 N 名 / 后 N 名。以 Oracle 为例,SQL 是这样的:
select * from (select x from T order by x desc) where rownum<=N
从 SQL 语句上看,是将全部数据按照 x 排序后,取出前 N 条。测试表明,Oracle 可以自动优化这种全集 TopN,没有大排序,性能很好。但是,对于分组后计算组内 TopN 这种更复杂的情况,数据库就很难优化了。
Oracle 组内 TopN 的写法是:
select * from
(select y,x,
row_number()over (partition by y order by x desc) rn
from T)
where rn<=N
SQL 无法直接写出分组后取 TopN 的运算,只能借助窗口函数计算组内序号,再过滤出结果。对于没有窗口函数的数据库来说,组内 TopN 的 SQL 甚至很难写出来。
实测发现,Oracle 计算组内 TopN 比全集 TopN 慢了 n 倍,说明很可能做了排序。情况复杂化之后,Oracle 的优化引擎不起作用了。
排序求 TopN 方法之所以这么慢,是因为算法复杂度很高:M 个成员排序的复杂度是 Mlog2M,需要对数据遍历 log2M 次,计算非常耗时。当数据量较大时,内存无法装下全部数据,还要缓存到外存中,计算速度更会急剧下降。而且,大排序也不容易并行。
提速方案
为了让 TopN 避免全排序,我们可以保持一个大小为 N 的小集合。遍历数据时,将已经计算过的数据前 N 名保存在这个小集合中,取到的新数据如果比当前的第 N 名还大,则插入进去并丢掉现在的第 N 名,如果比当前的第 N 名要小,则不做动作。
这样做,只需要对数据遍历一次。而且,一般来说 N 都不会太大,小集合可以在内存中放得下,即使总数据量很大,也不会发生内外存的数据交换。实际上,对于总数据量不大,全内存计算 TopN 的情况,由于新的算法不必排序,性能也会提高。
在 M 个成员中取出前 N 名,新算法的复杂度是 Mlog2N,在 N<<M 时,比排序方式的复杂度低了很多。分段并行也比较容易,只要每个段都算出前 N 名,再对这些前 N 名的并集计算最终的前 N 名即可,不会出现硬盘并发写的情况。
这种算法的本质是将 TopN 看做聚合运算。通常,我们理解的聚合运算是将一个集合计算成一个单值,比如求和、计数、最大、最小。现在,我们将 TopN 这种返回一个小结果集的情况也看成是聚合,就可以用聚合运算的思路来解决 TopN 问题。这样,分组求组内 TopN 问题,就和分组求和、计数、最大、最小值一样了,很容易实现,可以避免排序计算。
代码示例
一、全集 TopN
A |
|
1 |
=file("T.ctx").open() |
2 |
=A1.cursor@m(x;;4).total(top(-5,x), top(5,x)) |
A1:打开组表。
A2:建立 4 线程的多路游标。total()是对数据全集做聚合,top(-5,x) 计算出 x 最大的前 5 名,top(5,x) 是 x 最小的前 5 名。
A2 执行结果如下:
从上图可以看到,最大前 5 名和最小前 5 名的计算结果各是一个集合。
二、组内 TopN
A |
|
1 |
=file("T.ctx").open() |
2 |
=A1.cursor@m(x,y).groups(y;top(-3,x), top(3,x)) |
A2:groups(y;top(-3,x), top(3,x) 是按照 y 分组,计算组内 x 最大、最小的前 3 名。游标参数中省略了并行数,计算时自动按系统配置中的最大并行数生成多路游标。可以用 top(-3,x):top3 的方式来重命名结果集合中的字段。
A2 执行结果如下:
可以看到,结果集除了分组字段以外,第二个字段是最大前 3 名的集合,第三个字段是最小前 3 名的集合。