mysql如何存儲樹形結構的數據


需求
一般樹形結構的數據使用需求有兩點:

顯示整棵樹的數據

select * from treeNodes

給出某個點,顯示到達該點所經過的路徑

a=select * from treeNodes where id='7'
b=select * from treeNodes where id=a.pid
c=select * from treeNodes where id=b.pid

…依次遞歸到Root節點。

還可以使用如下幾種方法獲取經過的路徑:

方法一、利用函數來得到所有子節點號

創建一個function getChildLst, 得到一個由所有子節點號組成的字符串.

mysql> delimiter /
mysql>
mysql> CREATE FUNCTION getChildLst(rootId INT)
-> RETURNS varchar(1000)
-> BEGIN
-> DECLARE sTemp VARCHAR(1000);
-> DECLARE sTempChd VARCHAR(1000);
->
-> SET sTemp = ‘$’;
-> SET sTempChd =cast(rootId as CHAR);
->
-> WHILE sTempChd is not null DO
-> SET sTemp = concat(sTemp,’,’,sTempChd);
-> SELECT group_concat(id) INTO sTempChd FROM treeNodes where FIND_IN_SET(pid,sTempChd)>0;
-> END WHILE;
-> RETURN sTemp;
-> END
-> //
Query OK, 0 rows affected (0.00 sec)

mysql>
mysql> delimiter ;

  

使用我們直接利用find_in_set函數配合這個getChildlst來查找

mysql> select getChildLst(1);
+—————–+
| getChildLst(1) |
+—————–+
| $,1,2,3,4,5,6,7 |
+—————–+
1 row in set (0.00 sec)

mysql> select * from treeNodes
-> where FIND_IN_SET(id, getChildLst(1));
+—-+———-+——+
| id | nodename | pid |
+—-+———-+——+
| 1 | A | 0 |
| 2 | B | 1 |
| 3 | C | 1 |
| 4 | D | 2 |
| 5 | E | 2 |
| 6 | F | 3 |
| 7 | G | 6 |
+—-+———-+——+
7 rows in set (0.01 sec)

mysql> select * from treeNodes
-> where FIND_IN_SET(id, getChildLst(3));
+—-+———-+——+
| id | nodename | pid |
+—-+———-+——+
| 3 | C | 1 |
| 6 | F | 3 |
| 7 | G | 6 |
+—-+———-+——+
3 rows in set (0.01 sec)

  

優點: 簡單,方便,沒有遞歸調用層次深度的限制 (max_sp_recursion_depth,最大255) ;

缺點:長度受限,雖然可以擴大 RETURNS varchar(1000),但總是有最大限制的。

MySQL目前版本( 5.1.33-community)中還不支持function 的遞歸調用。

方法二、利用臨時表和過程遞歸

創建存儲過程如下。
createChildLst 為遞歸過程,showChildLst為調用入口過程,准備臨時表及初始化。

mysql> delimiter //
mysql>
mysql> # 入口過程
mysql> CREATE PROCEDURE showChildLst (IN rootId INT)
-> BEGIN
->CREATE TEMPORARY TABLE IF NOT EXISTS tmpLst
-> (sno int primary key auto_increment,id int,depth int);
->DELETE FROM tmpLst;
->
->CALL createChildLst(rootId,0);
->
->select tmpLst.,treeNodes. from tmpLst,treeNodes where tmpLst.id=treeNodes.id order by tmpLst.sno;
-> END;
-> //
Query OK, 0 rows affected (0.00 sec)
mysql>
mysql> # 遞歸過程
mysql> CREATE PROCEDURE createChildLst (IN rootId INT,IN nDepth INT)
-> BEGIN
->DECLARE done INT DEFAULT 0;
->DECLARE b INT;
->DECLARE cur1 CURSOR FOR SELECT id FROM treeNodes where pid=rootId;
->DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
->
->insert into tmpLst values (null,rootId,nDepth);
->
->OPEN cur1;
->
->FETCH cur1 INTO b;
->WHILE done=0 DO
->CALL createChildLst(b,nDepth+1);
->FETCH cur1 INTO b;
->END WHILE;
->
->CLOSE cur1;
-> END;
-> //
Query OK, 0 rows affected (0.00 sec)
mysql> delimiter ;

  

調用時傳入結點

mysql> call showChildLst(1);
+-----+------+-------+----+----------+------+
| sno | id | depth | id | nodename | pid|
+-----+------+-------+----+----------+------+
| 4 |1 | 0 |1 | A|0 |
| 5 |2 | 1 |2 | B|1 |
| 6 |4 | 2 |4 | D|2 |
| 7 |5 | 2 |5 | E|2 |
| 8 |3 | 1 |3 | C|1 |
| 9 |6 | 2 |6 | F|3 |
|10 |7 | 3 |7 | G|6 |
+-----+------+-------+----+----------+------+
7 rows in set (0.13 sec)
Query OK, 0 rows affected, 1 warning (0.14 sec)
mysql>
mysql> call showChildLst(3);
+-----+------+-------+----+----------+------+
| sno | id | depth | id | nodename | pid|
+-----+------+-------+----+----------+------+
| 1 |3 | 0 |3 | C|1 |
| 2 |6 | 1 |6 | F|3 |
| 3 |7 | 2 |7 | G|6 |
+-----+------+-------+----+----------+------+
3 rows in set (0.11 sec)
Query OK, 0 rows affected, 1 warning (0.11 sec)

  

depth 為深度,這樣可以在程序進行一些顯示上的格式化處理。類似於oracle中的 level 偽列。sno 僅供排序控制。這樣你還可以通過臨時表tmpLst與數據庫中其它表進行聯接查詢。

