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

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

JOIN是關係資料庫中常用運算,用於把多個表進行關聯,關聯條件一般是判斷某個關聯欄位的值是否相等。隨著關聯表的增多或者關聯條件越來越複雜,無論理解查詢含義、實現查詢語句,還是在查詢的效能方面,可以說JOIN都是最具挑戰的SQL運算,沒有之一

 

特別是JOIN的效能,一直是個老大難問題。下面我們將基於資料計算中介軟體(DCM)——集算器,來提供一些提升運算效能的方法。

當然,我們不是介紹如何在寫SQL語句時怎麼寫JOIN,也就是我們假設已經對查詢需求有了正確的理解並且能正確地實現SQL。這種情況下,要提升效能,就必須從最基本的提升資料IO配合演算法及並行等手段做起。正因如此,如果資料仍然儲存在資料庫中,那也沒什麼好辦法提速,因為資料庫的IO效率很低,又幾乎無法並行,即使把運算寫得再精巧也無濟於事。所以,要提高效能,一定要把資料搬出資料庫,我們下面的討論都是基於這個思路,而集算器正是實現這個思路的利器,甚至神器!

把資料表搬出資料庫儲存到集算器的集檔案中很簡單,只要用兩行程式碼:

 

A

1

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

2

=file("Order.btx")[email protected](A1)

這兩行程式碼把資料庫裡訂單表的資料匯出到集檔案Order.btx。

因為資料庫IO效能不佳,而且資料量也可能很大,所以這個“搬家”動作可能時間也不短,但還好是一次性的。後面我們的計算都將從集檔案中取數。

1 判斷 JOIN 的型別

在將資料搬出資料庫後,我們需要首先判斷JOIN的型別,然後才能採取有針對性的優化措施。

JOIN運算大家都很熟悉,按照SQL的語法定義劃分,包括INNER JOIN(內連線)、LEFT JOIN(左連線)、RIGHT JOIN(右連線)、FULL JOIN(全連線)幾個型別,這是根據在運算中對空值的處理規則進行劃分的。而我們的分析和優化,則會從更貼近需求的語義角度出發,根據各個表的主鍵參與關聯的情況進行劃分,總體來說有這麼三種:外來鍵表、同維表、主子表。

外來鍵表

當表A的某些欄位與表B的主鍵關聯,B稱為A的外來鍵表,A表中與B表主鍵關聯的欄位稱為A指向B的外來鍵。此時A表也稱為事實表,B表也稱為維表。

表A:Order訂單表

ID

訂單編號

CustomerID

客戶編號

SellerID

銷售編號

OrderDate

訂購日期

Amount

訂單金額

表B:Customer客戶表

ID

客戶編號

Name

客戶名稱

Area

所在區域

   

 

表C:seller銷售人員表

ID

員工編號

Name

姓名

Age

年齡

……

 

 

這是一個典型的例子,訂單表的客戶編號與客戶表的主鍵客戶編號進行關聯,此時A指向B是多對一的關係,即A表有可能存在多條記錄指向B表的同一條記錄。

這種情況,我們可以把外來鍵欄位(例子中的“CustomerID”)的值理解成指向外來鍵表中對應記錄的“指標”,而外來鍵表中對應的記錄就可以理解成一個物件,而外來鍵表的欄位就可以理解為物件的屬性, “指標”的作用只是用於找到外來鍵表中對應那條記錄。例子中對錶A和表B做關聯,一定是想獲得某些訂單的客戶的姓名或所在區域等詳細資訊,這樣,如果能寫成 customerID.name 和customerID.area就會更容易理解,這種語法在集算器中也得到了完美的支援。

同時,表A還可以有多個外來鍵表,例如表A的銷售編號(SellerID)可以指向一個銷售人員資訊表C,從而獲得該訂單銷售人員的屬性資訊。

 

同維表

表A的主鍵與表B的主鍵關聯,A和B相互稱為同維表。同維表是一對一的關係,JOIN、LEFT JOIN和FULL JOIN的情況都會有,例如:員工表和經理表。

 

表A:employee員工表

ID

員工編號

Name

姓名

Salary

工資

 

表B:manager客戶表

ID

編號

Allowance

補貼

……

 

