需求
一般樹形結構的數據使用需求有兩點:
顯示整棵樹的數據
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