一、 原了解釋
所謂主子表關聯計算,就是針對主表的每條記錄,按關聯字段找到子表中對應的一批記錄。以訂單(主表)和訂單明細(子表)為例,兩者以訂單ID為關聯字段。下圖顯示了關聯計算過程中對主表中一條記錄的處理情況,紅色箭頭代表沒找到對應記錄(不可關聯),綠色箭頭代表找到了對應記錄(可關聯):
假設訂單(主表)有m條記錄,訂單明細(子表)有n條記錄,在不考慮優化算法時,主表中每一條記錄的關聯都需要周遊子表,相應的時間複雜度為O(n)。而主表一共有m條記錄,是以整個計算的複雜度就是O(m*n),顯然過高。雖然資料庫一般會采用hash方案來優化,但在資料量較大或較多表關聯時,仍然會面臨時難以并行、使用外存緩存資料的問題,性能依舊會急劇下降。
而對于集算器來說,針對大主子表關聯算法,可以通過兩步來實作顯著優化:資料有序化、歸并關聯。
l 資料有序化
對主表和子表,首先分别按照關聯字段排序,形成有序資料。
l 歸并關聯
首先在主表和子表上分别用指針指向第一條記錄,然後開始比對,對于主表的第一條記錄,如果子表遇到比對的記錄,則表示可以關聯,記錄後子表指針前移;如果遇到不比對的記錄,表示主表第一條記錄的關聯計算完成,此時子表指針不動,主表指針下移一位,指向第二條記錄。以此類推……
優化後,單條記錄的關聯計算可用下圖示意:
可以看到,經過優化,主表中單條記錄的關聯隻需比對部分資料,不再需要周遊子表。事實上,對主表所有記錄的關聯,才會周遊一次子表,也就是複雜度為O(n)。再加上主表本身會周遊一次,是以整個計算的複雜度就是O(m+n)。
這樣,經過集算器優化後,算法的時間複雜度變為線性,而且不再需要生成落地的中間資料,性能自然得到大幅提升。
當然,需要注意的是,有序化本身也會耗費時間,是以這種優化方法不适合隻做一次的關聯算法。但在實際業務中,關聯算法通常會反複執行,這時有序化的開銷就是一次性的,完全可以忽略不計。
二、 具體實作
下面還是以訂單和訂單明細為例,說明集算器優化大主子表關聯的方法。
首先進行資料有序化(注意,這是一次性動作)。集算器腳本“資料有序化.dfx”如下:

