如何在數據庫中存儲一棵樹

樹形結構的數據在項目開發中比較常見,好比比較典型的是論壇主題留言。node

每個主題(節點)能夠有n個留言(子節點)。這些留言又能夠有本身的留言。所以這種結構就是一顆樹。本文討論的是數據庫中如何存儲這種樹形結構。sql

假設有以下一棵樹:數據庫

無標題

方法一數據結構

注意:本例中的數據庫是SQLite,所以SQL語句只對SQLite有效,其餘數據庫能夠參考該寫法。閉包

要存儲於數據庫中,最簡單直接的方法,就是存儲每一個元素的父節點ID。spa

暫且把這種方法命名依賴父節點法,所以表結構設計以下:設計

598F8C3BAEC249C7B7C21FCAE42C097F

存儲的數據以下格式:3d

D91E5117473F4F75B42E8542953BE78C

這種結構下,若是查詢某一個節點的直接子節點,十分容易,好比要查詢D節點的子節點。code

1
select  from  tree1  where  parentid=4

若是要插入某個節點,好比在D節點下,再次插入一個M節點。blog

只須要以下SQL:

1
INSERT  INTO  tree1 (value,parentid)  VALUES ( 'M' ,4);

這種結構在查找某個節點的全部子節點,就稍顯複雜,不管是SELECT仍是DELETE均可能涉及到獲取全部子節點的問題。好比要刪除一個節點而且該節點的子節點也要所有刪除,那麼首先要得到全部子節點的ID,由於子節點並不僅是直接子節點,還可能包含子節點的子節點。好比刪除D節點及其子節點,必須先查出D節點下的全部子節點,而後再作刪除,SQL以下:

1
2
3
4
select  nodeid  from  tree1  where  parentid=4  --返回8,9
select  nodeid  from  tree1  where  parentid  in  (8,9)  --返回10,11,12
select  nodeid  from  tree1  where  parentid  in  (10,11,12)  --返回空
delete  from  tree1  where  nodeid  in  (4,8,9,10,11,12)

若是是隻刪除D節點,對於其它節點不作刪除而是作提高,那麼必須先修改子節點的parentid,而後才能刪除D節點。

正如上面演示的,對於這種依賴父節點法,最大的缺點就是沒法直接得到某個節點的全部子節點。所以若是要select全部的子節點,須要繁瑣的步驟,這不利於作聚合操做。

對於某些數據庫產品,支持遞歸查詢語句的,好比微軟的SQL Server,可使用CTE技術實現遞歸查詢。好比,要查詢D節點的全部子節點。只須要以下語句:

1
2
3
4
5
6
WITH  tmp  AS (
SELECT  FROM  Tree1  WHERE  nodeid = 4
UNION  ALL
SELECT  a.*  FROM  Tree1  AS  a,tmp  AS  WHERE  a.parentid = b. nodeid
)
SELECT  FROM  tmp

可是對於那些不支持遞歸查詢的數據庫來講,實現起來就比較複雜了。

 

方法二

還有一種比較土的方法,就是存儲路徑。暫且命名爲路徑枚舉法。

這種方法,將存儲根結點到每一個節點的路徑。

55778B9842DC47279FFCFF48B54ABDA1

這種數據結構,能夠一眼就看出子節點的深度。

若是要查詢某個節點下的子節點,只須要根據path的路徑去匹配,好比要查詢D節點下的全部子節點。

1
select  from  tree2  where  path  like  '%/4/%'

或者出於效率考慮,直接寫成

1
select  from  tree2  where  path  like  '1/4/%'

214EF7DB11684064ABB9C4FCBDDD5CD4

若是要作聚合操做,也很容易,好比查詢D節點下一共有多少個節點。

select count(*) from tree2 where path like '1/4/%';

要插入一個節點,則稍微麻煩點。要插入本身,而後查出父節點的Path,而且把本身生成的ID更新到path中去。好比,要在L節點後面插入M節點。

首先插入本身M,而後獲得一個nodeid好比nodeid=13,而後M要插入到L後面,所以,查出L的path爲1/4/8/12/,所以update M的path爲1/4/8/12/13

1
2
3
4
5
update  tree2  set
path=( select  path  from  tree2  where  nodeid=12)  --此處開始拼接
||last_insert_rowid()|| '/'
where
nodeid= last_insert_rowid();