這兩個表的主鍵都是員工編號ID,也就是經理也是員工之一,不過因為經理比普通員工多了一些屬性,所以需要另用一個經理表來儲存。對於這種一對一的情況,邏輯上可以簡單地看成一個表來對待。同維表JOIN時兩個表都是按主鍵關聯,相應記錄是唯一對應的。

 

主子表

表A的主鍵與表B的部分主鍵關聯,A稱為主表,B稱為子表。主子表是一對多的關係,只有JOIN和LEFT JOIN,不會有FULL JOIN,如:訂單和訂單明細。

 

表A:Order訂單表

ID

訂單編號

CustomerID

客戶編號

OrderDate

訂購日期

……

 

 

表B:OrderDetail訂單明細表

ID

訂單編號

NO

訂單序號

Product

訂購產品

Price

價格

……

 

 

表A的主鍵是ID,表B的主鍵是ID和NO,表A裡的一條記錄會對應表B裡的多條記錄。此時,可以把訂單明細表裡的相關記錄看成是訂單表的一條記錄的屬性,該屬性的取值是一個集合,而且常常需要使用聚合運算把集合值計算成單值。例如查詢每個訂單的總金額,可以描述為:

        SELECT ID, SUM(OrderDetail.Price) FROM Order

顯然,主子表關係是不對等的,而且從兩個方向的引用都有意義。從主表引用子表的情況就是通過聚合運算得到一個單值,而從子表引用主表則和外來鍵表類似。

 

那麼,這樣劃分三種JOIN運算,外來鍵表、同維表、主子表,有什麼用處呢?當然是為了優化效能!對於需要優化的JOIN運算,在準確判斷是哪種型別基礎上,後面的優化才會更加有效。另外,有必要說明一下,這裡提到的表A和表B不要求必須是一個實體表,也可能是一個子查詢產生的“邏輯表”。

下面我們就開始針對這三種類型以及實際的業務情況進行分析和提速。

 

2 全記憶體時的外來鍵表

如果所有參與運算的資料都能裝入記憶體,那麼就可以使用“外來鍵指標化”技術來實現外來鍵式JOIN運算的優化。

 

2.1 單個外來鍵

以上面的訂單表和客戶表為例,要查詢每一筆訂單的客戶名稱和所在地區:

我們需要查詢所有訂單的訂單編號、使用者名稱、使用者級別和下單時間, SQL是這麼寫的:

SELECT 訂單編號,使用者名稱,VIP級別,下單時間 FROM 訂單表,使用者資訊表 WHERE 訂單表.使用者編號 = 使用者資訊表.使用者編號

用集算器實現則是這樣:

 

A

1

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

2

=A1.keys(使用者編號)

3

=A1.index()

4

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

5

=A4.switch(使用者編號,A3:使用者編號)

6

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

A1從集檔案中查詢使用者資料;

A2,設定使用者資訊表的鍵為使用者編號;

A3,以使用者編碼欄位建立索引;

A4,從集檔案中查詢訂單資料;

A5,關聯,在A4訂單表的使用者編碼欄位上建立指向使用者資訊表記錄的指標;

A6,外來鍵指標化之後,將外來鍵表字段作為使用者名稱、使用者級別屬性使用。

實際有效運算的也就是A5和A6這兩格,別的都是資料準備。

 

再來看一個例子,這次需要計算各個VIP級別使用者的訂單的總數,SQL是這麼寫的:

SELECT VIP級別,count(訂單編號) 訂單數 FROM 訂單表,使用者資訊表 WHERE 訂單表.使用者編號 = 使用者資訊表.使用者編號 GROUP BY VIP級別

使用集算器則是這樣的:

 

A

1

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

2

=A1.keys(使用者編號)

3

=A1.index()

4

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

5

=A4.switch(使用者編號,A3:使用者編號)

6

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

7

=A5.groups(使用者編號.VIP級別; count(訂單編號):訂單數)

這個計算跟上一個例子的處理步驟大部分都一樣,只是在上一個例子的計算後接著再執行一下A7,對關聯的結果進行彙總,能這麼做是因為外來鍵的指標關聯在上一次計算裡已經完成,這裡可以對A5的結果進行復用。實際使用中這兩個計算放在一個DFX檔案裡執行,所以整個過程只需要進行一次關聯。

