1. 程式人生 > >多層科目任意組合彙總報表的效能優化 (下)

多層科目任意組合彙總報表的效能優化 (下)

2.4 有序計算方案

在充分利用遍歷一次的特點進行優化後,可能我們還會覺得計算效能有點慢,希望有進一步優化的空間。由於每次只需要取出總資料量的很小一部分 (100 個指標涉及的所有科目號大概幾百個,即在幾百萬記錄中取幾百條),這時我們通常能想到的是:如果能利用資料有序直接進行有序查詢(若源資料有序,可以快速定位到這幾百條記錄,不需要遍歷幾百萬記錄甚至更多的資料),將能夠獲得更好的查詢效率。

我們可以利用集算器提供的 iselect() 函式對每個計算的指標進行有序查詢,從而減少遍歷次數。這裡需要注意兩個關鍵點:

1、 iselect()函式用單個主鍵的查詢速度會比用多個主鍵查詢更快,並且寫法上也會簡單很多。

2、 在資料預處理時,遇到多個主鍵時應該想辦法合併成一個,並且數字化後進行排序,以便使用 iselect() 函式。

關於 iselect() 函式的具體用法和有序計算的解釋這裡不再贅述,可參考集算器教程的相關章節。

2.4.1 合併主鍵、排序

為了滿足上面提出的兩個關鍵點,我們需要對源資料重新預處理一遍,關於分組計算彙總值、利用跨行組計算累計值等原理上面已經講過了,這裡主要說合併主鍵和排序

第一步,在原始資料中,用“年”和“月”兩列欄位動態計算一個變數值,稱為“月號”,以便與“科目”欄位合併成唯一主鍵。程式碼中相應的改動如下:


A

B

1

=file("總賬憑證 -pre.btx")


2

=file("總賬憑證 -mid.btx")


3

[email protected]()

>A3.run(((年 -inityear)*12+ 月): 月 )

4

=A3.groupx(科目, 月:月號;sum(金額): 金額 )


5

for A4;科目

=A5.run(金額 = 金額 [-1]+ 金額 )

6