這種方法有一個明顯的缺點就是path字段的長度是有限的,這意味着,不能無限制的增長節點深度。所以這種方法適用於存儲小型的樹結構。

方法三

下面介紹一種方法,稱之爲閉包表。

該方法記錄了樹中全部節點的關係,不只僅只是直接父子關係,它須要使用2張表,除了節點表自己以外,還須要使用1張表來存儲節祖先點和後代節點之間的關係(同時增長一行節點指向自身),而且根據須要,能夠增長一個字段,表示深度。所以這種方法數據量不少。設計的表結構以下:

Tree3表:

E1D5EEEE05EF4188ADE17192C9B95ECC

NodeRelation表:

C3E90EA4EEBE490D87035F98DFC39EA2

如例子中的樹,插入的數據以下:

Tree3表的數據

20ADFF42DB6E45CC9CA0C287DA49C5B5

NodeRelation表的數據

9F3B8EC76E0B4D67830FF29B6F6EEC4E

能夠看到,NodeRelation表的數據量不少。可是查詢很是方便。好比,要查詢D節點的子元素

只須要

1
select  from  NodeRelation  where  ancestor=4;

要查詢節點D的直接子節點,則加上depth=1

1
select  from  NodeRelation  where  ancestor=4  and  depth=1;

要查詢節點J的全部父節點,SQL:

1
select  from  NodeRelation  where  descendant=10;

若是是插入一個新的節點,好比在L節點後添加子節點M,則插入的節點除了M自身外,還有對應的節點關係。即還有哪些節點和新插入的M節點有後代關係。這個其實很簡單,只要和L節點有後代關係的,和M節點一定會有後代關係,而且和L節點深度爲X的和M節點的深度一定爲X+1。所以,在插入M節點後,找出L節點爲後代的那些節點做爲和M節點之間有後代關係,插入到數據表。

1
2
3
4
5
6
7
INSERT  INTO  tree3 (value)  VALUES ( 'M' ); --插入節點
INSERT  INTO   NodeRelation(ancestor,descendant,depth)
select  n.ancestor,last_insert_rowid(),n.depth+1 --此處深度+1做爲和M節點的深度
from  NodeRelation n
where  n.descendant=12
Union  ALL
select   last_insert_rowid() ,last_insert_rowid(),0  --加上自身

在某些並不須要使用深度的狀況下,甚至能夠不須要depth字段。

若是要刪除某個節點也很容易,好比,要刪除節點D,這種狀況下,除了刪除tree3表中的D節點外,還須要刪除NodeRelation表中的關係。

首先以D節點爲後代的關係要刪除,同時以D節點的後代爲後代的這些關係也要刪除:

1
2
delete  from  NodeRelation  where  descendant  in
( select  descendant  from  NodeRelation  where  ancestor=4 ); --查詢以D節點爲祖先的那些節點,即D節點的後代。

這種刪除方法,雖然完全,可是它也刪除了D節點和它本來的子節點的關係。

若是隻是想割裂D節點和A節點的關係,而對於它原有的子節點的關係予以保留,則須要加入限定條件。

限制要刪除的關係的祖先不以D爲祖先,即若是這個關係以D爲祖先的,則不用刪除。所以把上面的SQL加上條件。

1
2
3
delete  from  NodeRelation  where  descendant  in
( select  descendant  from  NodeRelation  where  ancestor=4 ); --查詢以D節點爲祖先的那些節點,即D節點的後代。
and  ancestor  not  in  ( select  descendant  from  NodeRelation   where  ancestor =4 )

上面的SQL用文字描述就是,查詢出D節點的後代,若是一個關係的祖先不屬於D節點的後代,而且這個關係的後代屬於D節點的後代,就刪除它。

這樣的刪除,保留了D節點自身子節點的關係,如上面的例子,實際上刪除的節點關係爲:

569AD87B6E7B4F428D3521B550F9D0FF

若是要刪除節點H,則爲

8579EB3DB87C4175B5DAAEAA9E182395

總結:

上面主要講了3種方式,各有優勢缺點。能夠根據實際須要,選擇合適的數據模型。

 

本文出自 「一隻博客」 博客,請務必保留此出處http://cnn237111.blog.51cto.com/2359144/1226911

相關文章
相關標籤/搜索