一、 問題背景與適用場景
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腳本如下:
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爲例:
圖中只列了orderdetail的第一條記錄的預關聯情況,其它記錄與此類似。限於版面寬度,各表只列出了部分字段。
2. 兩表關聯
編寫SPL腳本如下:
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腳本如下:
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語言則無法實現這一優化技術。