>[email protected](B5,#1:科目,#2: 月號,#3: 累計金額 )

其他格子的程式碼,前面已經解釋過了,這裡不再贅述。

B3:首先在集算器中定義引數名稱:inityear,設定值為 2014,如下圖:

undefined

假設原始資料是從 2014 年開始的,所以把初始年份的預設值設定為 2014。所謂“月號”就是每條記錄的時間是從初始年份 1 月開始的第幾個月。比如:當前一條資料記錄中年是 2017,月是 3 的話,那麼根據這個公式的結果:月號 =(2017-2014)*12+3,也就是 2014 年 1 月開始的第 39 個月。將計算結果利用 run() 函式重新賦值給月欄位,以便後面與科目構造唯一主鍵。

A4:按科目、月號進行分組,金額進行求和(前面已經解釋過)

B5:對金額欄位進行累計(前面已經解釋過)

B6:計算後的結果集以追加的方式儲存到集檔案中(前面已經解釋過),即總賬憑證 -mid.btx,執行結果如下圖:

      undefined

第二步,對科目前 N 位分別彙總金額;如何計算多層科目彙總值前面已經講過了,這裡主要關注月號和科目合併成主鍵 key,然後進行排序。月號計算出來是 2 位(假設資料記錄跨度不超過 99 個月),科目為固定的 10 位,這樣為了保證合併成主鍵後的唯一性,需要定義新主鍵的總長度為 12 位

這樣,新主鍵的構造規則就是:key(12 位)= 月號 (月號為 2 位)10000000000+ 總賬科目 (最長為 10 位)。有一個技巧需要說明一下:這裡設定 key 的長度為 12 位,可以存放在一個 long 型別中,如果更長 (與需求有關),就要用字串了,雖然會相對慢一點,但也影響不大。

集算器的 SPL 指令碼如下:


A

1

=file("總賬憑證 -mid.btx")

2

=file("總賬憑證 -later.btx")

3

[email protected]()

4

=channel(A3).groupx((科目 \100): 科目, 月號;sum( 累計金額): 累計金額彙總 )

5

=channel(A3).groupx((科目 \10000): 科目, 月號;sum( 累計金額): 累計金額彙總 )

6

=A3.groupx((科目 \1000000): 科目, 月號;sum( 累計金額): 累計金額彙總 )

7

=[A6,A5.result(),A4.result()].conjx()

8

=A7.new(#210000000000+#1:key,#3:累計金額彙總 ).sortx(key)

9

>[email protected](A8)

A1-A3:前面已經解釋過了,這裡不再贅述。

A4:建立管道,將遊標 A3 中的資料推送到管道,其中 ch.groupx() 函式針對管道中的有序記錄分組並返回管道;按科目擷取前 8 位、月號進行分組,累計金額進行彙總。返回的資料結構如下圖:

undefined

A5:同理於 A4 返回管道,按科目擷取前 6 位、月號進行分組,累計金額進行彙總。返回的資料結構如下圖:

undefined

A6:返回遊標,按科目擷取前 4 位、月號進行分組,累計金額進行彙總。返回的資料結構如下圖:

undefined

A7:多個遊標運算結果合併成一個結果集;其中 ch.result() 代表管道的運算結果

A8:對 A7 的每條記錄生成新序表,序表包含兩個欄位:由月號 (月號為 2 位)10000000000,然後再加上擷取後生成的新的科目 (最長為 10 位),重新定義為 key 和累計金額彙總兩列欄位,接著再對 key 進行排序。

特別說明一下,cs.groupx() 函式按照欄位分組後,會對該欄位進行排序,也就是說運算後的結果本身就是有序的,所以我們可以利用這個特性,先按月號分組 (寫前面),再用 cs.mergex() 函式按照月號、科目做有序歸併運算,歸併後的結果就不再需要排序了。相應地程式碼改動如下:


A

4

=channel(A3).groupx(月號,(科目 \100): 科目;sum( 累計金額): 累計金額彙總 )

5

=channel(A3).groupx(月號,(科目 \10000): 科目;sum( 累計金額): 累計金額彙總 )

6

=A3.groupx(月號,(科目 \1000000): 科目;sum( 累計金額): 累計金額彙總 )

7

=[A6,A5.result(),A4.result()].mergex(月號, 科目 )

8

=A7.new(#110000000000+#2:key,#3:累計金額彙總 )

A9:計算後的結果集匯出並儲存到集檔案中,即總賬憑證 -later.btx。資料結構如下圖:

undefined

2.4.2 有序查詢

這樣,我們就按照要求完成了資料預處理工作,接下來分兩步驗證報表查詢:

1、定義子程式: 任意給定一個計算指標,能夠快速返回指標彙總值;然後多次呼叫子程式來完成 100 個指標的計算,返回結果集。

2、任意給定 100 個計算指標,快速返回與之對應的指標彙總值。

2.4.2.1 多次 ISELECT 查詢

首先,定義一個子程式,任意給定一個計算指標(可能只有科目號前面 N 位,比如前 4 位 /6 位 /8 位 /10 位等,自由組合出現),返回這個指標彙總值。

然後,通過呼叫子程式來完成 100 個指標的計算,先定義查詢引數, yyyy 代表查詢年,mm 代表查詢月,比如:查詢 2017 年 1 月的資料,如下圖:

undefined

呼叫子程式的樣例:


A

B

C

1

=inityear=2014

=((yyyy-inityear)*12+mm)10000000000

=file("總賬憑證 -later.btx")

2

func



3


=A2.(B1+)

=B3.sort()

4


[email protected](C3,key)


5


=B4.fetch()


6


return B5.sum(累計金額彙總 )

/指標引數列

7

=func(A2,C7)


[1001,1002]

8

=func(A2,C8)


[2702,153102,12310105,1122,12310101,12310401,12319001,12310201,12310301,12310501,12310601,12310701,12310801,12319101]


107

return [A7:A106]



A1:定義變數 inityear,假定原始資料是從 2014 年開始的,所以設定預設值為 2014;

B1:按照前面相同的規則生成“月號”。如果引數是 2017 年 1 月,執行結果如下:

undefined

C1:預處理後的資料檔案物件

A2-C6:子程式程式碼。子程式是以語句 func 為主格的程式碼塊,結果用 return 語句返回。這個子程式主要功能是任意給定一個計算指標,返回彙總值。

B3:接收引數中每一個科目號,利用月號 (月號為 12 位) 加上當前科目號,形成指標的引數集合。比如傳入引數為:[1214,1207], 則執行結果如下圖:

undefined

C3:接著對對指標引數集合 B3 排序。執行結果如下圖:

undefined

B4:根據指標 C3 中的引數集合與結果集檔案中有序的 key 欄位進行比對查詢,返回遊標;其中 @b 代表從集檔案中讀取。

B5:從遊標中獲取記錄,執行結果如下圖:

undefined

B6:對累計金額彙總求和,返回指標的計算結果。

C7:指標 A 的引數條件 (按科目號前 4 位擷取的多個值形成的集合)

C8:指標 B 的引數條件 (按科目號前 4 位 / 前 6 位 / 前 8 位擷取的多個值,形成的引數集合) ,剩餘的 98 個指標,計算的寫法類似 A8,引數的寫法類似 C8,依次類推到 100。

A7:呼叫 func 子程式,把 C7 的指標引數值傳入到子程式中,子程式計算後返回結果。

A8:同理,計算指標 B 的結果集

A107:合併 A7-A106 每個格子的值 (從上往下,100 個指標的計算結果),返回一個單列資料集,可以供報表工具使用。

這樣做已經可以利用有序查詢了,但計算 100 個指標還需要執行 100 次子程式的 iselect() 函式,依然遍歷太多,編碼過程也比較繁瑣。

2.4.2.2 一次 ISELECT 查詢

那麼,有沒有辦法只做一次 iselect 查詢呢?

答案是有!我們可以把 100 個需要計算的指標的科目號都整理好,然後執行一次 iselect() 函式,把所有指標彙總結果都查找出來,這樣,就大功告成了。

這裡,需要注意兩個關鍵點:

1、需要將多個計算指標中的不同科目號進行合併、構造主鍵、排序。

2、利用 pos()的函式技巧,根據每個計算指標中多個科目號與月號構造的主鍵在結果集中的找到座標位置 ( 與 key 列欄位比對),返回位置序號, 接著根據位置序號在結果集中找到的累計金額彙總欄位進行求和,求和結果再按位置序號倒回到每個指標中,即每個指標的彙總值計算完成。

為了便於理解,舉個例子,詳細解釋一下利用 pos() 函式是如何做到定位計算的?示意圖如下:

undefined

解釋:指標 A 和指標 B 的所有科目號合併,然後統一排序生成序號,通過序號在有序結果集中找到對應的金額,再利用位置序號把金額倒回到每個指標中,每個指標下對多個科目號的金額彙總,即指標彙總值。

最終,計算 100 個指標的集算器的 SPL 指令碼樣例如下:


A

B

C

D

E

1

/引數變數

=now()

=((yyyy-inityear)*12+mm)*10000000000



2

[1001,1002,1012]

[2001]

[1101]

[1121,12310106,12310206,12310306,12310406,12310506,12310606,12310706,12310806,12319006,12319106]

[2101]

21

[221102]

[1221,12310102,12310202,12310302,12310402,12310502,12310602,12310702,12310802,12319002,12319102]

[2221]

[1321,1401,1402,1403,1404,1405,1406,1407,1408,1409,1411,1412,1461,1471]

[1403,147101,1471050100]

22

=[A2:E21]

=A22.(.(C1+))

=A22.union().sort()



23

=file("總賬憑證 -later.btx")





24

[email protected](C22,key)

=A24.fetch()

=B24.(key)

=B24.(累計金額彙總 )


25

=A22.(.([email protected]()))





26

=A25.(.sum(D24(~)))





27

return A26


[email protected](B1,now())



A22:把 A2 到 E21 範圍內 100 個計算指標的科目號合併起來,其中 A2 格子代表指標 1,B2 代表指標 2,依次類推;合併完後,執行結果如下圖:

undefined

B22:對 A22 指標中每一個科目號,分別利用月號 (月號為 2 位)*10000000000,加上當前科目號,形成指標的引數組集合。執行結果如下圖:

undefined

C22:對指標引數組集合進行合併,然後排序

undefined

A23:開啟預處理後的集檔案物件

A24:根據指標 C22 中構造的引數組集合與檔案中有序的 key 欄位進行比對,記錄返回成遊標

B24:從遊標中獲取記錄,返回所有科目號的查詢到的結果集,執行結果如下圖:

undefined

C24-D24:分別獲取結果集中的:key、累計金額彙總。

A25:按照 A22 中每個科目號的順序,在 B24 結果集中利用 key 列與當前科目號 + 月號構造的主鍵進行比對,然後返回位置序號,其中 pos() 函式中 @b 代表使用二分法查詢,效率更高,但要求被尋找序列是有序的。運算結果如下圖:

undefined

A26:利用 A25 每個成員座標位置的序號,在結果集 B24 中對比找到的累計金額彙總欄位進行求和,求和結果再按位置序號倒回到每個指標中,比如序號 1 返回的結果代表 A2 的引數查詢出來的指標 1 彙總結果,序號 2 返回的結果代表 B2 的引數查詢出來的指標 2 彙總結果。依次類推。即每個指標的彙總值計算完成。執行結果如下圖:

undefined

A27:返回結果集,供報表工具使用。

至此,利用一次遍歷,搞定所有事情,難題迎刃而解!

實測結果:報表從取數到展現整個環節大概需要1-2秒,其中指標計算部分用時不到1秒。

2.5 作為報表資料來源

對於報表的製作過程來說,並不需要做什麼改變,只需要把資料來源切換到集算器即可。假定報表工具是潤乾報表 V5:

首先,匯入 excel 表樣,建立資料集型別為集算器,選取已經做好的 dfx 指令碼;同時設定相應的查詢引數等。

然後,在報表的每個單元格里分別按順序取值,即可得到每個指標彙總結果;比如單元格 C5 的表示式寫法:=ds1.select(#1)(1),單元格 C6 的寫法:=ds1.select(#1)(2),……,報表單元格表示式從上往下,依次類推。樣例如下圖:

undefined

2.6 總結

在實際的報表開發過程中,當我們遇到問題,往往並不能一開始就想到最優的解決辦法。我們可以試著先用最簡單、最容易的辦法實現,然後再一步步進行優化;對比每種方案的存在的缺陷及改進後所帶來的效能提升,從而最終滿足業務需求。

本文中我們就採用了這種方式,逐步優化的步驟如下:

1、多次遍歷方案

2、一次遍歷方案

3、預先彙總方案,查詢部分在 2 的基礎上進行優化

4、有序計算方案

整個過程中,我們用到的集算器相關技術包括:遊標、管道、遍歷複用、資料外接、分組子集、跨行組計算、有序計算 / 查詢、二分法查詢、位置序號等

瞭解了這些概念並熟練掌握集算器相關的函式後,我們就可以寫出高效的程式碼,快速實現報表資料集的準備工作!

三 引入集算器後的報表系統結構

實際業務中,我們針對客戶提供的生產環境資料 (原資料表大概 6000 萬明細記錄),利用這種方案進行了 POC 實測。結果表明,原來需要 30-40 秒才能呈現的資產負債表,現在提高到 1-2 秒內。而且,集算器指令碼可以與報表模板一起管理,從而有效降低應用管理的複雜度。

使用集算器進行預處理計算後,形成的資料快取檔案,能夠很好的優化現有報表實現模式,有效解決大資料集報表運算慢的難題

原有模式和引入集算器後的報表系統結構對比如下圖所示:

 

undefined

根據報表的業務特點,通常具體的實現步驟如下:

1、集算器抽取來自資料來源的資料,根據報表的業務規則,取出需要的維度、過濾欄位、計算指標等明細記錄。

2、集算器對明細記錄進行預處理計算,生成報表需要的各類指標。

3、計算後的指標以資料快取檔案的方式存放,可以按照業務種類、模組關係、時間順序進行多級目錄管理,也可以和報表模板一起管理。

4、報表工具通過 JDBC 方式呼叫集算器,計算結果返回給報表工具呈現。

5、可以設定計劃任務定時執行,完成上述各項資料預處理動作。