本博客會陸續寫一些和操作數據有關的基本算法。內容都很基礎,算是幫助大家回顧記憶。也可以給和我一樣,剛接觸數據庫,數據挖掘等技術的同學,提供一個迅速了解基本算法的文檔。我認為多多體會基本算法,不光是為了編程、性能優化,還可以學習到很多分析解決問題的方法。好了,不多廢話,歡迎大家來評論;如文中有錯誤,也歡迎大家來拍磚哈~
我們查詢數據時經常會用到聯合查詢
select r,s from R join S on R.id=S.rid
這個連接是怎么運算的呢?代價有多大呢?我們來分析一下。先來看下運算結果集的大小:若R∩S=空,則連接為笛卡爾積。 若R∩S是R的碼,可知s的一個元組至多與r的一個元組連接。因此連接結果集的元組數不會超過s中的元組數。 若R∩S既不是R的碼也不是S的碼,令R∩S={A},設S的元組數ns,V(A,s)為S中屬性A所具有的不同值的數目,則ns/V(A,s)是S關系中屬性A的值給定情況下平均的元組數目,則在r連接s的結果集中有nr*ns/V(A,s)個元組。
首先介紹最基礎的連接方式:嵌套循環連接。要計算r和s的theta連接,看偽代碼:
for each 元組tr in r do
begin
for each 元組ts in s do
begin
測試元組對(tr,ts)是否滿足連接條件theta
如果滿足,把tr*ts加到結果中
end
end
若元組對數目是nr*ns,對於關系r中的每一條記錄,我們必須對s做一次完整掃描。最好的情況,內存空間容納兩個關系,此時每一數據塊只需賭一次,從而只需bs+br次塊存取(設bs,br代表含有關系S,R的元組塊數目)。如果較小的那個關系能完全放在內存中,把這個關系作為內層關系來處理,則內層循環關系只需讀一次。最壞的情況下,緩沖區只能容納每個關系的一個數據塊,這是共需nr*bs+br次塊存取。
可以看到,當內存不能同時容納兩個關系,這種方法的代價很高。下面我們做一下小的優化:
for each 塊 Br of r do
begin
for each 塊 Bs of s do
begin
for each 元組 tr in Br do
begin
for each 元組ts in s do
begin
測試元組對(tr,ts)是否滿足連接條件θ
如果滿足,把tr*ts加到結果中
end
end
end
end
此算法以塊的方式而不是以元組的方式處理關系,叫做塊嵌套循環連接。最壞的情況下,對於外層關系的每一塊,內層關系s的每一塊只需讀一次,需br*bs+br次塊訪問。在塊嵌套循環連接算法中,外層關系的塊可以不用磁盤塊作單位,而以內存中最多能容納的大小為單位。若內層循環連接屬性上有索引,可以用更有效的索引查找法代替文件掃描法。
上面的兩種算法可以用於所有連接運算:(如果是自然連接或等值連接中的連接屬性是內層關系的碼,則內層循環一旦找到首條匹配元組就可以終止。)。如果是自然連接或等值連接,還有更高效的算法:
歸並連接
pr = r的第一個元組的地址;
ps = s的第一個元組的地址;
while(ps不等於null and pr不等於null) do
begin
ts=ps所指向的元組;
Ss={ts};
讓ps指向關系s的下一個元組;
done=false;
while(not done and ps不等於null)do
begin
ts=ps所指向的元組;
if(ts[JoinAttrs]=tr[JoinAttrs])
then begin
Ss=Ss並ts;
讓ps指向關系s的下一個元組;
end
else done=true;
end
tr=pr所指向的元組;
while(pr不等於null and tr[JoinAttrs]<ts[JoinAttrs])do
begin
讓pr指向關系r的下一個元組;
tr=pr所指向的元組;
end
while(pr不等於null and tr[JoinAttrs]=ts[JoinAttrs])do
begin
for each ts in Ss do
begin
將ts連接tr加入結果中;
end
讓pr指向關系r的下一個元組;
tr=pr所指向的元組;
end
end
歸並連接算法為每個關系分配一個指針,這些指針一開始指向對應關系的第一個元組,隨着算法的進行,指針遍歷整個關系。其中一個關系中在連接屬性上有相同值的所有元組被加入到Ss中。由於關系已排序,這樣已排序的每一元組只需讀一次,磁盤訪問次數:br+bs。
散列連接
如果關系r的一個元組與關系s的一個元組滿足連接條件,那么它們在連接屬性上有相同的值。若該值經散列函數映射為i,則關系r的那個元組必在Hri中,關系s的那個元組必在Hsi中。因此,Hri中的元組只需與Hsi中的元組想比較,而無需與s的其它任何分划比較。
for each 元組 ts in s do
begin Hsi=h(ts[JoinAttrs]);
Hsi=Hsi並{ts};
end
for each 元組 tr in r do
begin Hri=h(tr[JoinAttrs]);
Hri=Hri並{tr};
end
for Hsi=0 to max do
begin 讀Hsi在內存中建立其散列索引;
for each 元組tr in Hsi do
begin 檢索Hsi的散列索引,定位所有滿足tr[JoinAttrs]=ts
[JoinAttrs]的元組ts;
for each 匹配的元組ts in Hsi do
begin 將tr連接ts加入結果中;
end
end
end
選擇的max值應足夠大,以使對任意i,內存中可以容納構造用輸入關系的任意Hsi以及其上的散列索引。最好用較小的輸入關系作為構造用輸入關系。
遞歸划分:如果max的值大於等於內存頁幀數,關系的划分不可能一趟完成。每一趟中,輸入的可最多分裂的分化數不超過用於輸出的緩沖頁數。每一趟產生的存儲桶又在下一趟中分別讀入並 再次划分。每趟划分中所用的散列函數與上一趟所用的散列函數是不同的。分裂過程不斷重復直到構造用輸入關系的各個分划都能被內存容納為止。當M(內存塊)>max+1,max=b/M,關系不需要遞歸划分。
溢出處理:當Hsi的散列索引大於內存時,構造用輸入關系s的分划i發生散列表溢出。原因:相同值元組多;散列函數沒有隨機性、均勻性。
分解:任給i,若發現Hsi太大,用另一個散列函數進一步將之划分。
避讓:將關系s划分成許多小分划,然后把某些分划組合在一起,確保組合后的分划
能被內存容納。
如果有大象相同值,不采用在內存中創建散列索引后用嵌套循環連接算法對分划連接的方法,而是在分划使用其他技術,如塊嵌套循環連接。
代價:
無遞歸划分:3(br+bs)+2*max。分划時讀入寫出2(br+bs),構造檢索每個分划讀入br+bs,max分划中每個分划可能有一個部分滿的塊,讀寫各一次,不超過2*max。
有遞歸划分:2(br+bs)[logM-1(bs)-1]+br+bs。每一趟分划大小減小為原來的1/(M-1),直到每個分划最多占M塊。
復雜連接
嵌套循環連接於塊嵌套循環連接不管在什么條件下均可使用。其他連接技術比嵌套循環連接更有效,但只能處理簡單的連接條件,如自然連接或等值連接。
例如:
r與s在(theta1∩theta2∩…thetan)條件下的連接,可分別計算r與s在thetan下的連接,在計算其滿足(theta1∩theta2∩…thetan)條件的連接。
3個關系的連接除了利用"結合律",還可以兩邊建索引,計算中間每一個元組與兩邊對應的元組。