優化 Join 運算的系列方法(2)

3 半內存時的外鍵表

外鍵指針化的前提是事實表和維表均可以裝入內存,但實際業務中涉及的數據量可能很大,那就不能採用這種方法了。算法

3.1 維表內存化

若是隻是事實表很大,而維表仍然能夠所有裝入內存,那麼仍然能夠採用上面的外鍵指針化方法處理,只要修改一下對事實表的訪問,使用遊標的方式取從集文件裏分批取數進行處理便可。不過由於這種指針是在遊標取數時才臨時創建的,因此就不象全內存時那樣能夠複用已經創建過的指針了。數據庫

咱們仍然按照用戶級別和賣家信用等級彙總訂單數量,而訂單表太大沒法導入內存,那麼用集算器實現以下:segmentfault

A
1 =file("訂單表")
2 =A1.cursor@b()
3 =file("用戶信息表").import@b().keys(用戶編號).index()
4 =file("賣家信息表").import@b().keys(賣家編號).index()
5 =A2.switch(用戶編號,A3:用戶編號;賣家編號,A4:賣家編號)
6 =A5.groups(用戶編號.VIP級別:用戶級別,賣家編號.信用等級:賣家等級;sum(訂單編號):訂單數)

這個實現跟外鍵指針化的實現原理相同,只不過訂單表的數據沒有一次性導入內存,而是經過遊標的方式訪問。因爲事實表會不斷增加,因此事實表很大而維表較小會是實際業務中常見的狀況。性能優化

這是個多外鍵的例子。多層外鍵的狀況和單層外鍵相似,只是在內存化某外鍵表時,該表的外鍵表也必須內存化,從而能夠事先創建內存的外鍵指針。臨時基於遊標創建的外鍵關聯只會針對最下層的外鍵表。函數

遊標也能夠實現並行計算,上面的代碼只要改爲這樣:性能

A
1 =file("訂單表").cursor@bm(;4)
2 =file("用戶信息表").import@b().keys(用戶編號).index()
3 =file("賣家信息表").import@b().keys(賣家編號).index()
4 =A1.switch(用戶編號,A2:用戶編號;賣家編號,A3:賣家編號)
5 =A4.groups(用戶編號.VIP級別:用戶級別,賣家編號.信用等級:賣家等級;sum(訂單編號):訂單數)

把來自集文件的訂單表數據分紅4段遊標取出,在執行groups函數就會以並行的方式進行計算了。這裏之因此能夠進行分段取數,是由於數據已經導出到集文件中了,若是數據仍然在數據庫中則沒法作到這一點的,這也是咱們爲何要把數據導出到集文件的緣由之一。優化

若是維表太大也沒法裝入內存怎麼辦?這種狀況就要使用集羣或者優化過的外存HASH JOIN技術了,後面的篇章中咱們會詳細講解。設計

3.2 外鍵序號化

外鍵序號化的思路是,若是維表的主鍵是從1開始的天然數,那麼就能夠用序號直接定位維表記錄,而再也不須要計算和比對HASH值了。這能夠看作是在外存實現了外鍵指針化,從而進一步提高性能。按照外鍵序號化思路,前面訂單表和用戶表的關聯處理能夠改爲這樣:指針

A
1 =file("用戶信息表").import@b()
2 =file("訂單表").cursor@b()
3 =A2.switch(用戶編號,A1:#)
4 =A3.new(訂單編號, 用戶編號.用戶名:用戶名, 用戶編號.VIP級別:用戶級別, 下單時間)

A1,將客戶表所有導入內存;code

A2,將訂單表使用遊標導入;

A3,在A2訂單表中把用戶編號的值做爲序號,用這個序號去用戶信息表找相應的記錄,創建關聯;

A4,經過外鍵屬性化的方式,將外鍵表字段做爲用戶名、用戶級別屬性使用。

3.3 序號化準備

但維表的主鍵不必定是序號值,那麼就沒法直接使用外鍵序號化進行性能優化。這時,能夠把維表的主鍵轉換成序號後再使用外鍵序號化。處理的步驟是這樣的:

1)新建一個鍵值-序號對應表,保存維表的鍵值和天然序號的對應關係;

2)把維表的鍵值替換爲天然序號,獲得一個新的維表文件;

3)把事實表裏的外鍵值修改成序號,修改的依據是鍵值-序號對應表,修改後獲得一個新的事實表;

這樣就獲得了新的維表和事實表文件,舊的表文件也能夠刪除了。