MySQL中你可以利用系統參數 max_sp_recursion_depth 來控制遞歸調用的層數上限。如下例設為12.

mysql> set max_sp_recursion_depth=12;
Query OK, 0 rows affected (0.00 sec)

  

優點 : 可以更靈活處理,及層數的顯示。並且可以按照樹的遍歷順序得到結果。
缺點 : 遞歸有255的限制。

方法三、利用中間表和過程

(本方法由yongyupost2000提供樣子改編)
創建存儲過程如下。由於MySQL中不允許在同一語句中對臨時表多次引用,只以使用普通表tmpLst來實現了。當然你的程序中負責在用完后清除這個表。

delimiter //
drop PROCEDURE IF EXISTSshowTreeNodes_yongyupost2000//
CREATE PROCEDURE showTreeNodes_yongyupost2000 (IN rootid INT)
BEGIN
DECLARE Level int ;
drop TABLE IF EXISTS tmpLst;
CREATE TABLE tmpLst (
id int,
nLevel int,
sCort varchar(8000)
);
Set Level=0 ;
INSERT into tmpLst SELECT id,Level,ID FROM treeNodes WHERE PID=rootid;
WHILE ROW_COUNT()>0 DO
SET Level=Level+1 ;
INSERT into tmpLst
SELECT A.ID,Level,concat(B.sCort,A.ID) FROM treeNodes A,tmpLst B
WHEREA.PID=B.ID AND B.nLevel=Level-1;
END WHILE;
END;
delimiter ;
CALL showTreeNodes_yongyupost2000(0);

  

執行完后會產生一個tmpLst表,nLevel 為節點深度,sCort 為排序字段。
使用方法

SELECT concat(SPACE(B.nLevel*2),'+--',A.nodename)
FROM treeNodes A,tmpLst B
WHERE A.ID=B.ID
ORDER BY B.sCort;

 

  

優點 : 層數的顯示。並且可以按照樹的遍歷順序得到結果。沒有遞歸限制。
缺點 : MySQL中對臨時表的限制,只能使用普通表,需做事后清理。

 

 

存儲結構對比優化
假設有如下一棵樹:

1、存儲父節點

要存儲於數據庫中,最簡單直接的方法,就是存儲每個元素的父節點ID。
暫且把這種方法命名依賴父節點法,因此表結構設計如下:

存儲的數據如下格式:

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

select * from tree1 where parentid=4

 

如果要插入某個節點,比如在D節點下,再次插入一個M節點。
只需要如下SQL:

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

 

這種結構在查找某個節點的所有子節點,就稍顯復雜,無論是SELECT還是DELETE都可能涉及到獲取所有子節點的問題。比如要刪除一個節點並且該節點的子節點也要全部刪除,那么首先要獲得所有子節點的ID,因為子節點並不只是直接子節點,還可能包含子節點的子節點。比如刪除D節點及其子節點,必須先查出D節點下的所有子節點,然后再做刪除,SQL如下:

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節點的所有子節點。只需要如下語句:

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

  

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

2、存儲路徑

還有一種比較土的方法,就是存儲路徑。暫且命名為路徑枚舉法。
這種方法,將存儲根結點到每個節點的路徑。

這種數據結構,可以一眼就看出子節點的深度。
如果要查詢某個節點下的子節點,只需要根據path的路徑去匹配,比如要查詢D節點下的所有子節點。

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

 

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

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

如果要做聚合操作,也很容易,比如查詢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

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

  

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

3、存儲關系表和深度

下面介紹一種方法,稱之為閉包表。
該方法記錄了樹中所有節點的關系,不僅僅只是直接父子關系,它需要使用2張表,除了節點表本身之外,還需要使用1張表來存儲節祖先點和后代節點之間的關系(同時增加一行節點指向自身),並且根據需要,可以增加一個字段,表示深度。因此這種方法數據量很多。設計的表結構如下:
Tree3表:

NodeRelation表:

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

NodeRelation表的數據

可以看到,NodeRelation表的數據量很多。但是查詢非常方便。比如,要查詢D節點的子元素
只需要

select * from NodeRelation where ancestor=4;

 

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

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

 

要查詢節點J的所有父節點,SQL:

select * from NodeRelation where descendant=10;

  


如果是插入一個新的節點,比如在L節點后添加子節點M,則插入的節點除了M自身外,還有對應的節點關系。即還有哪些節點和新插入的M節點有后代關系。這個其實很簡單,只要和L節點有后代關系的,和M節點必定會有后代關系,並且和L節點深度為X的和M節點的深度必定為X+1。因此,在插入M節點后,找出L節點為后代的那些節點作為和M節點之間有后代關系,插入到數據表。

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節點的后代為后代的這些關系也要刪除:

delete from NodeRelation where descendant in
(select descendant from NodeRelation where ancestor=4 );

  

–查詢以D節點為祖先的那些節點,即D節點的后代。
這種刪除方法,雖然徹底,但是它也刪除了D節點和它原本的子節點的關系。
如果只是想割裂D節點和A節點的關系,而對於它原有的子節點的關系予以保留,則需要加入限定條件。
限制要刪除的關系的祖先不以D為祖先,即如果這個關系以D為祖先的,則不用刪除。因此把上面的SQL加上條件。

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節點自身子節點的關系,如上面的例子,實際上刪除的節點關系為:

如果要刪除節點H,則為

總結:
上面主要講了3種方式,各有優點缺點。可以根據實際需要,選擇合適的數據模型。

參考鏈接:
http://blog.csdn.net/sky786905664/article/details/52742392
http://langgufu.iteye.com/blog/1891798


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM