1. 程式人生 > >28娛樂PC28加拿大28手機版原始碼基於前序遍歷的無遞迴的樹形結構的資料庫表設計

28娛樂PC28加拿大28手機版原始碼基於前序遍歷的無遞迴的樹形結構的資料庫表設計

1)Adjacency List(鄰接表):每個節點僅記錄父節點主鍵。優點是簡單,缺點是訪問子樹需要遞迴遍歷,對資料庫壓力大(即使是支援遞迴SQL的資料庫)。
2)Path Enumerations( 路徑列舉):用一個字串記錄當前節點所在路徑。優點是查詢方便,缺點是佔用空間大,查詢需要使用like模糊方法,效率低,插入新記錄時要手工更改此節點以下所有路徑,維護不便。
3)Closure Table(閉包表):專門一張表維護Path,缺點是佔用空間大,操作不直觀。
4)Nested Sets (巢狀集):記錄左值和右值,優點是查詢子樹無需遞迴,缺點是非常複雜、難操作。

本文介紹的基於樹形結構的前序遍歷序列方法,示意圖如下:  


如上圖左邊的樹結構,對映在資料庫裡的結構見右圖表格,注意整個表格是一個排好序的樹結構的前序遍歷序列,相同節點深度的排序以line為準。 表格的最後一行(或每個根節點)必須有一個END標記,level設為0,上表中的ID為主鍵,Level為樹節點的深度。在以上基礎上,只要一句SQL,就可以無遞迴查詢出任意節點的所有子樹節點, 比某些資料自帶的基於遞迴原理的查詢更高效。 假設節點的行號為X,level為Y,則查詢整個子樹的SQL為: 
select * from tb where line>=X and line<(select min(line) from tb where line>X and level<=Y) 
例如獲取D節點及其所有子節點: 
select * from tb where line>=7 and line< (select min(line) from tb where line>7 and level<=2)

它依據的原理很簡單:按樹結構的前序遍歷序列儲存的樹結構,判斷它的所有子結點,只要從這個節點開始往下劃豎線,豎線右邊的全是它的子節點,直到碰到豎線上或豎線左邊出現非空值則終止,原理圖如下:


這種方案的優點是查詢高效,刪除高效(只需要刪除當前行即可,雖然會造成line欄位的跳號,但是不影響使用),插入節點也很簡單,只需要2行簡單的SQL即可,例如要在line=9和line=10的兩個節點之間插入一個新節點,則新節點行號應為10,原行號為10的將變為11, SQL操作為:
update tb2 set line=line+1 where line>=10; //把10以後的所有行號加1
insert into tb (line,id,level) values (10,'T',4); //執行插入新節點操作
雖然影響的行數非常多,但是理解和維護都非常簡單。 如果擔心插入操作對效能的影響,還可以將行號跳號設計(如按100000跳號),新的節點可以插入在兩個節點的空號區中段(如果節點間存在空號區),這樣可以很大程式上消除進行加1操作的概率。

這種方案的缺點是當需要移動節點時,必須維護整張表的前序遍歷排序順序,必要時要進行整張表或子樹的重排序, 這是一個很耗時的工作(其它三種方案Path Enumerations/Closure Table/Nested Sets也有這個缺點),這是不可避免的,是以始終維護一個索引為代價換取查詢效能的提高。因此它最適合只有增刪查操作,但是很少有移動節點的場合。

總結:這種方案最適合的場合是樹的深度很深(可以超過上百萬),不經常移動節點、對查詢速度要求極高的場合(因為無遞迴)。

本文實際上可以抽象成一種利用前序遍歷給多叉樹建查詢索引的方案,即使在沒有資料庫存在的情況下,也是有可能在程式中應用到這種方案的,只要將SQL中的查詢功能改成手工進行陣列遍歷即可。
另外,資料庫開發者也可以考慮,對於鄰接表模式(即只有id和pid兩個關鍵欄位)儲存的樹結構, 可以用這種方法建立一個內部索引,將level和line作為資料庫表的隱藏欄位,這樣使用者就可以不用手工維護level、line以及維護前序遍歷排序這些與業務無關的操作,這才是最人性化的使用方式。