性能優化技巧 - 遍歷

【摘要】
數據分析場景中,充斥着聚合運算,常見的有求和、計數、均值、最大最小值等等,想要得到正確的結果值,遍歷技術必不可少,如何更加高效地對數據進行遍歷?點擊:性能優化技巧 - 遍歷,來乾學院一探究竟!

 

1. 存儲方案

集文件是行存方式,組表有行存和列存兩種方式。兩種格式都有一定壓縮效果。

首先,我們來建立一個的普通的文本文件,並在該文件中生成一些數據,代碼如下:

  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

 

2. 並行遍歷

序表過濾時用 [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 毫秒。

 

3. 過濾條件

 

多個條件 && 時注意書寫次序,如果前面的子項爲假時,後面就不會再計算了,這樣把容易爲假的條件項寫到前面,會減少後面條件項的計算次數。

  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選項也可以實現過濾效果,這裏不再單獨例舉。

 

4. 預先過濾

 

組表遊標在創建時即可寫入一些過濾條件。集算器會識別這些條件,利用組表本身的排序信息快速跳到相應的數據位置。另外,這些條件不滿足時取出字段就不會被讀出,可以減少對象產生次數。而已經產生了遊標後再做過濾就沒有這些效果了。我們來看這樣一個例子。

  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 毫秒。

 

5. 遊標取數

 

遊標取數性能和每次取出的記錄數相關,要做些測試,一般最好是幾萬行,不要一次只取一行。

  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 毫秒。

 

6. 遍歷複用

 

使用管道技術可以對基於同一次遍歷計算出多個結果,減少硬盤的訪問。

  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 毫秒。(關於管道的使用,新版本中還有更優寫法,代碼簡潔明瞭,歡迎各位讀者自行體驗集算器語法的優雅之處。)