若是維表增長了新數據,那麼就按照以下步驟處理:

1)先追加鍵值-序號對應表;

2)再把新數據追加到新的維表,追加時依據鍵值-序號對應表;

3)最後追加事實表,追加時依據鍵值-序號對應表;

當完成了外鍵的序號化之後就可使用外鍵序號化的方式來提升性能了。序號化這種方法適用於維表基本不變的狀況,事實表數據則能夠不斷追加。

下面仍以訂單表、用戶信息表爲例來講明一個序號化的具體實現:

1)新建一個用戶信息表的鍵值-序號對應表,保存到集文件中,同時生成一個用戶信息表文件;

A
1 =db.query("select *,0 AS NEW_ID from 用戶信息表 order by 用戶編號")
2 =A1.run(#:NEW_ID)
3 =file("OldKey_NewID").export@b(A2,   用戶編號, NEW_ID)
4 =file("用戶信息表").export@b(A2, NEW_ID:用戶編號, 用戶名,聯繫手機,VIP級別)

A1從數據庫的用戶信息表取出全部字段,並增長一個用來保存序號的字段NEW_ID;

A2將NEW_ID賦值爲從1開始的天然數;

A3是保存舊的用戶編號和序號到集文件;

A4用NEW_ID字段值做爲用戶編號字段的值,其它字段不改變,把數據保存到用戶信息表文件。

2)根據訂單表,獲得新的訂單表;

A
1 =file("OldKey_NewID").import@b()
2 =db.cursor("select * from 訂單表")
3 =A2.switch(用戶編號,A1:用戶編號)
4 =A3.run( 用戶編號.NEW_ID:用戶編號)
5 =file("新訂單表").export@ba(A4)

A1把對應關係表導入內存;

A2用遊標從訂單表取出數據;

A3把訂單表裏的用戶編號字段根據對應表進行替換;

A4把替換後的用戶編號字段的值作一個轉換(A3獲得的用戶編號字段值是記錄類型,因此在A4轉變爲字段);

A5把遊標數據導出到新訂單表文件裏(實際中可能要分屢次導出);

經過這兩步,就能夠完成對數據庫裏已有數據的序號化,並導出到用戶信息表、訂單表這兩個集文件,同時還獲得了一個鍵值-序號對應表文件,命名爲OldKey_NewID。

前面提到過,序號化適用於維表數據基本不變的狀況,若是維表變化了,那就須要重造這些數據後再使用序號化。不過,若是可以明確知道事實表和維表上新追加的數據(例如經過時間等條件),那麼也能夠用下面的辦法來實現。

1)先追加用戶信息表和鍵值-序號對應文件;

A
1 =db.query("select *,0 AS NEW_ID from 用戶信息表 where 註冊時間>’2018-01-01’ order by 用戶編號")
2 =file("用戶信息表").cursor@b().skip()
3 =A1.run(A2+#:NEW_ID)
4 =file("OldKey_NewID").export@ab(A3,   用戶編號, NEW_ID)
5 =file("用戶信息表").export@ab(A3, NEW_ID:用戶編號, 用戶名,聯繫手機,VIP級別)

A1獲得用戶信息表要追加的新數據,這裏是從數據庫裏取2018年以來新註冊的用戶數據;

A2獲得用戶信息表已有記錄條數;

A3填寫新數據裏的NEW_ID值,從A2開始繼續計數;

A4把用戶編號和序號追加到鍵值-序號對應的文件;

A5追加新數據到用戶信息表文件。

3)追加訂單表;

A
1 =db.query("select * from訂單表 where 下單時間>=’2018-01-01’ order by 訂單編號")
2 =file("OldKey_NewID").cursor@b()
3 =A1.switch(用戶編號,A2:用戶編號)
4 =A3.run( 用戶編號.NEW_ID:用戶編號)
5 =file("訂單表").export@ba(A4)

A1獲得訂單表要追加的新數據的遊標,這裏是從數據庫取出2018年以來的訂單做爲新數據;

A2是獲得鍵值序號的對應表;

A3把新數據遊標裏的用戶編號字段根據對應表進行替換;

A4把替換後的用戶編號字段的值作一個轉換;

A5使用循環方式從遊標取數,追加到訂單表文件,這個過程和用戶信息表的追加是相似的。

上面是一個單外鍵作序號化的例子,對多外鍵的序號化處理也是同樣的,只是有多個維表要處理。若是是多層外鍵,那麼上層的就沒有必要作序號化了,只要對最下層的維表作個序號化就能夠了,由於上層已經全內存指針化了。

外鍵序號化處理本質是優化了查找外鍵的方法,把外鍵值做爲序號直接去維表找記錄,因此通過外鍵序號化的數據仍然可使用並行計算,實現方式跟前面講的同樣,在此再也不詳述。

4 同維表和主子表

在這裏咱們把同維表和主子表兩種狀況一塊兒來分析,由於這兩種狀況的提速手段是同樣的,那就是有序歸併。

4.1 有序歸併

咱們先看簡單的狀況,若是兩個表對關聯鍵都已是有序的,那麼就能夠直接使用歸併算法來處理關聯。來看一個例子,

訂單表
訂單編號
用戶編號
賣家編號
下單日期
訂單明細表
訂單編號
商品編號
數量
金額
賣家信息表
賣家編號
名稱
……
用戶信息表
用戶編號
用戶名
……

此時訂單表是主表,訂單明細表是子表,這是一個典型的一對多的狀況,如今要查詢訂單及其明細,那麼就要把兩個表按照訂單編號字段進行關聯。先來看一下數據量不大時的例子,計算目標是彙總每一個賣家的銷售額:

A
1 =file("訂單表").import@b()
2 =file("訂單明細表").import@b()
3 =join@m(A1:訂單,訂單編號;A2:明細,訂單編號)
4 =A3.groups(訂單.賣家編號 :賣家編號; sum(明細.金額):總銷售額 )

A1將訂單表所有導入內存。

A2將訂單明細表所有導入內存。

A3經過有序歸併算法(@m選項)對兩個表按照訂單編號關聯。

A4對join的結果進行分組彙總。

集算器的join操做的結果與SQL不一樣,SQL裏join的結果是兩個表的字段,而集算器join的結果是把兩個表的記錄做爲結果字段,因此作groups時的語法須要寫成「字段.子字段」這樣(相似「對象.屬性」),例如訪問賣家編號就要寫成「訂單.賣家編號」。

若是數據很大沒法導入內存,則可使用遊標方式進行有序歸併。

A
1 =file("訂單表").cursor@b()
2 =file("訂單明細表"). cursor@b()
3 =joinx(A1:訂單,訂單編號;A2:明細,訂單編號)
4 =A3.groups(訂單.賣家編號:賣家編號; sum(明細.金額):總銷售額 )

注意,這裏進行有序歸併的前提是訂單表、訂單明細表已是對訂單編號字段有序的。

A1將訂單表經過遊標導入;

A2將訂單明細表經過遊標導入;

A3經過有序歸併算法對兩個遊標按照訂單編號關聯;

A4對joinx的結果進行分組彙總。一樣地,joinx的結果的字段也是記錄,因此在groups時對賣家編號的訪問語法就變成了訂單.賣家編號,對金額的訪問語法就成了明細.金額。

有序歸併還能夠和遊標外鍵一塊兒使用,例如咱們要計算消費總金額大於1000的用戶名:

A
1 =file("訂單表").cursor@b()
2 =file("訂單明細表"). cursor@b()
3 =file("用戶信息表"). import@b()
4 =A1.switch@i(用戶編號, A3:用戶編號)
5 =joinx(A4:訂單,訂單編號;A2:明細,訂單編號)
6 =A5.groups(訂單.用戶編號.用戶名; sum(明細.金額):總額) .select(總額>1000)

A1將訂單表經過遊標導入;

A2將訂單明細表經過遊標導入;

A3將用戶信息表導入內存;

A4使用用戶編號字段和用戶信息表作外鍵關聯;

A5經過有序歸併算法對兩個遊標按照訂單編號關聯;

A6 經過用戶名字段(訂單.用戶編號.用戶名)進行分組彙總,並選出總額大於1000的。

4.2 有序歸併的數據準備

不過,若是數據事先沒有按主鍵有序呢?那麼就須要事先進行排序。同維表和主子表能夠在數據準備階段就作好排序,這是由於對於同維表或主子表的關聯,用到的字段都是那一個(一組),即主鍵(的部分);而對於外鍵表,事實表有可能要跟多個維表作關聯,每次關聯的字段均可能是不一樣的,而一個表是不可能同時對全部的外鍵都有序的。

所以,對於數據庫中並不保證次序的原始數據,咱們能夠在作數據外置時同時進行排序。本節將描述如何排序以及排序後如何有序地更新數據。

