1. 程式人生 > >優化 Join 運算的系列方法(2)

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

3 半記憶體時的外來鍵表

外來鍵指標化的前提是事實表和維表都可以裝入記憶體,但實際業務中涉及的資料量可能很大,那就不能採用這種方法了。

3.1 維表記憶體化

如果只是事實表很大,而維表仍然可以全部裝入記憶體,那麼仍然可以採用上面的外來鍵指標化方法處理,只要修改一下對事實表的訪問,使用遊標的方式取從集檔案裡分批取數進行處理即可。不過因為這種指標是在遊標取數時才臨時建立的,所以就不象全記憶體時那樣可以複用已經建立過的指標了。

我們仍然按照使用者級別和賣家信用等級彙總訂單數量,而訂單表太大無法匯入記憶體,那麼用集算器實現如下:

 

A

1

=file("訂單表")

2

[email protected]()

3

=file("使用者資訊表")[email protected]().keys(使用者編號).index()

4

=file("賣家資訊表")[email protected]().keys(賣家編號).index()

5

=A2.switch(使用者編號,A3:使用者編號;賣家編號,A4:賣家編號)

6

=A5.groups(使用者編號.VIP級別:使用者級別,賣家編號.信用等級:賣家等級;sum(訂單編號):訂單數)

這個實現跟外來鍵指標化的實現原理相同,只不過訂單表的資料沒有一次性匯入記憶體,而是通過遊標的方式訪問。由於事實表會不斷增長,所以事實表很大而維表較小會是實際業務中常見的情況。

這是個多外來鍵的例子。多層外來鍵的情況和單層外來鍵類似,只是在記憶體化某外來鍵表時,該表的外來鍵表也必須記憶體化,從而可以事先建立記憶體的外來鍵指標。臨時基於遊標建立的外來鍵關聯只會針對最下層的外來鍵表。

遊標也可以實現平行計算,上面的程式碼只要改成這樣:

 

A

1

=file("訂單表")[email protected](;4)

2

=file("使用者資訊表")[email protected]().keys(使用者編號).index()

3

=file("賣家資訊表")[email protected]().keys(賣家編號).index()

4

=A1.switch(使用者編號,A2:使用者編號;賣家編號,A3:賣家編號)

5

=A4.groups(使用者編號.VIP級別:使用者級別,賣家編號.信用等級:賣家等級;sum(訂單編號):訂單數)

把來自集檔案的訂單表資料分成4段遊標取出,在執行groups函式就會以並行的方式進行計算了。這裡之所以可以進行分段取數,是因為資料已經匯出到集檔案中了,如果資料仍然在資料庫中則無法做到這一點的,這也是我們為什麼要把資料匯出到集檔案的原因之一。

如果維表太大也無法裝入記憶體怎麼辦?這種情況就要使用叢集或者優化過的外存HASH JOIN技術了,後面的篇章中我們會詳細講解。

 

3.2 外來鍵序號化

外來鍵序號化的思路是,如果維表的主鍵是從1開始的自然數,那麼就可以用序號直接定位維表記錄,而不再需要計算和比對HASH值了。這可以看做是在外存實現了外來鍵指標化,從而進一步提升效能。按照外來鍵序號化思路,前面訂單表和使用者表的關聯處理可以改成這樣:

 

A

1

=file("使用者資訊表")[email protected]()

2

=file("訂單表")[email protected]()

3

=A2.switch(使用者編號,A1:#)

4

=A3.new(訂單編號, 使用者編號.使用者名稱:使用者名稱, 使用者編號.VIP級別:使用者級別, 下單時間)

A1,將客戶表全部匯入記憶體;

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")[email protected](A2,   使用者編號, NEW_ID)

4

=file("使用者資訊表")[email protected](A2, NEW_ID:使用者編號, 使用者名稱,聯絡手機,VIP級別)

A1從資料庫的使用者資訊表取出所有欄位,並增加一個用來儲存序號的欄位NEW_ID;

A2將NEW_ID賦值為從1開始的自然數;

A3是儲存舊的使用者編號和序號到集檔案;

A4用NEW_ID欄位值作為使用者編號欄位的值,其它欄位不改變,把資料儲存到使用者資訊表文件。

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

 

A

1

=file("OldKey_NewID")[email protected]()

2

=db.cursor("select * from 訂單表")

3

=A2.switch(使用者編號,A1:使用者編號)

4

=A3.run( 使用者編號.NEW_ID:使用者編號)

5

=file("新訂單表")[email protected](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("使用者資訊表")[email protected]().skip()

3

=A1.run(A2+#:NEW_ID)

4

=file("OldKey_NewID")[email protected](A3,   使用者編號, NEW_ID)

5

=file("使用者資訊表")[email protected](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")[email protected]()

3

=A1.switch(使用者編號,A2:使用者編號)

4

=A3.run( 使用者編號.NEW_ID:使用者編號)

5

=file("訂單表")[email protected](A4)

A1得到訂單表要追加的新資料的遊標,這裡是從資料庫取出2018年以來的訂單作為新資料;

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

A3把新資料遊標裡的使用者編號欄位根據對應表進行替換;

A4把替換後的使用者編號欄位的值做一個轉換;

A5使用迴圈方式從遊標取數,追加到訂單表文件,這個過程和使用者資訊表的追加是類似的。

上面是一個單外來鍵做序號化的例子,對多外來鍵的序號化處理也是一樣的,只是有多個維表要處理。如果是多層外來鍵,那麼上層的就沒有必要做序號化了,只要對最下層的維表做個序號化就可以了,因為上層已經全記憶體指標化了。

外來鍵序號化處理本質是優化了查詢外來鍵的方法,把外來鍵值作為序號直接去維表找記錄,所以經過外來鍵序號化的資料仍然可以使用平行計算,實現方式跟前面講的一樣,在此不再詳述。

 

4 同維表和主子表

在這裡我們把同維表和主子表兩種情況一起來分析,因為這兩種情況的提速手段是一樣的,那就是有序歸併。

4.1 有序歸併

我們先看簡單的情況,如果兩個表對關聯鍵都已經是有序的,那麼就可以直接使用歸併演算法來處理關聯。來看一個例子,

訂單表

訂單編號

使用者編號

賣家編號

下單日期

 

訂單明細表

訂單編號

商品編號

數量

金額

 

賣家資訊表

賣家編號

名稱

……

 

使用者資訊表

使用者編號

使用者名稱

……

 

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

 

A

1

=file("訂單表")[email protected]()

2

=file("訂單明細表")[email protected]()

3

[email protected](A1:訂單,訂單編號;A2:明細,訂單編號)

4

=A3.groups(訂單.賣家編號 :賣家編號; sum(明細.金額):總銷售額 )

A1將訂單表全部匯入記憶體。

A2將訂單明細表全部匯入記憶體。

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

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

集算器的join操作的結果與SQL不同,SQL裡join的結果是兩個表的欄位,而集算器join的結果是把兩個表的記錄作為結果欄位,所以做groups時的語法需要寫成“欄位.子欄位”這樣(類似“物件.屬性”),例如訪問賣家編號就要寫成“訂單.賣家編號”。

如果資料很大無法匯入記憶體,則可以使用遊標方式進行有序歸併。

 

A

1

=file("訂單表")[email protected]()

2

=file("訂單明細表"). [email protected]()

3

=joinx(A1:訂單,訂單編號;A2:明細,訂單編號)

4

=A3.groups(訂單.賣家編號:賣家編號; sum(明細.金額):總銷售額 )

注意,這裡進行有序歸併的前提是訂單表、訂單明細表已經是對訂單編號欄位有序的。

A1將訂單表通過遊標匯入;

A2將訂單明細表通過遊標匯入;

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

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

 

有序歸併還可以和遊標外來鍵一起使用,例如我們要計算消費總金額大於1000的使用者名稱:

 

A

1

=file("訂單表")[email protected]()

2

=file("訂單明細表"). [email protected]()

3

=file("使用者資訊表"). [email protected]()

4

[email protected](使用者編號, 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("訂單表")[email protected](A1;訂單編號)

3

=db.query("select * from 訂單明細表order by 訂單編號").cursor()

4

=file("訂單明細")[email protected](A1;訂單編號)

A1,從資料庫將訂單表通過遊標匯入,並且排序;

A2,將排序後的遊標資料寫入集檔案;

A3、A4同樣將資料庫的訂單明細表排序後寫入集檔案。

 

再來看看如果這兩個表又追加了新資料時該怎麼處理,我們僅以訂單表的追加為例:

 

A

1

=file("訂單表"). cursor @b()

2

=db.query("select * from 訂單表 where 下單日期>=’2018-01-01’ order by 訂單編號").cursor()

3

=[A1,A2].mergex(訂單編號)

4

=file("新訂單表")[email protected](A1;訂單編號)

A1,將訂單表通過遊標匯入;

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

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

A4,將歸併後的遊標資料寫入新的檔案。

後續使用時用新的檔案替換舊的訂單表文件,這樣就完成了新增資料和歷史資料的有序歸併,就可以按照有序的情況進行處理了。

新增資料和歷史資料的混合,是個有序歸併的過程,並不需要全部重新排序,只是把資料再讀寫一遍,時間成本並不高。

 

4.3 平行計算

如果資料量確實特別大,頻繁重寫的成本太高,這時可以每隔一個相對合適的週期才重寫所有資料,未到週期點時先把資料儲存到一個較小檔案,到了週期節點再把小檔案和歷史全檔案做歸併,具體的週期根據實際業務來設定。這樣就會有兩個檔案:歷史全檔案和週期內小檔案。可以使用多路遊標來一起訪問這兩個檔案。

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

當然,還可以保留以前每個月的檔案,作為歷史資料不再改動,然後使用多路遊標來訪問這多套資料,這樣效能可能會更好。這是以日期為例的情況,還可以根據其它的欄位來進行分段方案的設計,比如按地區等。

下面用每個月保留一個檔案的方法來舉例說明,先實現對當日新產生的資料的處理,仍然以訂單表為例:

 

A

1

=file("訂單表8月")[email protected]().cursor()

2

=db.query("select * from 訂單表 where 下單日期>=’2018-08-XX’ order by 訂單編號").cursor()

3

=[A1,A2].mergex(訂單編號)

4

=file("新訂單表8月")[email protected](A1;訂單編號)

A1,將8月份的訂單表月檔案通過遊標匯入;

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

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

A4,將歸併後的遊標資料寫入新的8月份的檔案。

處理後得到每個月份的訂單表集檔案,同理也可以得到每個月份的訂單明細表的集檔案。每個月份的兩個集檔案(訂單和明細)都是根據訂單時間產生的,對應的主子表記錄(訂單及其對應訂單明細)都在同一月份的檔案中,這樣就可以並行地針對每個月的資料做有序歸併來實現主子表連線,進一步提速。仍以統計賣家銷售總額為例,下面是具體實現:

 

A

1

=12.(file("訂單表"/~/"月")[email protected]())

2

=12.(file("訂單明細表"/~/"月")[email protected]())

3

=12.(joinx(A1(#):訂單,訂單編號;A2(#):明細 ,訂單編號))

4

=A3.mcursor()

5

=A4.groups(訂單.賣家編號 :賣家編號; sum(明細.金額):總銷售額 )

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

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

A3,使用joinx對12個月份資料進行歸併,得到遊標;

A4,合併為多路遊標;

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

閱讀下一頁