性能優化技巧:預關聯

一、  問題背景與適用場景

SQL中JOIN的性能是個老大難問題,特別是關聯表較多時,計算性能會急劇下降。

SQL實現JOIN一般是採用HASH分堆的辦法,即先計算關聯鍵的HASH值,再將相同HASH值的記錄放到一起再做遍歷對比。每一個JOIN都要做一輪這樣的運算。

如果數據量相對於內存並不是很大,可以事先全部加載到內存中,那麼可以利用內存指針的機制,事先把關聯關係建立好。這樣做運算時就不必再做HASH與對比運算了。具體來說,就是在數據加載時一次性把HASH和對比運算做完,用指針方式保存關聯結果,然後每次運算可以直接引用到關聯記錄,從而提高運算的性能。

不幸的是,SQL沒有指針數據類型,無法實現這個優化邏輯,即使數據量可以在內存中裝下,也很難利用預關聯技巧提速,基於SQL的內存數據庫也大都有這個缺點。而SPL有指針數據類型,就可以實現這種機制。

我們下面來測試一下SQL實現單表計算和多表關聯計算的差距,再用SPL利用預關聯技巧同樣做一遍,看一下兩者的差距對比。

 

二、  測試環境

採用TPCH標準生成的8張數據表,共50G數據(要小到能放進內存)。TPCH數據表的結構網上有很多介紹,這裏就不再贅述了。

測試機有兩個Intel2670 CPU,主頻2.6G,共16核,內存128G,SSD固態硬盤。

由於 lineitem 表數據量太大,這臺服務器內存不足以將它裝入,所以創建了一張表結構與它一樣的表 orderdetail, 將數據量減少到內存能裝下,下面就用這張表來做測試。

爲方便看出差距,下面測試都用單線程計算,多核並不起作用。

 

三、  SQL測試

這裏用 Oracle 數據庫作爲 SQL 測試的代表,從orderdetail表裏查詢每年零件訂單的總收入。

1.  兩表關聯

查詢的SQL語句如下:

select

       l_year,

       sum(volume) as revenue

from

       (

              select

                     extract(year from l_shipdate) as l_year,

                     (l_extendedprice * (1 - l_discount) ) as volume

              from

                     orderdetail,

                     part

              where

                     p_partkey = l_partkey

                     and length(p_type)>2

       ) shipping

group by

       l_year

order by

       l_year;

2.   六表關聯

查詢的SQL語句如下:

select

       l_year,

       sum(volume) as revenue

from

       (

              select

                     extract(year from l_shipdate) as l_year,

                     (l_extendedprice * (1 - l_discount) ) as volume

              from

                     supplier,

                     orderdetail,

                     orders,

                     customer,

                     part,

                     nation n1,

                     nation n2

              where

                     s_suppkey = l_suppkey

                     and p_partkey = l_partkey

                     and o_orderkey = l_orderkey

                     and c_custkey = o_custkey

                     and s_nationkey = n1.n_nationkey

                     and c_nationkey = n2.n_nationkey

                     and length(p_type) > 2

                     and n1.n_name is not null

                     and n2.n_name is not null

                     and s_suppkey > 0

       ) shipping

group by

       l_year

order by

       l_year;

3.  測試結果


兩表關聯 六表關聯
運行時間(秒) 26 167

兩個查詢語句都用了嵌套寫法,Oracle自動優化後的計算性能比無嵌套時還要好一些(無嵌套時group by和select有可能會有重複計算)。

這兩個測試數據是多次運行後的結果,在測試中發現,Oracle 在第一次運行某查詢時,往往比第 2、3... 次要慢很多,說明在內存大於數據量時,數據庫可以把全部數據都緩存進內存(Oracle的緩存很強),所以我們取多次運行中最快那一次的時間,這樣就幾乎沒有硬盤讀取時間,僅是運算時間。

同時,在上面兩組測試中,過濾條件始終都爲真,也就是沒有對數據產生實質過濾,兩個查詢都涉及orderdetail表的全部記錄,計算規模是相當的。