這也是集算器的另一大特點,可以對中間計算結果進行復用,從而提高整體查詢效能。複用的次數越多,效能的優化就越明顯。這一點在SQL中就做不到,兩個查詢要執行兩次SQL語句,每次執行都要做一次關聯,整體效能自然就差了。

 

此外,還可能存在多欄位外來鍵的情況,事實表的多個欄位關聯到一個維表,這種情況略有些複雜,在以後的篇章中再做詳細介紹。

2.2 一層多個外來鍵

下面再看一個多外來鍵的例子,假設資料庫中有訂單表、使用者資訊表、賣家資訊表三個表:

我們需要按照使用者級別和賣家信用等級來彙總訂單數量,SQL是這麼寫的:

SELECT VIP級別 使用者級別, 信用等級 賣家等級,count(訂單編號) 訂單數

FROM 訂單表,使用者資訊表,賣家資訊表

WHERE 訂單表.使用者編號 = 使用者資訊表.使用者編號 AND 訂單表.賣家編號 = 賣家資訊表.賣家編號

GROUP BY VIP級別, 信用等級

使用集算器則是這樣:

 

A

1

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

2

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

3

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

4

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

5

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

A1,中查詢訂單資料;

A2,中查詢使用者資料;

A3,中查詢賣家資料;

A4,一次性關聯使用者資訊表、賣家資訊表這兩個維表;

A5,對關聯的結果進行彙總。

2.3 多層外來鍵

外來鍵表還可能會有多層的情況,下面這個例子中,假設資料庫中有訂單明細表、商品資訊表、類別資訊表三個表:

我們要查詢按照商品的大類名稱彙總的售出商品數量,SQL是這麼寫的:

SELECT 大類名稱, SUM (商品編號) 商品數

FROM 訂單明細表,商品資訊表,類別資訊表

WHERE 訂單明細表.商品編號 = 商品資訊表.商品編號 AND 商品資訊表.類別編號 = 類別資訊表.類別編號

GROUP BY 大類名稱

使用集算器則是這樣:

 

A

1

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

2

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

3

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

4

[email protected](類別編號,A3:類別編號)

5

[email protected](商品編號,A4:商品編號)

6

=A5.groups(商品編號.類別編號.大類名稱:大類名稱;sum(數量):商品數)

A1,查詢訂單明細資料;

A2,查詢商品資訊資料;

A3,查詢類別資訊資料;

A4,通過switch函式在A2建立指向類別資訊記錄的指標,實現關聯;

A5,通過switch函式在A1建立指向商品資訊記錄的指標,實現關聯,這樣就得到了一個三層關聯;

A6,對關聯的結果進行彙總。

 

用外來鍵指標化的辦法解決JOIN,對事實表遍歷一次就可以解析所有外來鍵。而資料庫的HASH JOIN演算法每執行一次(意味著遍歷一次資料)只能解析掉一個JOIN。

同時,如果全部使用記憶體,這個指標一旦建立後就可以複用,如果我們還要針對這個關聯情況再次計算 ,就不需要再去建立關聯了。而資料庫SQL則不行,還要再做HASH JOIN,即便是基於SQL體系的記憶體資料庫,在這方面也做不快。

2.4 平行計算

外來鍵指標化之後,還可以通過並行方式進一步優化效能。對已經裝入記憶體的事實表,我們可以對它進行分段訪問,從而以平行計算的方式明顯地提升計算的效能。

還是用上面多外來鍵的情形為例,仍然是按照使用者級別和賣家信用等級彙總訂單數量,我們來看看如何用集算器實現並行訪問事實表:

 

A

1

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

2

[email protected](4)

3

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

4

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

5

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

6

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

這段程式碼和前面的基本一樣,只是對訂單表這個事實表的訪問方式有所不同。A2使用@m選項把訂單表資料分為了4段,得到一個多路遊標,後面的計算都是基於這個多路遊標進行的。groups函式內部會自動判斷,如果A2是多路遊標那麼就會自動進行計算。這裡分為4段遊標,是假定CPU的個數是4個,實際使用中可以根據CPU的數量來決定分幾段。

並行在單任務時有很明顯的效果,主要是因為充分利用了CPU資源,如果併發較多時沒有太多空閒的CPU可用,那麼意義就不大了。

 

閱讀下一頁