【摘要】
主子表是數據庫最多見的關聯關係之一,最典型的包括合同和合同條款、訂單和訂單明細、保險保單和保單明細、銀行帳戶和帳戶流水、電商用戶和訂單、電信帳戶和計費清單或流量詳單。當主子表的數據量較大時,關聯計算的性能將急劇下降,在增長服務器負載的同時嚴重影響用戶體驗。做爲面向過程的結構化數據計算語言,集算器 SPL 可經過有序歸併的方法,顯著提高大主子表關聯計算的性能。 下面就來乾學院一探究竟:大主子表關聯的性能優化方法。html
所謂主子表關聯計算,就是針對主表的每條記錄,按關聯字段找到子表中對應的一批記錄。以訂單(主表)和訂單明細(子表)爲例,二者以訂單ID爲關聯字段。下圖顯示了關聯計算過程當中對主表中一條記錄的處理狀況,紅色箭頭表明沒找到對應記錄(不可關聯),綠色箭頭表明找到了對應記錄(可關聯):java
假設訂單(主表)有m條記錄,訂單明細(子表)有n條記錄,在不考慮優化算法時,主表中每一條記錄的關聯都須要遍歷子表,相應的時間複雜度爲O(n)。而主表一共有m條記錄,因此整個計算的複雜度就是O(m*n),顯然太高。雖然數據庫通常會採用hash方案來優化,但在數據量較大或較多表關聯時,仍然會面臨時難以並行、使用外存緩存數據的問題,性能依舊會急劇降低。linux
而對於集算器來講,針對大主子表關聯算法,能夠經過兩步來實現顯著優化:數據有序化、歸併關聯。程序員
l 數據有序化算法
對主表和子表,首先分別按照關聯字段排序,造成有序數據。數據庫
l 歸併關聯編程
首先在主表和子表上分別用指針指向第一條記錄,而後開始比對,對於主表的第一條記錄,若是子表遇到匹配的記錄,則表示能夠關聯,記錄後子表指針前移;若是遇到不匹配的記錄,表示主表第一條記錄的關聯計算完成,此時子表指針不動,主表指針下移一位,指向第二條記錄。以此類推……windows
優化後,單條記錄的關聯計算可用下圖示意:緩存
能夠看到,通過優化,主表中單條記錄的關聯只需比對部分數據,再也不須要遍歷子表。事實上,對主表全部記錄的關聯,纔會遍歷一次子表,也就是複雜度爲O(n)。再加上主表自己會遍歷一次,所以整個計算的複雜度就是O(m+n)。性能優化
這樣,通過集算器優化後,算法的時間複雜度變爲線性,並且再也不須要生成落地的中間數據,性能天然獲得大幅提高。
固然,須要注意的是,有序化自己也會耗費時間,所以這種優化方法不適合只作一次的關聯算法。但在實際業務中,關聯算法一般會反覆執行,這時有序化的開銷就是一次性的,徹底能夠忽略不計。
下面仍是以訂單和訂單明細爲例,說明集算器優化大主子表關聯的方法。
首先進行數據有序化(注意,這是一次性動做)。集算器腳本「數據有序化.dfx」以下:
A1鏈接Oracle數據源,A5關閉數據源。集算器可鏈接大部分經常使用數據源,包括數據庫、Excel、阿里雲、SAP等等。
A二、B2:用SQL語句分別取訂單和訂單明細,並按關聯字段排序。因爲數據量較大,沒法一次性讀入內存,所以這裏用到了遊標函數cursor。
A三、B3:分別建立組表文件「訂單.ctx」和「訂單明細.ctx」,用於存儲有序化以後的數據。這裏須要指定字段名,其中帶#號的字段是主鍵,。數據將按主鍵排序,且主鍵的值不可重複。
A4-B4:將遊標追加寫入組表文件。
其次,對於一般會反覆執行的關聯算法,能夠用集算器腳本「歸併關聯.dfx」實現以下:
A一、B1:讀入組表文件「訂單.ctx」和「訂單明細.ctx」。注意組表默認爲列式存儲,所以只需讀入後續計算須要的字段,從而大幅下降I/O。
A2:對有序遊標A一、B1進行歸併關聯,其中「主表」、「子表」是別名,方便後續引用,若是省略別名,後續能夠經過默認別名_一、_2引用。注意,函數joinx默認進行內關聯,可用選項@1指定左關聯,或者@f指定全關聯。若是有多個遊標都要與A1關聯,可用分號依次隔開。
A3:對關聯結果進行後續計算,例如彙總產品數量。事實上後續計算能夠支持任意算法,也不是本文的討論範圍了。
上面介紹了集算器SPL腳本的寫法,而在實際執行時,還須要部署集算器的運行環境。有兩種部署方式可供選擇:內嵌部署和獨立部署。
l 內嵌部署
內嵌部署時,集算器的用法相似內嵌數據庫,應用系統使用集算器驅動(JDBC)執行同一個JVM下的集算器腳本。
下面是Java調用「歸併關聯.dfx」的代碼
在上述JAVA代碼中,集算器腳本以文件的形式保存,調用語法相似存儲過程。而若是腳本很簡單,也能夠不保存腳本文件,直接書寫表達式,調用語法相似SQL,這時第5行能夠寫成:
這篇文章詳細介紹了JAVA 調用集算器的過程:http://doc.raqsoft.com.cn/esproc/tutorial/bjavady.html
除了使用Java代碼,也能夠經過報表訪問集算器,這時按照訪問通常數據庫的方法便可,具體可參考《讓Birt報表腳本數據源變得既簡單又強大》。
對於腳本「數據有序化.dfx」,能夠用一樣的方法執行。不過這個腳本一般只執行一次,因此也能夠直接在命令行中執行,windows用法以下:
Linux 下用法相似,能夠參考http://doc.raqsoft.com.cn/esproc/tutorial/minglinghang.html
l 獨立部署
獨立部署時,集算器的用法相似遠程數據庫,應用系統可使用集算器驅動(JDBC或ODBC驅動)訪問集算服務器。這種狀況下,應用系統和集算器服務器一般部署在不一樣的機器上。
例如集算服務器的IP地址爲192.168.0.2,端口號爲8281,那麼JAVA應用系統能夠經過以下代碼訪問:
關於集算服務器的部署和使用,詳細內容可參考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應寫成:
B1:一樣生成多線程遊標,並與A1的多線程遊標對齊。
A2-A3:歸併關聯,再執行後續算法。這兩步寫法上沒變化,但底層會自動進行多線程合併和二次計算,從而下降了程序員的編程難度。
在前面算法的基礎上,還能夠進一步提高計算性能,那就是以層次結構存儲數據,直接記錄關聯關係。
具體來講,先用「結構優化有序化.dfx」生成組表文件:
B4:在主表的基礎上附加子表,命名爲訂單明細。與主表不一樣的是,子表默認繼承了主表的主鍵,所以能夠省略訂單ID,只須要寫另外一個主鍵產品ID。這樣,2個表寫在了一個組表文件中,從而才能造成層次結構。
B5:向子表寫入數據。
此時,組表「多層訂單.ctx」將按層次結構存儲,邏輯示意圖以下:
能夠看到,每條主表記錄與對應的子表記錄,在邏輯上已經緊密相關,無需額外關聯,這樣即可大幅提升關聯算法的性能。
進行關聯計算時,使用如下腳本「結構優化歸併關聯.dfx」:
A一、B1:打開主表,以及附加在主表上的子表。
A二、B2:以多線程方式分別讀取主表和子表。須要注意的是,多層組表裏的實表之間自然具有相關性,所以無需特地指定子表和主表的分段關係,代碼比以前更清晰簡單。
A3,A4:歸併關聯並執行後續算法,這兩步沒變化。
前面的優化方式都基於庫表全量導出爲組表文件的狀況,但實際業務中數據庫表總會發生變化,所以須要考慮數據更新的問題,也就是要將變化的數據定時更新到組表文件中。
顯然,更新數據應選擇在無人查詢組表文件時進行,通常都是半夜或凌晨。而更新的頻率,則須要按照數據實時性要求來設定,例如天天一次或每週一次。至於更新的方式,須要按照數據的變化規律來考慮,最多見的是數據追加,有時也會遇到增刪改。
下面先看數據追加:
訂單和訂單明細天天都會產生新記錄,假設須要在天天凌晨2點將昨天新增的記錄追加到組表文件中。下圖顯示了2018/11/23新增記錄的狀況,注意,有些訂單(訂單ID:20001)並無對應的訂單明細:
把主子表追加到組表文件中的腳本 「追加組文件.dfx」以下:
A二、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取兩張表的數據,主要是爲了提升效率。因爲有些訂單並無對應的訂單明細,所以用訂單左關聯訂單明細,且將對應不上的訂單明細置空。計算結果以下:
A五、B5:拆出新增的主子表記錄,結果示例以下:
A6-B8:將主表和子表追加到組表文件中。
腳本寫完以後,還須要在天天的02:00:00定時執行,這可使用操做系統內置的任務調度。
在Windows下,創建以下的bat批處理文件,:
再使用windows內置的"計劃任務",定時執行批處理文件便可。
在linux下,創建以下的sh批處理文件,:
再使用crontab命令,定時執行批處理文件便可。
固然也可以使用圖形化工具定時執行腳本,好比Quartz。
須要注意的是,大多數狀況下,可以選擇無人使用組表文件的時候進行追加,但有些業務中組表文件全天都要使用,而有些項目對容錯要求更高,要求追加失敗時再次追加,這類項目就須要更加細緻的追加方法,詳情可參考《基於文件系統實現可追加的數據集市》。
除了追加這種主要的更新方式,業務中也會遇到增刪改都存在的狀況。
在這種狀況下,就須要知道哪些是刪除的記錄,哪些是修改或新增的記錄。若是條件容許,能夠在原表中新加「標記」字段,並將維護狀態記錄在該字段中。若是不方便修改原表,則應當建立對應的「維護日誌表」。例以下面兩張表,分別是訂單和訂單明細的維護日誌。
根據維護日誌更新組表文件,可以使用下面的腳本:
A二、B2:從數據庫查出應刪除的記錄
A三、B3:從數據查出應修改和新增的記錄
A五、B5:對組表進行刪除操做。
A六、B6:從組表進行修改新增操做。
A七、B7:清空維護日誌表,以便下次繼續更新數據。
經過定時追加,能保證組表文件與昨天的數據同步,從而實現T+1計算,但有時須要進行實時大主表關聯,即T+0計算。
對於T+0計算,須要將兩種不一樣的數據源進行混合計算,因爲SQL或SP的數據模型較爲封閉,所以難以實現混合計算,而使用集算器就很是簡單。
好比對組表文件定時追加後,數據庫當天又產生了以下新數據:
可以使用以下腳本實現T+0實時計算:
A1:算出當天的起始時間點,即2018-11-26 00:00:00。
A3:針對數據庫當天產生的新數據,進行關聯計算。因爲當天數據量較小,所以性能能夠接受。
A4-A7:針對組表文件歷史數據,進行高性能關聯計算。
A8:合併當天和歷史,並進行二次計算,以得到最終計算結果。其中符號|表示縱向合併,這是實現混合計算的關鍵。事實上,這種寫法也代表集算器支持任意數據源之間的混合計算,好比Excel與elasticSearch之間。
關於T+0計算更多的細節,可參考相關文章《實時報表 T+0 的實現方案》