A1連接配接Oracle資料源,A5關閉資料源。集算器可連接配接大部分常用資料源,包括資料庫、Excel、阿裡雲、SAP等等。
A2、B2:用SQL語句分别取訂單和訂單明細,并按關聯字段排序。由于資料量較大,無法一次性讀入記憶體,是以這裡用到了遊标函數cursor。
A3、B3:分别建立組表檔案“訂單.ctx”和“訂單明細.ctx”,用于存儲有序化之後的資料。這裡需要指定字段名,其中帶#号的字段是主鍵,。資料将按主鍵排序,且主鍵的值不可重複。
A4-B4:将遊标追加寫入組表檔案。
其次,對于通常會反複執行的關聯算法,可以用集算器腳本“歸并關聯.dfx”實作如下:
A1、B1:讀入組表檔案“訂單.ctx”和“訂單明細.ctx”。注意組表預設為列式存儲,是以隻需讀入後續計算需要的字段,進而大幅降低I/O。
A2:對有序遊标A1、B1進行歸并關聯,其中“主表”、“子表”是别名,友善後續引用,如果省略别名,後續可以通過預設别名_1、_2引用。注意,函數joinx預設進行内關聯,可用選項@1指定左關聯,或者@f指定全關聯。如果有多個遊标都要與A1關聯,可用分号依次隔開。
A3:對關聯結果進行後續計算,例如彙總産品數量。事實上後續計算可以支援任意算法,也不是本文的讨論範圍了。
上面介紹了集算器SPL腳本的寫法,而在實際執行時,還需要部署集算器的運作環境。有兩種部署方式可供選擇:内嵌部署和獨立部署。
l 内嵌部署
内嵌部署時,集算器的用法類似内嵌資料庫,應用系統使用集算器驅動(JDBC)執行同一個JVM下的集算器腳本。
下面是Java調用“歸并關聯.dfx”的代碼
1. com.esproc.jdbc.InternalConnection con=null;
2. try {
3. Class.forName("com.esproc.jdbc.InternalDriver");
4. con =(com.esproc.jdbc.InternalConnection)DriverManager.getConnection("jdbc:esproc:local://");
5. ResultSet rs = con.executeQuery("call 歸并關聯()");
6. } catch (SQLException e){
7. out.println(e);
8. }finally{
9. if (con!=null) con.close();
10. }
在上述JAVA代碼中,集算器腳本以檔案的形式儲存,調用文法類似存儲過程。而如果腳本很簡單,也可以不儲存腳本檔案,直接書寫表達式,調用文法類似SQL,這時第5行可以寫成:
ResultSet rs = con.executeQuery("=joinx(file("訂單.ctx").create().cursor(訂單ID),訂單ID; file("訂單明細.ctx").create().cursor(訂單ID,數量),訂單ID).groups(;sum(_2.數量))");
這篇文章詳細介紹了JAVA調用集算器的過程:
http://doc.raqsoft.com.cn/esproc/tutorial/bjavady.html除了使用Java代碼,也可以通過報表通路集算器,這時按照通路一般資料庫的方法即可,具體可參考《讓Birt報表腳本資料源變得既簡單又強大》。
對于腳本“資料有序化.dfx”,可以用同樣的方法執行。不過這個腳本通常隻執行一次,是以也可以直接在指令行中執行,windows用法如下:
D:raqsoft64esProcbin>esprocx 資料有序化.dfx
Linux下用法類似,可以參考
http://doc.raqsoft.com.cn/esproc/tutorial/minglinghang.htmll 獨立部署
獨立部署時,集算器的用法類似遠端資料庫,應用系統可以使用集算器驅動(JDBC或ODBC驅動)通路集算伺服器。這種情況下,應用系統和集算器伺服器通常部署在不同的機器上。
例如集算伺服器的IP位址為192.168.0.2,端口号為8281,那麼JAVA應用系統可以通過如下代碼通路:
st = con.createStatement();
st.executeQuery("=callx("歸并關聯.dfx";["192.168.0.2:8281"])");
關于集算伺服器的部署和使用,詳細内容可參考
http://doc.raqsoft.com.cn/esproc/tutorial/fuwuqi.html關于JDBC和ODBC驅動的部署方法,可分别參考
http://doc.raqsoft.com.cn/esproc/tutorial/jdbcbushu.html http://doc.raqsoft.com.cn/esproc/tutorial/odbcbushu.html三、 多線程優化
前面介紹了基本的優化思路和實作方法,也就是針對資料本身的優化。而現實中伺服器都是多核心CPU,是以可以進一步對上述算法進行多線程優化。
多線程優化的原理,是将主表和子表各分為N段,使用N個線程同時進行關聯計算。
原理雖簡單,但真正實作的時候,就會發現很多難題:
l 分段效率
想把資料分為N段,就要先找到每一段的起始行号,如果用周遊的笨辦法數行号,顯然會白白消耗大量的I/O資源。
l 資料跨段
理論上,關聯字段值相同的子表記錄,應該分到同一段。如果對子表随意分段,很可能形成跨段的資料。
l 分段對齊
更進一步,理論上,子表的第i段資料,應該與主表的第i段資料對齊,也就是主子表關聯字段值的範圍應該一緻。如果兩者各自獨立分段,則可能導緻分段資料難以對齊。
l 二次計算
如果後續計算不涉及聚合,例如隻是過濾,那麼隻需将N個線程的計算結果直接合并。但如果後續計算涉及聚合,比如sum或分組彙總,那就要單獨再進行二次計算聚合。
好在集算器已經充分解決了上述難題,分段時不會耗費IO資源、關聯字段值相同的記錄會分在同一段、子表和主表會保持對齊、各種二次計算無需單獨實作。
具體來說,首先,資料有序化腳本需要做如下修改(紅色字型為修改部分):
B3:生成“訂單明細多線程.ctx”時,資料按“#訂單ID”分段。這将保證訂單ID相同的記錄,将來會分到同一段。
歸并關聯的腳本需修改如下:
A1:@m表示對資料分段,形成多線程遊标(也叫多路并行遊标)。其中線程數量是預設值,由系統參數“最大并行數”決定,也可手工修改。例如希望生成4線程遊标,A1應寫成:
=file("訂單多線程.ctx").create().cursor@m(訂單ID ;;4)
B1:同樣生成多線程遊标,并與A1的多線程遊标對齊。
A2-A3:歸并關聯,再執行後續算法。這兩步寫法上沒變化,但底層會自動進行多線程合并和二次計算,進而降低了程式員的程式設計難度。
四、 結構優化
在前面算法的基礎上,還可以進一步提升計算性能,那就是以層次結構存儲資料,直接記錄關聯關系。
具體來說,先用“結構優化有序化.dfx”生成組表檔案:
B4:在主表的基礎上附加子表,命名為訂單明細。與主表不同的是,子表預設繼承了主表的主鍵,是以可以省略訂單ID,隻需要寫另一個主鍵産品ID。這樣,2個表寫在了一個組表檔案中,進而才能形成層次結構。
B5:向子表寫入資料。
此時,組表“多層訂單.ctx”将按層次結構存儲,邏輯示意圖如下:
可以看到,每條主表記錄與對應的子表記錄,在邏輯上已經緊密相關,無需額外關聯,這樣便可大幅提高關聯算法的性能。
進行關聯計算時,使用以下腳本“結構優化歸并關聯.dfx”:
A1、B1:打開主表,以及附加在主表上的子表。
A2、B2:以多線程方式分别讀取主表和子表。需要注意的是,多層組表裡的實表之間天然具備相關性,是以無需特意指定子表和主表的分段關系,代碼比之前更清晰簡單。
A3,A4:歸并關聯并執行後續算法,這兩步沒變化。
五、 資料更新
前面的優化方式都基于庫表全量導出為組表檔案的情況,但實際業務中資料庫表總會發生變化,是以需要考慮資料更新的問題,也就是要将變化的資料定時更新到組表檔案中。
顯然,更新資料應選擇在無人查詢組表檔案時進行,一般都是半夜或淩晨。而更新的頻率,則需要按照資料實時性要求來設定,例如每天一次或每周一次。至于更新的方式,需要按照資料的變化規律來考慮,最常見的是資料追加,有時也會遇到增删改。
下面先看資料追加:
訂單和訂單明細每天都會産生新記錄,假設需要在每天淩晨2點将昨天新增的記錄追加到組表檔案中。下圖顯示了2018/11/23新增記錄的情況,注意,有些訂單(訂單ID:20001)并沒有對應的訂單明細:
A2、B2:計算昨天的起止時間,以便查詢新增資料。函數now擷取目前時間點,理論上應該是2018-11-24 02:00:00。A2是昨天的起始時間點,即2018-11-22 00:00:00。B2是終止時間點,即2018-11-23 00:00:00。之是以在集算器中計算起止時間,主要是為了增加可讀性和移植性。實際上也可以在SQL中計算。
A4:取出新增的主表和子表記錄。這裡用一句SQL取兩張表的資料,主要是為了提高效率。由于有些訂單并沒有對應的訂單明細,是以用訂單左關聯訂單明細,且将對應不上的訂單明細置空。計算結果如下:
A6-B8:将主表和子表追加到組表檔案中。
腳本寫完之後,還需要在每天的02:00:00定時執行,這可以使用作業系統内置的任務排程。
在Windows下,建立如下的bat批處理檔案,:
"D:raqsoft64esProcbinesprocx.exe" 追加組檔案.dfx
再使用windows内置的"計劃任務",定時執行批處理檔案即可。
在linux下,建立如下的sh批處理檔案,:
/raqsoft/esProc/bin/esprocx.sh synclastday.dfx
再使用crontab指令,定時執行批處理檔案即可。
當然也可使用圖形化工具定時執行腳本,比如Quartz。
需要注意的是,大多數情況下,能夠選擇無人使用組表檔案的時候進行追加,但有些業務中組表檔案全天都要使用,而有些項目對容錯要求更高,要求追加失敗時再次追加,這類項目就需要更加細緻的追加方法,詳情可參考《基于檔案系統實作可追加的資料集市》。
除了追加這種主要的更新方式,業務中也會遇到增删改都存在的情況。
在這種情況下,就需要知道哪些是删除的記錄,哪些是修改或新增的記錄。如果條件允許,可以在原表中新加“标記”字段,并将維護狀态記錄在該字段中。如果不友善修改原表,則應當建立對應的“維護日志表”。例如下面兩張表,分别是訂單和訂單明細的維護日志。
A2、B2:從資料庫查出應删除的記錄
A3、B3:從資料查出應修改和新增的記錄
A5、B5:對組表進行删除操作。
A6、B6:從組表進行修改新增操作。
A7、B7:清空維護日志表,以便下次繼續更新資料。
六、 T+0實時計算
通過定時追加,能保證組表檔案與昨天的資料同步,進而實作T+1計算,但有時需要進行實時大主表關聯,即T+0計算。
對于T+0計算,需要将兩種不同的資料源進行混合計算,由于SQL或SP的資料模型較為封閉,是以難以實作混合計算,而使用集算器就非常簡單。
比如對組表檔案定時追加後,資料庫當天又産生了如下新資料:
A1:算出當天的起始時間點,即2018-11-26 00:00:00。
A3:針對資料庫當天産生的新資料,進行關聯計算。由于當天資料量較小,是以性能可以接受。
A4-A7:針對組表檔案曆史資料,進行高性能關聯計算。
A8:合并當天和曆史,并進行二次計算,以獲得最終計算結果。其中符号|表示縱向合并,這是實作混合計算的關鍵。事實上,這種寫法也表明集算器支援任意資料源之間的混合計算,比如Excel與elasticSearch之間。
關于T+0計算更多的細節,可參考相關文章《實時報表 T+0 的實作方案》