TopN是常見的運算,用SQL寫出來是這樣(以Oracle爲例):
select * from (select * from T order by x desc) where rownum<=N
這個SQL的運算邏輯從其語句上看,要先做排序(Order by),然後再取出前N條。
我們知道,排序是個非常慢的動作,複雜度很高(n*logn),如果涉及數據量大到內存放不下,那還需要進行內外存數據交換,性能還會急劇下降。
而事實上,要計算TopN,我們能設計出不需要全排序的算法,只要保持一個大小爲N的小集合,對數據集遍歷一次,將遍歷過的數據的前N名保存在這個小集合中,遍歷到新一條數據,如果比當前的第N名還大,則插入進去並丟掉現在的第N名,如果比當前的第N名要小,則不做動作。
這樣計算的複雜度要低很多(n*logN,n是總數據條數),而且一般N都不大,可以在內存中放下,無論數據量有多大,都不會涉及內外存交換問題。
但是,SQL無法描述上面這個計算過程,這時,我們只能寄希望於數據庫引擎是不是能自己優化。使用SPL就容易描述上面這個計算過程,從而實現高性能計算。
我們來測試一下看Oracle是不是會做這個優化,即用Oracle實現TopN後和SPL做同樣運算相比。因爲SPL能使用優化算法,如果Oracle的計算時間和SPL差不多,則說明它做了優化,如果差得很遠,則可能是做了全排序。
一、 數據準備和環境
用SPL腳本生成數據文件,數據共兩列,第一列id是小於20億的隨機整數,第二列amount是不大於1千萬的隨機實數。數據記錄爲80億行,生成的原始文本文件大小爲169G。利用數據庫提供的數據導入工具將此文件數據導入到Oracle的數據表topn中,同時也用此文件數據生成SPL組表文件topn.ctx。
在一臺Intel服務器上完成測試,2個Intel3014 CPU,主頻1.7G,共12核,內存64G。數據庫表數據及SPL組表文件均存儲在同一塊SSD硬盤上。
我們刻意將數據量設計得比內存大,這樣如果進行排序則會有內外存交換動作,性能下降會非常大,容易被觀測到。
二、 常規TopN
取出topn表中amount最大的前100條。
1. Oracle測試
查詢用的SQL語句是:
select * from (
select /*+ parallel(4) */
* from topn order by amount desc
) where rownum<=100;
說明:/*+ parallel(4) */ 是Oracle的並行查詢語法,其中4是並行數。
2. SPL測試
編寫SPL腳本執行測試:
A | ||
1 | =now() | /記錄時間 |
2 | =4 | /並行數 |
3 | =file("/home/topn/topn.ctx").create() | /產生組表對象 |
4 | =A3.cursor@m(id,amount;;A2).groups(;top(100;-amount)) | |
5 | =interval@s(A1,now()) | /算出執行時間 |
與SQL不同,SPL把TopN看成是一種聚合運算,和sum/count這類運算一樣,不同的只是TopN將返回一個集合,而sum/count返回的是單值。但它們的計算邏輯是一樣的,都只需要對原數據集遍歷一次,並且不涉及全排序。
A4格中的groups(;top(100;-amount)就是對全集做TopN聚合運算,計算出前100名。
3. 結論與分析
常規TopN測試時間見下表:
測試結果(時間單位:秒)
並行數 | 1 | 2 | 4 | 8 | 12 |
Oracle | 1922 | 952 | 526 | 308 | 256 |
SPL組表 | 2641 | 1565 | 729 | 371 | 321 |
測試表明,Oracle比SPL還要更快一點,而SPL沒有做全排序,這說明Oracle在這種情況會自動優化。
Oracle比SPL快也可以理解,因爲Oracle是用C++開發的,而目前版本的SPL是用java開發的。C++比java計算更快是正常的,而且這次測試讀取了全部的兩列數據,且數據隨機無序,很難壓縮,所以組表的列式存儲也沒有優勢。
三、 增加複雜度
對於最基本的TopN,Oracle很聰明,即使寫成了子查詢也會優化。我們下面增加問題的難度,改成分組後在每一組中做TopN。
具體運算設計爲:按照id字段最後一位數字值進行分組,然後查詢出每組中amount最大的前100條記錄。id是個整數,這樣也就只有10個分組,分組本身的計算量並不大,不過要對分組數據做TopN,整體計算複雜度略高了一些。如果沒有全排序,總體運算時間應當比剛纔的情況多,但還在同一數量級範圍內。
1. Oracle測試
查詢用的SQL語句是:
select * from (
select /*+ parallel(2) */
mod(id,10) as gid,amount,
row_number()over (partition by mod(id,10) order by amount desc) rn
from topn
) where rn <= 100;
SQL無法直接寫出分組後取TopN的運算,只能藉助窗口函數計算序號,語法中還是有排序(order by)的字樣。
2. SPL組表測試
編寫SPL腳本執行測試:
A | ||
1 | =now() | /記錄時間 |
2 | =4 | /並行數 |
3 | =file("/home/topn/topn.ctx").create() | /產生組表對象 |
4 | =A3.cursor@m(id,amount;;A2).groups@u(id%10:gid;top(100;-amount)) | |
5 | =interval@s(A1,now()) | /算出執行時間 |
因爲SPL把TopN看成是聚合計算,所以可以很容易放在分組彙總中,和全量聚合的寫法幾乎是一樣的。
3. 結論與分析
測試結果(時間單位:秒)
並行數 | 1 | 2 | 4 | 8 | 12 |
Oracle | 41649 | 19602 | 9359 | 4627 | 3211 |
SPL組表 | 4380 | 2127 | 1007 | 465 | 349 |
增加難度後,Oracle比之前的簡單情況慢了10倍還多,而且比SPL做同樣運算也慢了近10倍。這說明Oracle在這種情況下很可能做了排序動作,情況複雜化之後,Oracle的優化引擎不起作用了。
而SPL在這兩種情況下的運算時間差別不到2倍,基本上在同一數量級,符合我們之前的分析,算法的優勢得到了充分的體現。