從測試結果可以看出,六表關聯比兩表關聯慢了167/26=6.4倍!性能下降非常多。排除掉硬盤時間後,這裏增加的時間主要就是表間關聯以及針對關聯表字段的判斷,而這些判斷非常簡單,所以大部分時間消耗在表間關聯上。

這個測試表明,SQL的JOIN性能確實很差。

 

四、  SPL預關聯測試

1.   預關聯

實現預關聯的SPL腳本如下:

bef4400cbc05de35bf684369d9db10ed.gif A
1 >env(region,   file(path+"region.ctx").create().memory().keys@i(R_REGIONKEY))
2 >env(nation,   file(path+"nation.ctx").create().memory().keys@i(N_NATIONKEY))
3 >env(supplier,   file(path+"supplier.ctx").create().memory().keys@i(S_SUPPKEY))
4 >env(customer,   file(path+"customer.ctx").create().memory().keys@i(C_CUSTKEY))
5 >env(part,   file(path+"part.ctx").create().memory().keys@i(P_PARTKEY))
6 >env(orders,file(path+"orders.ctx").create().memory().keys@i(O_ORDERKEY))
7 >env(orderdetail,file(path+"orderdetail.ctx").create().memory())
8 >nation.switch(N_REGIONKEY,region)
9 >customer.switch(C_NATIONKEY,nation)
10 >supplier.switch(S_NATIONKEY,nation)
11 >orders.switch(O_CUSTKEY,customer)
12 >orderdetail.switch(L_ORDERKEY,orders;L_PARTKEY,part;L_SUPPKEY,supplier)

腳本中前7行分別將7個組表讀入內存,生成內表,並設成全局變量。後5行完成表間連接。在SPL服務器啓動時,就先運行此腳本,完成環境準備。

我們來看看預關聯後,內存中表對象的數據結構,以orderdetail爲例:

5870ee404fc2aa212faf2cfdc35e0117.png

圖中只列了orderdetail的第一條記錄的預關聯情況,其它記錄與此類似。限於版面寬度,各表只列出了部分字段。

 

2.  兩表關聯

編寫SPL腳本如下:

bef4400cbc05de35bf684369d9db10ed.gif A
1 =orderdetail.select(len(L_PARTKEY.P_TYPE)>2)
2 =A1.groups(year(L_SHIPDATE):l_year;   sum(L_EXTENDEDPRICE * (1 - L_DISCOUNT)):revenue)

 

3.   六表關聯

編寫SPL腳本如下:

bef4400cbc05de35bf684369d9db10ed.gif A
1 =orderdetail.select(len(L_PARTKEY.P_TYPE)>2   && L_ORDERKEY.O_CUSTKEY.C_NATIONKEY.N_NAME!=null &&  L_SUPPKEY.S_NATIONKEY.N_NAME!=null && L_SUPPKEY.S_SUPPKEY>0 )
2

=A1.groups(year(L_SHIPDATE):l_year;

sum(L_EXTENDEDPRICE   * (1 - L_DISCOUNT)):revenue)

預關聯後,SPL代碼也非常簡單,關聯表的字段直接可以作爲本表字段的子屬性訪問,很易於理解。

 

4.  運行結果


兩表關聯 六表關聯
運行時間(秒) 28 56

六表關聯僅僅比兩表關聯慢2倍,基本上就是增加的計算量(引用這些關聯表字段)的時間,而因爲有了預關聯,關聯運算本身不再消耗時間。

五、   結論

測試結果彙總:

運行時間(秒) 兩表關聯 六表關聯 性能降低倍數
SQL 26 167 6.4
SPL預關聯 28 56 2

六表關聯比兩表關聯,SQL慢了6.4倍,說明SQL處理JOIN消耗CPU很大,性能降低明顯。而採用預關聯機制後的SPL只慢2倍,多JOIN幾個表不再出現明顯的性能下降。

在進行關聯表較多的查詢時,如果內存大到足以將數據全部讀入內存(內存數據庫的應用場景),使用預關聯技術將極大地提升計算性能!而關係數據庫(包括內存數據庫)用SQL語言則無法實現這一優化技術。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章