先看原始數據的導出。若是要排序的同維表或主子表的數據源都是數據庫,那麼就用數據庫排序。若是數據源不是數據庫,那麼可使用集算器的sortx函數進行排序。排序後用export函數保存到一個新的文件裏。若是要採用分段並行,還要注意在導出的時候加上選項@z。處理流程是這樣的:

A
1 =db.query("select * from 訂單表 order by 訂單編號").cursor()
2 =file("訂單表").export@z(A1;訂單編號)
3 =db.query("select * from 訂單明細表order by 訂單編號").cursor()
4 =file("訂單明細").export@z(A1;訂單編號)

A1,從數據庫將訂單表經過遊標導入,而且排序;

A2,將排序後的遊標數據寫入集文件;

A三、A4一樣將數據庫的訂單明細表排序後寫入集文件。

再來看看若是這兩個表又追加了新數據時該怎麼處理,咱們僅以訂單表的追加爲例:

A
1 =file("訂單表"). cursor @b()
2 =db.query("select * from 訂單表 where 下單日期>=’2018-01-01’ order by 訂單編號").cursor()
3 =[A1,A2].mergex(訂單編號)
4 =file("新訂單表").export@z(A1;訂單編號)

A1,將訂單表經過遊標導入;

A2,從數據庫中將2018年以來產生的新數據取出;

A3,兩個遊標按照訂單編號字段進行有序歸併;

A4,將歸併後的遊標數據寫入新的文件。

後續使用時用新的文件替換舊的訂單表文件,這樣就完成了新增數據和歷史數據的有序歸併,就能夠按照有序的狀況進行處理了。

新增數據和歷史數據的混合,是個有序歸併的過程,並不須要所有從新排序,只是把數據再讀寫一遍,時間成本並不高。

4.3 並行計算

若是數據量確實特別大,頻繁重寫的成本過高,這時能夠每隔一個相對合適的週期才重寫全部數據,未到週期點時先把數據保存到一個較小文件,到了週期節點再把小文件和歷史全文件作歸併,具體的週期根據實際業務來設定。這樣就會有兩個文件:歷史全文件和週期內小文件。可使用多路遊標來一塊兒訪問這兩個文件。

例如,能夠計劃每隔一個月才重寫全部數據,天天追加的數據合在一個當月的小文件中,在月中只用這個小文件和當日數據歸併,到了月末才把當月文件和歷史全文件所有歸併,這樣就可以減小全量歸併的次數,減小總的處理時間。這種方式下兩個文件就是歷史文件和當月文件。

固然,還能夠保留之前每月的文件,做爲歷史數據再也不改動,而後使用多路遊標來訪問這多套數據,這樣性能可能會更好。這是以日期爲例的狀況,還能夠根據其它的字段來進行分段方案的設計,好比按地區等。

下面用每月保留一個文件的方法來舉例說明,先實現對當日新產生的數據的處理,仍然以訂單表爲例:

A
1 =file("訂單表8月").import@b().cursor()
2 =db.query("select * from 訂單表 where 下單日期>=’2018-08-XX’ order by 訂單編號").cursor()
3 =[A1,A2].mergex(訂單編號)
4 =file("新訂單表8月").export@z(A1;訂單編號)

A1,將8月份的訂單表月文件經過遊標導入;

A2,從數據庫中將2018年8月某一天以來產生的新數據取出;

A3,兩個遊標按照訂單編號字段進行有序歸併;

A4,將歸併後的遊標數據寫入新的8月份的文件。

處理後獲得每月份的訂單表集文件,同理也能夠獲得每月份的訂單明細表的集文件。每月份的兩個集文件(訂單和明細)都是根據訂單時間產生的,對應的主子表記錄(訂單及其對應訂單明細)都在同一月份的文件中,這樣就能夠並行地針對每月的數據作有序歸併來實現主子錶鏈接,進一步提速。仍以統計賣家銷售總額爲例,下面是具體實現:

A
1 =12.(file("訂單表"/~/"月").cursor@b())
2 =12.(file("訂單明細表"/~/"月").cursor@b())
3 =12.(joinx(A1(#):訂單,訂單編號;A2(#):明細 ,訂單編號))
4 =A3.mcursor()
5 =A4.groups(訂單.賣家編號 :賣家編號; sum(明細.金額):總銷售額 )

A1,建立12個月份的訂單表遊標;

A2,建立12個月份的訂單明細表遊標;

A3,使用joinx對12個月份數據進行歸併,獲得遊標;

A4,合併爲多路遊標;

A5,對多路遊標進行分組彙總。

閱讀下一頁

相關文章
相關標籤/搜索