上一篇里面我們實現了單表查詢和top N查詢,這一篇我們來講述如何實現多表連接和group by分組。
一、多表連接
多表連接的時間是數據庫一個非常耗時的操作,因為連接的時間復雜度是M*N(M,N是要連接的表的記錄數),如果不對進行優化,連接的產生的臨時表可能非常大,需要寫入磁盤,分多趟進行處理。
1、雙表等值join
我們看這樣一個連接sql:
select PS_AVAILQTY,PS_SUPPLYCOST,S_NAME from SUPPLIER,PARTSUPP where PS_SUPPKEY = S_SUPPKEY and PS_AVAILQTY > 2000and S_NATIONKEY = 1;
可以把這個sql理解為在SUPPLIER表的S_SUPPKEY屬性和PARTSUPP表的PS_SUPPKEY屬性上作等值連接,並塞選出滿足PS_AVAILQTY > 2000和 S_NATIONKEY = 1的記錄,輸入滿足條件記錄的PS_AVAILQTY,PS_SUPPLYCOST,S_NAME屬性。這樣的理解對我們人來說是很明了的,但數據庫不能照這樣的方式執行,上面的PS_SUPPKEY其實是PARTSUPP的外鍵,兩個表進行等值連接,得到的連接結果是很大的。所以我們應該先從單表查詢條件入手,在單表查詢過濾之后再進行等值連接,這樣需要連接的記錄數會少很多。
首先根據PS_AVAILQTY > 2000找出滿足條件的PARTSUPP表的記錄行號集A,然后根據S_NATIONKEY = 1找出SUPPLIER表找出相應的記錄行號集B,在記錄集A、B上進行等值連接,看圖很簡單:
依次掃描的時間復雜度為max(m,n),加上折半查找,總的時間復雜度為max(m,n)*(log(m1)+log(n1)),其中m1、n1表示where條件塞選出的記錄數。
來看一下執行的結果:
Input SQL: select PS_AVAILQTY,PS_SUPPLYCOST,S_NAME from SUPPLIER,PARTSUPP where PS_SUPPKEY = S_SUPPKEY and PS_AVAILQTY > 2000 and S_NATIONKEY = 1; {'FROM': ['SUPPLIER', 'PARTSUPP'], 'GROUP': None, 'ORDER': None, 'SELECT': [['PARTSUPP.PS_AVAILQTY', None, None], ['PARTSUPP.PS_SUPPLYCOST', None, None], ['SUPPLIER.S_NAME', None, None]], 'WHERE': [['PARTSUPP.PS_AVAILQTY', '>', '2000'], ['SUPPLIER.S_NATIONKEY', '=', '1'], ['PARTSUPP.PS_SUPPKEY', '=', 'SUPPLIER.S_SUPPKEY']]} Quering: PARTSUPP.PS_AVAILQTY > 2000 Quering: SUPPLIER.S_NATIONKEY = 1 Quering: PARTSUPP.PS_SUPPKEY = SUPPLIER.S_SUPPKEY Output: The result hava 26322 rows, here is the fisrt 10 rows: ------------------------------------------------- rows PARTSUPP.PS_AVAILQTY PARTSUPP.PS_SUPPLYCOST SUPPLIER.S_NAME ------------------------------------------------- 1 8895 378.49 Supplier#000000003 2 4286 502.00 Supplier#000000003 3 6996 739.71 Supplier#000000003 4 4436 377.80 Supplier#000000003 5 6728 529.58 Supplier#000000003 6 8646 722.34 Supplier#000000003 7 9975 841.19 Supplier#000000003 8 5401 139.06 Supplier#000000003 9 6858 786.94 Supplier#000000003 10 8268 444.21 Supplier#000000003 ------------------------------------------------- Take 26.58 seconds.
從Quering后面的信息可以看到我們處理where子條件的順序,先處理單表查詢,再處理多表連接。
2、多表join
處理完雙表join后,我們看一下怎么實現三個的join,示例sql:
select PS_AVAILQTY,PS_SUPPLYCOST,S_NAME from SUPPLIER,PART,PARTSUPP where PS_PARTKEY = P_PARTKEY and PS_SUPPKEY = S_SUPPKEY and PS_AVAILQTY > 2000 and P_BRAND = 'Brand#12' and S_NATIONKEY = 1;
這里進行三個表的連接,三個表連接得到的應該是三個表的記錄合並的結果,那根據where條件選出的記錄行號應當包含三列,每一列是一個表的行號:
三個表的連接事實上建立在兩個表連接的基礎上的,先進行兩個表的連接后,得到兩組行號表,再將這兩組行號表合並:
主要代碼如下:
1 sortJoin(joina,cloumi)#cloumi表示公共表在joina的列號 2 sortJoin(joinb,cloumj)#cloumj表示公共表在joinb的列號 3 i = j = 0#左右指針初試為0 4 while i < len(joina) and j < len(joinb): 5 if joina[i][cloumi] < joinb[j][cloumj]: 6 i += 1 7 elif joina[i][cloumi] > joinb[j][cloumj]: 8 j += 1 9 else:#相等,進行連接 10 lastj = j 11 while j < len(joinb) and joina[i][cloumi] == joinb[j][cloumj]: 12 temp = joina[i] + joinb[j] 13 temp.remove(joina[i][cloumi])#刪掉重復的元素 14 mergeResult.append(temp) 15 j += 1 16 j = lastj#右指針回滾 17 i += 1
我們分析一下這個算法的時間復雜度,首先要對兩個表排序,復雜度為O(m1log(m1)),在掃描的過程中,右邊指針會回溯,所以不再是O(max(m1,n1)),我們可以認為是k*O(m1*n1),這個系數k應該是很小的,因為一般右指針不會回溯太遠,總的時間復雜度是O(m1log(m1))+k*O(m1*n1),應該是小於N方的復雜度。
看一下執行的結果:
Input SQL: select PS_AVAILQTY,PS_SUPPLYCOST,S_NAME from SUPPLIER,PART,PARTSUPP where PS_PARTKEY = P_PARTKEY and PS_SUPPKEY = S_SUPPKEY and PS_AVAILQTY > 2000 and P_BRAND = 'Brand#12' and S_NATIONKEY = 1; {'FROM': ['SUPPLIER', 'PART', 'PARTSUPP'], 'GROUP': None, 'ORDER': None, 'SELECT': [['PARTSUPP.PS_AVAILQTY', None, None], ['PARTSUPP.PS_SUPPLYCOST', None, None], ['SUPPLIER.S_NAME', None, None]], 'WHERE': [['PARTSUPP.PS_AVAILQTY', '>', '2000'], ['PART.P_BRAND', '=', 'Brand#12'], ['SUPPLIER.S_NATIONKEY', '=', '1'], ['PARTSUPP.PS_PARTKEY', '=', 'PART.P_PARTKEY'], ['PARTSUPP.PS_SUPPKEY', '=', 'SUPPLIER.S_SUPPKEY']]} Quering: PARTSUPP.PS_AVAILQTY > 2000 Quering: PART.P_BRAND = Brand#12 Quering: SUPPLIER.S_NATIONKEY = 1 Quering: PARTSUPP.PS_PARTKEY = PART.P_PARTKEY Quering: PARTSUPP.PS_SUPPKEY = SUPPLIER.S_SUPPKEY Output: The result hava 1022 rows, here is the fisrt 10 rows: ------------------------------------------------- rows PARTSUPP.PS_AVAILQTY PARTSUPP.PS_SUPPLYCOST SUPPLIER.S_NAME ------------------------------------------------- 1 4925 854.19 Supplier#000002515 2 4588 455.04 Supplier#000005202 3 8830 852.13 Supplier#000007814 4 8948 689.89 Supplier#000002821 5 3870 488.38 Supplier#000005059 6 6968 579.03 Supplier#000005660 7 9269 228.31 Supplier#000000950 8 8818 180.32 Supplier#000003453 9 9343 785.01 Supplier#000003495 10 3364 545.25 Supplier#000006030 ------------------------------------------------- Take 50.42 seconds.
這個查詢的時間比Mysql快了很多,在mysql上運行這個查詢需要10分鍾(建立了索引),想想也是合理的,我們的設計已經大大簡化了,完全不考慮表的修改,犧牲這么的實用性必然能提升在查詢上的效率。
二、group by分組
在執行完where條件后,讀取原始記錄,然后可以按group by的屬性分組,分組的屬性可能有多條,比如這樣一個查詢:
select PS_AVAILQTY,PS_SUPPLYCOST,S_NAME,COUNT(*) from SUPPLIER,PART,PARTSUPP where PS_PARTKEY = P_PARTKEY and PS_SUPPKEY = S_SUPPKEY and PS_AVAILQTY > 2000 and P_BRAND = 'Brand#12' and S_NATIONKEY = 1; group by PS_AVAILQTY,PS_SUPPLYCOST,S_NAME;
按 PS_AVAILQTY,PS_SUPPLYCOST,S_NAME這三個屬性分組,我們實現時使用了一個技巧,將每個候選記錄的這三個字段按字符串格式拼接成一個新的屬性,拼接的示例如下:
"4925" "854.19" "Supplier#000002515" -->> "4925+854.19+Supplier#000002515"
注意中間加了一個加號“+”,這個加號是必須的,如果沒有加號,"105","201"與"10","5201"的拼接結果都是"105201",這樣得到的group by結果將會出錯,而添加一個加號它們兩的拼接結果是不同的。
拼接后,我們只需要按新的屬性進行分組,可以使用map來實現,map的key為新的屬性值,value為新屬性值key的后續記錄。再在組上進行聚集函數的運算。
這個小項目就寫到這里了,或許這壓根只是一個數據處理,談不上數據庫實現,不過通過這個小項目我對數據庫底層的實現還是了解了很多,以后做數據庫優化理解起來也容易一些。
謝謝關注,歡迎評論。
作者:MyDetail
出處:http://www.cnblogs.com/fengfenggirl/
本文版權歸作者MyDetail和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。