關於該問題,暫時本身尚未深刻研究,在網上找到幾種解決方案,各有優缺點。php
第一種方案:
使用遞歸算法,也是使用頻率最多的,大部分開源程序也是這麼處理,不過通常都只用到四級分類。這種算法的數據庫結構設計最爲簡單。category表中一個字段id,一個字段fid(父id)。這樣能夠根據WHERE id = fid來判斷上一級內容,運用遞歸至最頂層。
分析:經過這種數據庫設計出的無限級,能夠說讀取的時候至關費勁,因此大部分的程序最多3-4級分類,這就足以知足需求,從而一次性讀出全部的數據,再對獲得數組或者對象進行遞歸。自己負荷仍是沒太大問題。可是若是分類到更多級,那是不可取的辦法。
這樣看來這種分類有個好處,就是增刪改的時候輕鬆了…然而就二級分類而言,採用這種算法就應該算最優先了。node
在Oracle 中咱們知道有一個 Hierarchical Queries 經過CONNECT BY 咱們能夠方便的查了全部當前節點下的全部子節點。但很遺憾,在MySQL的目前版本中尚未對應的功能。mysql
在MySQL中若是是有限的層次,好比咱們事先若是能夠肯定這個樹的最大深度是4, 那麼全部節點爲根的樹的深度均不會超過4,則咱們能夠直接經過left join 來實現。算法
但不少時候咱們沒法控制樹的深度。這時就須要在MySQL中用存儲過程來實現或在你的程序中來實現這個遞歸。本文討論一下幾種實現的方法。sql
樣例數據:數據庫
CREATE TABLE treeNodes ( id INT PRIMARY KEY, nodename VARCHAR(20), pid INT );
mysql> select * from treenodes;
+----+----------+------+
| id | nodename | pid |
+----+----------+------+
| 1 | A | 0 |
| 2 | B | 1 |
| 3 | C | 1 |
| 4 | D | 2 |
| 5 | E | 2 |
| 6 | F | 3 |
| 7 | G | 6 |
| 8 | H | 0 |
| 9 | I | 8 |
| 10 | J | 8 |
| 11 | K | 8 |
| 12 | L | 9 |
| 13 | M | 9 |
| 14 | N | 12 |
| 15 | O | 12 |
| 16 | P | 15 |
| 17 | Q | 15 |
+----+----------+------+
17 rows in set (0.00 sec)數組
樹形圖以下
1:A
+-- 2:B
| +-- 4:D
| +-- 5:E
+-- 3:C
+-- 6:F
+-- 7:G
8:H
+-- 9:I
| +-- 12:L
| | +--14:N
| | +--15:O
| | +--16:P
| | +--17:Q
| +-- 13:M
+-- 10:J
+-- 11:K oracle
實現方法示例:數據庫設計
方法一:利用函數來獲得全部子節點號。函數
建立一個function getChildLst, 獲得一個由全部子節點號組成的字符串.
delimiter // CREATE FUNCTION `getChildList`(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 // delimiter ;
獲取全部父節點:
delimiter // CREATE FUNCTION `getParentList`(rootId INT) RETURNS varchar(1000) BEGIN DECLARE sParentList varchar(1000); DECLARE sParentTemp varchar(1000); SET sParentTemp =cast(rootId as CHAR); WHILE sParentTemp is not null DO IF (sParentList is not null) THEN SET sParentList = concat(sParentTemp,',',sParentList); ELSE SET sParentList = concat(sParentTemp); END IF; SELECT group_concat(pid) INTO sParentTemp FROM treeNodes where FIND_IN_SET(id,sParentTemp)>0; END WHILE; RETURN sParentList; END // delimiter ; /*獲取父節點*/ /*調用: 一、select getParentList(6) id; 二、select * From user_role where FIND_IN_SET(id, getParentList(2));*/
select getParentList(4);
使用咱們直接利用find_in_set函數配合這個getChildlst來查找
mysql> select getChildList(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, getChildList(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, getChildList(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爲調用入口過程,準備臨時表及初始化。
DELIMITER // # 入口過程 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; // DELIMITER ;
DELIMITER // # 遞歸過程 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; // 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 EXISTS showTreeNodes_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 WHERE A.PID=B.ID AND B.nLevel=LEVEL-1 ; END WHILE; SELECT tmpLst.*,treeNodes.* FROM tmpLst,treeNodes WHERE tmpLst.id=treeNodes.id ORDER BY tmpLst.id; END; // DELIMITER ;
CALL showTreeNodes_yongyupost2000(1);
執行完後會產生一個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;
+--------------------------------------------+
| concat(SPACE(B.nLevel*2),'+--',A.nodename) |
+--------------------------------------------+
| +--A |
| +--B |
| +--D |
| +--E |
| +--C |
| +--F |
| +--G |
| +--H |
| +--J |
| +--K |
| +--I |
| +--L |
| +--N |
| +--O |
| +--P |
| +--Q |
| +--M |
+--------------------------------------------+
17 rows in set (0.00 sec)
優勢 : 層數的顯示。而且能夠按照樹的遍歷順序獲得結果。沒有遞歸限制。
缺點 : MySQL中對臨時表的限制,只能使用普通表,需作過後清理。
以上是幾個在MySQL中用存儲過程比較簡單的實現方法。
第二種方案:
設置fid字段類型爲varchar,將父類id都集中在這個字段裏,用符號隔開,好比:1,3,6
這樣能夠比較容易獲得各上級分類的ID,並且在查詢分類下的信息的時候,
可使用:SELECT * FROM category WHERE pid LIKE 「1,3%」。
分析:相比於遞歸算法,在讀取數據方面優點很是大,可是若查找該分類的全部 父分類 或者 子分類 查詢的效率也不是很高,至少也要二次query,從某種意義看上,我的以爲不太符合數據庫範式的設計。假若遞增到無限級,還需考慮字段是否達到要求,並且在修改分類和轉移分類的時候操做將很是麻煩。
暫時,在本身項目中用的就是相似第二種方案的解決辦法。就該方案在個人項目中存在這樣的問題, 若是當全部數據記錄達到上萬甚至10W以上後,一次性將因此分類,有序分級的現實出來,效率很低。極有多是項目處理數據代碼效率低帶來的。如今正在改良。
第三種方案:
無限級分類----改進前序遍歷樹
那麼理想中的樹型結構應具有哪些特色呢?數據存儲冗餘小、直觀性強;方便返回整個樹型結構數據;能夠很輕鬆的返回某一子樹(方便分層加載);快整 獲以某節點的祖譜路徑;插入、刪除、移動節點效率高等等。帶着這些需求我查找了不少資料,發現了一種理想的樹型結構數據存儲及操做算法,改進的前序遍歷樹 模型(The Nested Set Model)。
原理:
咱們先把樹按照水平方式擺開。從根節點開始(「Food」),而後他的左邊寫上1。而後按照樹的順序(從上到下)給「Fruit」的左邊寫上2。 這樣,你沿着樹的邊界走啊走(這就是「遍歷」),而後同時在每一個節點的左邊和右邊寫上數字。最後,咱們回到了根節點「Food」在右邊寫上18。下面是標 上了數字的樹,同時把遍歷的順序用箭頭標出來了。
我 們稱這些數字爲左值和右值(如,「Food」的左值是1,右值是18)。正如你所見,這些數字按時了每一個節點之間的關係。由於「Red」有3和6兩個值, 因此,它是有擁有1-18值的「Food」節點的後續。一樣的,咱們能夠推斷全部左值大於2而且右值小於11的節點,都是有2-11的「Fruit」 節點的後續。這樣,樹的結構就經過左值和右值儲存下來了。這種數遍整棵樹算節點的方法叫作「改進前序遍歷樹」算法。
表結構設計:
undefined那麼咱們怎樣才能經過一個SQL語句把全部的分類都查詢出來呢,並且要求若是是子類的話前面要打幾個空格以表現是子分類。要想查詢出所 有分類很好辦:SELECT * FROM category WHERE lft>1 AND lft<18 ORDER BY lft這樣的話全部的分類都出來了,可是誰是誰的子類卻分不清,那麼怎麼辦呢?咱們仔細看圖不難發現若是相鄰的兩條記錄的右值第一條的右值比第二條的大那 麼就是他的父類,好比food的右值是18而fruit的右值是11 那麼food是fruit的父類,可是又要考慮到多級目錄。因而有了這樣的設計,咱們用一個數組來存儲上一條記錄的右值,再把它和本條記錄的右值比較,如 果前者比後者小,說明不是父子關係,就用array_pop彈出數組,不然就保留,以後根據數組的大小來打印空格。這樣就解決了這個問題。代碼以下
表結構:
--
-- 表的結構 `category`
--
CREATE TABLE IF NOT EXISTS `category` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`type` int(11) NOT NULL COMMENT '1爲文章類型2爲產品類型3爲下載類型',
`title` varchar(50) NOT NULL,
`lft` int(11) NOT NULL,
`rgt` int(11) NOT NULL,
`lorder` int(11) NOT NULL COMMENT '排序',
`create_time` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=10 ;
--
-- 導出表中的數據 `category`
--
INSERT INTO `category` (`id`, `type`, `title`, `lft`, `rgt`, `lorder`, `create_time`) VALUES
(1, 1, '頂級欄目', 1, 18, 1, 1261964806),
(2, 1, '公司簡介', 14, 17, 50, 1264586212),
(3, 1, '新聞', 12, 13, 50, 1264586226),
(4, 2, '公司產品', 10, 11, 50, 1264586249),
(5, 1, '榮譽資質', 8, 9, 50, 1264586270),
(6, 3, '資料下載', 6, 7, 50, 1264586295),
(7, 1, '人才招聘', 4, 5, 50, 1264586314),
(8, 1, '留言板', 2, 3, 50, 1264586884),
(9, 1, '總裁', 15, 16, 50, 1267771951);
/**
* 顯示樹,把全部的節點都顯示出來。
* 一、先獲得根結點的左右值(默認根節點的title爲「頂級目錄」)。
* 二、查詢左右值在根節點的左右值範圍內的記錄,而且根據左值排序。
* 三、若是本次記錄右值大於前次記錄的右值則爲子分類,輸出時候加空格。
* @return array
**/
function display_tree(){
//得到root左邊和右邊的值
$arr_lr = $this->category->where("title = '頂級欄目'")->find();
//print_r($arr_lr);
if($arr_lr){
$right = array();
$arr_tree = $this->category->query("SELECT id, type, title, rgt FROM category WHERE lft >= ". $arr_lr['lft'] ." AND lft <=".$arr_lr['rgt']." ORDER BY lft");
foreach($arr_tree as $v){
if(count($right)){
while ($right[count($right) -1] < $v['rgt']){
array_pop($right);
}
}
$title = $v['title'];
if(count($right)){
$title = '|-'.$title;
}
$arr_list[] = array('id' => $v['id'], 'type' => $type, 'title' => str_repeat(' ', count($right)).$title, 'name' =>$v['title']);
$right[] = $v['rgt'];
}
return $arr_list;
}
}
好了 只要這樣全部的分類均可以一次性查詢出來了,而不用經過遞歸了。
下面的問題是怎樣進行插入、刪除和修改操做
插入:插入操做很簡單找到其父節點,以後把左值和右值大於父節點左值的節點的左右值加上2,以後再插入本節點,左右值分別爲父節點左值加一和加二,能夠用一個存儲過程來操做:
CREATE PROCEDURE `category_insert_by_parent`(IN pid INT,IN title VARCHAR(20), IN type INT, IN l_order INT, IN pubtime INT)
BEGIN
DECLARE myLeft INT;
SELECT lft into myLeft FROM category WHERE id= pid;
UPDATE qy_category SET rgt = rgt + 2 WHERE rgt > myLeft;
UPDATE qy_category SET lft = lft + 2 WHERE lft > myLeft;
INSERT INTO qy_category(type, title, lft, rgt, lorder, create_time) VALUES(type ,title, myLeft + 1, myLeft + 2, l_order, pubtime);
commit;
END
刪除操做:
刪除的原理:1.獲得要刪除節點的左右值,並獲得他們的差再加一,@mywidth = @rgt - @lft + 1;
2.刪除左右值在本節點之間的節點
3.修改條件爲大於本節點右值的全部節點,操做爲把他們的左右值都減去@mywidth
存儲過程以下:
CREATE PROCEDURE `category_delete_by_key`(IN id INT)
BEGIN
SELECT @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
FROM category
WHERE id = id;
DELETE FROM category WHERE lft BETWEEN @myLeft AND @myRight;
UPDATE nested_category SET rgt = rgt - @myWidth WHERE rgt > @myRight;
UPDATE nested_category SET lft = lft - @myWidth WHERE lft > @myRight;
修改: 要命的修改操做,本人看了好久也沒有看出什麼規律出來,只要出此下策,先刪除再插入,只要調用上面2個存儲過程就能夠了! 總結:查詢方便,可是增刪改操做有點繁瑣,可是通常分類此類操做不是不少,仍是查詢用的多,再說弄個存儲過程也方便! 上面第三種方案具體講解類容是從http://home.phpchina.com/space.php?uid=45095&do=blog&id=184675拷貝過來,方便之後本身查看。 暫時從各方面及理論上考慮 偏向於第三方案。不過尚未作過測試,到底效率怎麼樣。 期待更好的解決方案!