【摘要】
數據分析場景中,充斥着聚合運算,常見的有求和、計數、均值、最大最小值等等,想要得到正確的結果值,遍歷技術必不可少,如何更加高效地對數據進行遍歷?點擊:性能優化技巧 - 遍歷,來乾學院一探究竟!
集文件是行存方式,組表有行存和列存兩種方式。兩種格式都有一定壓縮效果。
首先,我們來建立一個的普通的文本文件,並在該文件中生成一些數據,代碼如下:
A | B | |
1 | =file("txt/employee.txt") | |
2 | =file("info/ming_en_female.txt")[email protected]() | =A2.len() |
3 | =file("info/ming_en_male.txt")[email protected]() | =A3.len() |
4 | =file("info/xing_en.txt")[email protected]() | =A4.len() |
5 | =city=file("info/city_en.txt")[email protected]() | =A5.len() |
6 | =salary=20000 | /10000~30000 |
7 | =["one","two","three","four","five","six","seven","eight","nine","ten"] | =A7.len() |
8 | =height=50 | /160~210cm |
9 | =weight=50 | /50~100kg |
10 | =birthtime=946656 | /1970~2000 |
11 | for 100 | =to((A11-1)*100000+1,A11*100000).new(~:id,if(rand(2)==0,A2(rand(B2)+1),A3(rand(B3)+1))+" "+A4(rand(B4)+1):name,if(A2.pos(name.array(" ")(1)),"Female","Male"):sex,A5(rand(B5-1)+1):city,date(rand(birthtime)*long(1000000)):birthday,rand(salary)+10000:salary,A7(rand(B7-1)+1):level,rand(height)+160:height,rand(weight)+50:weight,if(rand(2)==0,A2(rand(B2)+1),A3(rand(B3)+1))+"&"+A4(rand(B4)+1)+" Co. Ltd":company) |
12 | [email protected](B11) |
代碼1.1
代碼 1.1,生成一個 txt 文件,總記錄數爲 1000 萬,其中部分數據如圖 1.1 所示。
圖1.1
A | |
1 | =file("txt/employee.txt")[email protected]() |
2 | =file("btx/employee.btx")[email protected](A1) |
代碼1.2
A | |
1 | =file("txt/employee.txt")[email protected]() |
2 | =file("ctx/employee.ctx").create(#id,name,sex,city,birthday,salary,level,height,weight,company).append(A1) |
代碼1.3
A | |
1 | =file("txt/employee.txt")[email protected]() |
2 | =file("ctx/[email protected]")[email protected](#id,name,sex,city,birthday,salary,level,height,weight,company).append(A1) |
代碼1.4
代碼 1.2、1.3、1.4 分別使用代碼 1.1 建立的 txt 文件,轉爲集文件 employee.btx、列存組表文件employee.ctx和行存組表文件[email protected],各文件大小如圖 1.2 所示。
圖1.2
按照文件佔用的硬盤空間大小排序可以得到:txt> 行存組表 > 集文件 > 列存組表。可見,同樣的數據,在不同的文件存儲格式下,所佔用的硬盤空間大小也不同,而文件的大小又會直接影響遍歷的效率。
排序能有效提高列存組表壓縮效率,重複次數多的字段排到前面。
A | |
1 | =file("ctx/employee.ctx").create().cursor().sortx(level,height,weight,city) |
2 | =file("ctx/employee_sort.ctx").create(#id,name,sex,city,birthday,salary,level,height,weight,company).append(A1) |
代碼1.5
代碼1.5對原組表文件的level,height,weight,city列依次進行排序。
排序後的組表文件 employee_sort.ctx與原始文件employee.ctx相比,明顯變小,如圖1.3所示。
圖1.3
序表過濾時用 [email protected] 可以並行計算。
A | |
1 | =now() |
2 | =file("ctx/employee.ctx").create().cursor(;level=="two" && city=="Reno").fetch() |
3 | [email protected](A1,now()) |
4 | =now() |
5 | =file("ctx/employee.ctx").create()[email protected](;level=="two" && city=="Reno";4).fetch() |
6 | [email protected](A4,now()) |
7 | =file("ctx/employee.ctx").create().cursor().fetch() |
8 | =now() |
9 | =A7.select(level=="two" && city=="Reno") |
10 | [email protected](A8,now()) |
11 | =now() |
12 | [email protected](level=="two" && city=="Reno") |
13 | [email protected](A11,now()) |
代碼2.1
代碼2.1 中:
A2、A5 分別是外存的串行和並行情況,耗時分別爲 2742 毫秒和 828 毫秒。
A9、A12 分別是內存的串行和並行情況,耗時分別爲 1162 毫秒和 470 毫秒。
集文件和組表上都可以定義多路遊標實現並行遍歷。列存 + 機械硬盤 + 取用列過多時多路遊標不一定會更快。
A | |
1 | =now() |
2 | =file("btx/[email protected]")[email protected](;4).select(level=="two" && city=="Reno").fetch() |
3 | [email protected](A1,now()) |
4 | =now() |
5 | =file("ctx/employee.ctx") |
6 | =A5.create()[email protected](;;4).select(level=="two" && city=="Reno") |
7 | =A6.fetch() |
8 | [email protected](A4,now()) |
代碼2.2
代碼2.2中:
前 3 行是集文件的並行遍歷,耗時 1861 毫秒。
後 5 行是相同數據的列存組表多路遊標並行遍歷。耗時 2282 毫秒。
使用 fork 語句並行時,不要返回遊標。遊標只是定義並沒有實際取數,這種並行沒有意義。要在 fork 代碼塊中做 fetch 或 groups 等實質取數的動作纔有意義。
A | B | |
1 | =file("ctx/employee.ctx").create() | |
2 | =now() | |
3 | fork to(4) | =A1.cursor(;level=="one" && height==180;A3:4).fetch() |
4 | return B3 | |
5 | =A3.conj() | |
6 | [email protected](A2,now()) | |
7 | =file("ctx/employee.ctx").create() | |
8 | =now() | |
9 | fork to(4) | =A7.cursor(;level=="one" && height==180;A9:4) |
10 | return B9 | |
11 | =A9.conjx().fetch() | |
12 | [email protected](A8,now()) |
代碼2.3
代碼2.3,前6行在fork代碼塊中完成fetch取數,然後合併結果,查詢耗時865毫秒。第7行至12 行,fork 返回遊標後,合併再進行 fetch 動作,耗時 1709 毫秒。
多個條件 && 時注意書寫次序,如果前面的子項爲假時,後面就不會再計算了,這樣把容易爲假的條件項寫到前面,會減少後面條件項的計算次數。
A | |
1 | =file("btx/employee.btx")[email protected]().fetch() |
2 | =now() |
3 | =A1.select(salary < 10010 && like(name,"*d*")) |
4 | [email protected](A2,now()) |
5 | =now() |
6 | =A1.select(like(name,"*d*") && salary < 10010) |
7 | [email protected](A5,now()) |
代碼3.1
代碼3.1:
A3 中的條件爲salary < 10010 && like(name,"*d*"),前面的子項返回結果集較小,查詢耗時748 毫秒。
A6 中的條件爲like(name,"*d*") && salary < 10010,前面的子項返回結果集較大,查詢耗時1814毫秒。
在集合中找成員時(IN 判斷),避免在過濾條件中臨時計算這個集合。集合成員較多時要先排序,然後用 [email protected] 或 [email protected] 去判斷,將使用二分法。
A | |
1 | =100.(rand(1000000)+1) |
2 | =file("txt/keys.txt").export(A1) |
代碼3.2
在代碼3.2中:
A1:取 100 個範圍在 1 至 1000000 中的隨機數;
A2:爲確保後續測試的數據一致,將這 100 個隨機數存到文件 keys.txt 中。
A | |
1 | =file("txt/keys.txt")[email protected]() |
2 | =A1.(~*10) |
3 | =file("ctx/employee.ctx").create() |
4 | =now() |
5 | =A3.cursor(;A2.pos(id)).fetch() |
6 | [email protected](A4,now()) |
代碼3.3
在代碼3.3中:
A2:將預先準備的每個鍵值都乘以 10。
A5:使用 pos 函數在組表文件 employee.ctx 中找滿足 A2 的成員並取出,耗時 15060 毫秒。
A | |
1 | =file("txt/keys.txt")[email protected]() |
2 | =file("ctx/employee.ctx").create() |
3 | =now() |
4 | =A2.cursor(;A1.(~*10).pos(id)).fetch() |
5 | [email protected](A3,now()) |
代碼3.4
在代碼3.4中:
與代碼3.3 的區別在於,把代碼 3.3 中 A2 的集合搬到了代碼 3.4 中 A4 的 cursor 過濾條件中臨時計算這個集合,執行耗時 32105 毫秒。相比代碼 3.3,雖然結果一致,但耗時多了一倍,應當避免這種寫法。
A | |
1 | =file("txt/keys.txt")[email protected]() |
2 | =A1.(~*10).sort() |
3 | =file("ctx/employee.ctx").create() |
4 | =now() |
5 | =A3.cursor(;[email protected](id)).fetch() |
6 | [email protected](A4,now()) |
代碼3.5
代碼3.5,基於代碼3.3,我們還可以再進行一些優化。
將代碼3.3 的 A2 排序,得到了有序鍵值。
在A5 中的 pos 函數採用選項 @b,使用二分法。執行耗時 7854 毫秒,相比代碼 3.3 快了將近一倍。
[email protected]@d 可用於快速實現鍵值過濾,hash 索引常常會比二分法更有效。
A | |
1 | =file("txt/keys.txt")[email protected]() |
2 | =A1.(~*10).sort() |
3 | =file("ctx/employee.ctx").create() |
4 | =now() |
5 | =A3.cursor()[email protected](id,A2).fetch() |
6 | [email protected](A4,now()) |
代碼3.6
代碼3.6 中:
A5:使用[email protected]過濾出滿足序列A2中的數據,結果與代碼3.3、代碼 3.4、代碼 3.5 一致,耗時爲 7104 毫秒。switch函數時會自動建索引@d選項也可以實現過濾效果,這裏不再單獨例舉。
組表遊標在創建時即可寫入一些過濾條件。集算器會識別這些條件,利用組表本身的排序信息快速跳到相應的數據位置。另外,這些條件不滿足時取出字段就不會被讀出,可以減少對象產生次數。而已經產生了遊標後再做過濾就沒有這些效果了。我們來看這樣一個例子。
A | |
1 | =file("ctx/employee.ctx").create() |
2 | =now() |
3 | =A1.cursor(city,sex;level=="one" && height<180) |
4 | =A3.groups(city;count(sex=="Male"):Male,count(sex=="Female"):Female) |
5 | [email protected](A2,now()) |
6 | =now() |
7 | =A1.cursor().select(level=="one" && height<180) |
8 | =A7.groups(city;count(sex=="Male"):Male,count(sex=="Female"):Female) |
9 | [email protected](A6,now()) |
代碼4.1
代碼 4.1 中:
A3 在組表遊標創建時寫入過濾條件 level=="one" && height<180 並且只取 city 和 sex 兩列數據。
A7 在組表遊標創建後,再通過 select 中的過濾條件篩選數據。
隨後兩者進行了相同的分組聚合運算,結果前者耗時1206 毫秒,後者耗時 4740 毫秒。
遊標取數性能和每次取出的記錄數相關,要做些測試,一般最好是幾萬行,不要一次只取一行。
A | B | |
1 | =file("ctx/employee.ctx").create().cursor() | |
2 | >ctx_length_10=0 | |
3 | =now() | |
4 | for A1,10 | =A4.len() |
5 | >ctx_length_10=ctx_length_10+B4 | |
6 | [email protected](A3,now()) | |
7 | =file("ctx/employee.ctx").create().cursor() | |
8 | >ctx_length_50000=0 | |
9 | =now() | |
10 | for A7,50000 | =A10.len() |
11 | >ctx_length_50000=ctx_length_50000+B10 | |
12 | [email protected](A9,now()) |
代碼5.1
代碼 5.1 中:
代碼通過遍歷組表遊標,獲取結果,並累計每次結果的記錄數。
前6行遍歷過程中每次取10條記錄,最終累計耗時7823毫秒。
後6行遍歷過程中每次取50000條記錄,最終累計耗時3923毫秒。
還可以使用 skip 函數計數,這樣不必把遊標數據讀出產生成 java 對象。
A | |
1 | =file("ctx/employee.ctx").create() |
2 | =now() |
3 | =A1.cursor(#1;).fetch().len() |
4 | [email protected](A2,now()) |
5 | =now() |
6 | =A1.cursor().skip() |
7 | [email protected](A5,now()) |
代碼5.2
代碼 5.2 中:
A3 在創建組表遊標時取第一列,然後取出該列數據後取得其長度。
A6 對組表遊標使用 skip 函數,獲取該組表記錄數。
這兩個單元格計算後的值都爲 10000000,但前者耗時 9676 毫秒,後者耗時 6473 毫秒。
使用管道技術可以對基於同一次遍歷計算出多個結果,減少硬盤的訪問。
A | B | C | |
1 | =file("ctx/employee.ctx").create().cursor() | ||
2 | =channel(A1) | =channel(A1) | =channel(A1) |
3 | >A2.select(salary<10123 && level=="nine").fetch() | >B2.select(level=="two" && height==183).fetch() | >C2.select(city=="Reno" && weight>95).fetch() |
4 | =now() | ||
5 | for A1,100000 | ||
6 | =A2.result() | =B2.result() | =C2.result() |
7 | [email protected](A4,now()) | ||
8 | =now() | ||
9 | =file("ctx/employee.ctx").create().cursor().select(salary<10123 && level=="nine").fetch() | =file("ctx/employee.ctx").create().cursor().select(level=="two" && height==183).fetch() | =file("ctx/employee.ctx").create().cursor().select(city=="Reno" && weight>95).fetch() |
10 | [email protected](A8,now()) |
代碼6.1
代碼 6.1 中:
A2、B2、C2 分別是組表遊標 A1 上建立的管道,A3、B3、C3 爲這三個管道定義不同篩選條件並定義取數,A6 中遍歷組表遊標,每次取 100000 條。A6、B6、C6 返回按前文定義的篩選條件返回的結果集。耗時 5182 毫秒。
第9 行的三個單元格沒有使用管道,分別三次建立組表遊標再按前文三個相同的篩選條件取出結果。耗時 12901 毫秒。(關於管道的使用,新版本中還有更優寫法,代碼簡潔明瞭,歡迎各位讀者自行體驗集算器語法的優雅之處。)