樹形數據在關係數據庫的存儲

  樹形數據在關係數據庫中的存儲同對象一樣,都會遇到一個"阻抗不匹配"的問題。如何設計一個表結構,才能較好的滿足需求呢?

  事實上,有很多解決方案,但是沒有哪一種是放之四海而皆準的。我個人認爲解決方案的選擇,必須依賴於需求背景。拋棄需求背景而就技術泛泛而談,就如同孔乙己對回字不同寫法的孜孜追求,滿身酸腐之氣。

  凡事有得就有失,十全十美的方案是不存的,合適的就是最好的。

下面就集中常見的方案做一下比對,然後詳細分析一下第四種方案。

方案一:parent_id

Pros:

  • 非常容易實現
  • 很方便的將子樹移動到另外一個節點
  • 添加節點非常簡單

Cons:

  • 檢索整棵樹需要使用遞歸,非常耗費時間
  • 查找指定節點的所有父(子)節點同樣要使用遞歸,非常耗費時間
方案二:  Path欄位
表添加Path欄位,每個節點記錄從根節點到自身的一個路徑。
Pros:
  • 很容易查找所有父子節點
  • 很容易檢索整棵樹
  • 添加節點很簡單
Cons:
  • 移動子樹比較麻煩,會導致大量數據更新
  • 取決於path的存儲方式,可能需要對path進行解析

方案三:Path表
建立一個表,記錄節點和其所有父節點的關聯關係。
Pros:
  • 很容易查找所有父子節點
  • 很容易檢索整棵樹
Cons:
  • 添加節點很麻煩,需要同時產生很多關聯關係
  • 移動子樹導致大量數據操作
  • 數據量可能很龐大
方案四: 預排序遍歷樹算法(modified preorder tree traversal algorithm)
Pros:
  • 很容易查找所有父子節點
  • 很容易檢索整棵樹
  • 直接使用SQL就可以得到相關的數據
  • 存儲方式決定了子節點都是有序存儲的
Cons:
  • 新增,更新,移動都很複雜,每次都會變更非常多的數據

第四種方案的算法的圖例如下,這個圖例比較難以理解,我另外做了個圖例,以矩形嵌套來描述它。如下面的圖一圖二:



                                                              (圖一)


(圖二)

圖二中的節點以矩形嵌套矩形的方式來描述父子關係,每個框代表一個節點,左右邊框各記錄一個值。值的維護從左到右依次遞增。
這個圖例很容易看出如何使用一個節點的左右邊界值來查找其子節點。

我們轉成二維表,看看數據庫的存儲形式:

Parent Title lft rgt

Food 1 18
Food Fruit 2 11
Fruit Red 3 6
Red Cherry 4 5
Fruit Yellow 7 10
Yellow Banana 8 9
Food Meat 12 17
Meat Beef 13 14
Meat Pork 15 16

下面我們來看看樹上的典型操作如何實現:

1. 檢索樹
給定一個節點,查找該節點及其子節點的sql:
SELECT * FROM tree WHERE lft BETWEEN 2 AND 11 ORDER BY lft ASC;

2. 查找所有父節點
所有父節點的特徵是左邊值小於當前節點的左值,右邊值大於當前節點的右值
SELECT title FROM tree WHERE lft < 4 AND rgt > 5 ORDER BY lft ASC;

3. 查找路徑 Food > Cherry
select * from tree where lft between 1 and 4 and rgt between 5 and 18 order by lft
改變lft的排序方式,即可實現從父節點>子節點,或者子節點到父節點


4. 計算子節點的數目
每個子節點佔2個數據,計算公式如下。判斷當前節點是否葉子節點,或者有多少個子節點,可以根據下面的公式
descendants = (right - left - 1) / 2

5. 新增子節點
以在Food>Fruit>Yellow 右邊再增加一個新的兄弟節點Black爲例子
1) 父節點的右邊值爲11. 新節點的左右值應該爲 11, 12
2) 變更所有的受影響的節點,給新節點騰出空位子。
所有左節點>=11的,都增加2
update tree set lft = lft + 2 where lft>=11
所有右節點>=11的,都增加2
update tree set rgt = rgt + 2 where rgt>=11
3) 新節點放到空位上,左右邊值分別爲11,12
INSERT INTO tree SET lft=11, rgt=12, title='Black'

6. 刪除子節點
以刪除Food>Fruit>Red 節點爲例子
1) 刪除子節點及其下面所有節點
delete from tree where lft >= 3 and rgt <= 6
2) 變更所有的受影響的節點. 
所有左節點大於6的減去4 (rgt - lft + 1)
update tree set lft = lft - 4 where lft > 6
所有右節點大於6的減去4
update tree set rgt = rgt - 4 where rgt> 6

7. 移動子節點
以將Food>Fruit>Yellow  移動到 Food>Meat>Pork下爲例子:
要移動的節點是(7,11),目標節點是(15,16)
1) 以目標節點的Pork爲參考,變更要移動節點的值
update tree set lft = lft + 9, rht=rgt + 9 where lft >=7 and rgt<=11
2) 原節點所在位置以被刪除看待
所有左節點大於11的減去5 (rgt - lft + 1)
update tree set lft = lft - 5 where lft > 11
所有右節點大於11的減去5
update tree set rgt = rgt - 5 where rgt> 11

可以看出這種方法的最大優勢是提升了讀的性能,但是犧牲了寫的性能,而且寫的時候必須鎖表。

以下資料供參考:

發佈了50 篇原創文章 · 獲贊 14 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章