產品分類,多級的樹狀結構的論壇,郵件列表等許多地方我們都會遇到這樣的問題:如何存儲多級結構的數據?在PHP的應用中,提供後臺數據存儲的通常是關係 型數據庫,它能夠保存大量的數據,提供高效的數據檢索和更新服務。然而關係型數據的基本形式是縱橫交錯的表,是一個平面的結構,如果要將多級樹狀結構存儲 在關係型數據庫裏就需要進行合理的翻譯工作。接下來我會將自己的所見所聞和一些實用的經驗和大家探討一下:
層級結構的數據保存在平面的數據庫中基本上有兩種常用設計方法:
- 毗鄰目錄模式(adjacency list model)
- 預排序遍歷樹算法(modified preorder tree traversal algorithm)
我不是計算機專業的,也沒有學過什麼數據結構的東西,所以這兩個名字都是我自己按照字面的意思翻的,如果說錯了還請多多指教。這兩個東西聽着好像很嚇人,其實非常容易理解。
簡單需求分析:
1.實現無限級分類。
2.實現無限級鏈接導航
3.實現逐級分類下各條信息的查詢,包括最多瀏覽量,最多評論量,最新信息。
4.隨意轉移子分類到任何級別而不用修改分類下的信息表
5.使用最少的參數得到所要的信息,URL參數最好只有一個,比如cID=1或者ID=1
6.不管多少級,只有一個PHP文件實現類列表和各種方式的信息調用。
表爲兩張,一張分類表,一張信息表。
信息表如下:
`ID` int(10) unsigned NOT NULL auto_increment,
`cID` tinyint(3) unsigned NOT NULL default '0',
`title` varchar(255) NOT NULL default 'No Title',
`content` mediumtext NOT NULL,
最簡單的無限級分類數據表,只是設置一個parentID來判斷父ID
數據表如下:
`cID` tinyint(3) unsigned NOT NULL auto_increment,
`parentID` tinyint(3) unsigned NOT NULL default '0',
`order` tinyint(3) NOT NULL default '0',
`name` varchar(255) NOT NULL default '',
這樣可以根據cID = parentID來判斷上一級內容,運用遞歸至最頂層。
缺點是隻能查詢最小分類下的信息。這樣就不能完成需求3、4點,而第二點也勉強符合
第二種方法是設置parentID爲varchar類型,將父類id都集中在這個字段裏,用符號隔開,比如:1,3,6
這樣可以比較容易得到各上級分類的ID,而且在查詢分類下的信息的時候,可以使用如:Select * From information Where cID Like "1,3%"。這樣能比較好解決需求3。不過在添加分類和轉移分類的時候操作將非常麻煩。
我就說到這裏,請大家討論一下如何能夠最簡單的方法實現無限級分類——考慮性能,代碼的簡練性,前後臺操作的容易性,擴展性!
2、預排序遍歷樹算法
- --
- -- 表的結構 `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);
現在讓我們看一看另外一種不使用遞歸計算,更加快速的方法,這就是預排序遍歷樹算法(modified preorder tree traversal algorithm)
這種方法大家可能接觸的比較少,初次使用也不像上面的方法容易理解,但是由於這種方法不使用遞歸查詢算法,有更高的查詢效率。
我們首先將多級數據按照下面的方式畫在紙上,在根節點Food的左側寫上 1 然後沿着這個樹繼續向下 在 Fruit 的左側寫上 2 然後繼續前進,沿着整個樹的邊緣給每一個節點都標上左側和右側的數字。最後一個數字是標在Food 右側的 18。 在下面的這張圖中你可以看到整個標好了數字的多級結構。(沒有看懂?用你的手指指着數字從1數到18就明白怎麼回事了。還不明白,再數一遍,注意移動你的 手指)。
這些數字標明瞭各個節點之間的關係,"Red"的號是3和6,它是 "Food" 1-18 的子孫節點。 同樣,我們可以看到 所有左值大於2和右值小於11的節點 都是"Fruit" 2-11 的子孫節點
- 1 Food 18
- |
- +------------------------------+
- | |
- 2 Fruit 11 12 Meat 17
- | |
- +-------------+ +------------+
- | | | |
- 3 Red 6 7 Yellow 10 13 Beef 14 15 Pork 16
- | |
- 4 Cherry 5 8 Banana 9
這樣整個樹狀結構可以通過左右值來存儲到數據庫中。繼續之前,我們看一看下面整理過的數據表。
- +----------+------------+-----+-----+
- | parent | name | 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 |
- +----------+------------+-----+-----+
注意:由於"left"和"right"在 SQL中有特殊的意義 ,所以我們需要用"lft"和"rgt"來表示左右字段。 另外這種結構中不再需要"parent"字段來表示樹狀結構。也就是 說下面這樣的表結構就足夠了。
好了我們現在可以從數據庫中獲取數據了,例如我們需要得到"Fruit"項下的所有所有節點就可以這樣寫查詢語句:
- SELECT * FROM tree WHERE lft BETWEEN 2 AND 11;
這個查詢得到了以下的結果。
- +------------+-----+-----+
- | name | lft | rgt |
- +------------+-----+-----+
- | Fruit | 2 | 11 |
- | Red | 3 | 6 |
- | Cherry | 4 | 5 |
- | Yellow | 7 | 10 |
- | Banana | 8 | 9 |
- +------------+-----+-----+
要獲知一個節點的路徑就更簡單了,如果我們想知道Cherry 的路徑就利用它的左右值4和5來做一個查詢。
- SELECT name FROM tree WHERE lft < 4 AND rgt >; 5 ORDER BY lft ASC;
那麼某個節點到底有多少子孫節點呢?很簡單,子孫總數=(右值-左值-1)/2
用這個簡單的公式,我們可以很快的算出"Fruit 2-11"節點有4個子孫節點,而"Banana 8-9"節點沒有子孫節點,也就是說它不是一個父節點了。
那麼對於這樣的結構我們該如何增加,更新和刪除一個節點呢?
增加一個節點一般有兩種方法:
第一種,保留原有的name 和parent結構,用老方法向數據中添加數據,每增加一條數據以後使用rebuild_tree函數對整個結構重新進行一次編號。
第二種,效率更高的辦法是改變所有位於新節點右側的數值。舉例來說:我們想增加一種新的水果"Strawberry"(草莓)它將成爲"Red"節點的最 後一個子節點。首先我們需要爲它騰出一些空間。"Red"的右值應當從6改成8,"Yellow 7-10 "的左右值則應當改成 9-12。 依次類推我們可以得知,如果要給新的值騰出空間需要給所有左右值大於5的節點 (5 是"Red"最後一個子節點的右值) 加上2。 所以我們這樣進行數據庫操作:
- UPDATE tree SET rgt = rgt + 2 WHERE rgt > 5;
- UPDATE tree SET lft = lft + 2 WHERE lft > 5;
這樣就爲新插入的值騰出了空間,現在可以在騰出的空間裏建立一個新的數據節點了, 它的左右值分別是6和7
- INSERT INTO tree SET lft=6, rgt=7, name='Strawberry';
數據庫結構:
採用左右值編碼的保存該樹的數據記錄如下(設表名爲tree):
c_id | name | left_node | right_node
1 |商品 | 1 | 18
2 | 食品 | 2 | 11
3 | 肉類 | 3 | 6
4 | 豬肉 | 4 | 5
5 | 菜類 | 7 | 10
6 | 白菜 | 8 | 9
7 | 電器 | 2 | 17
8 | 電視 | 13 | 14
9 | 電棒 | 15 | 16
第一次看見上面的數據記錄,相信大部分人都不清楚左值(left_node)和右值(right_node)是根據什麼規則計算出來的,而且,這種表設計似乎沒有保存父節點的信息。下面把左右值和樹結合起來,請看:
1商品18
+---------------------------------------+
2食品11 12電器17
+-----------------+ +---------------------+
3肉類6 7菜類10 13電視14 15電棒16
4豬肉5 8白菜9
請用手指指着上圖中的數字,從1數到18,學習過數據結構的朋友肯定會發現什麼吧?對,你手指移動的順序就是對這棵樹的進行先序遍歷的順序。接下來,讓我講述一下如何利用節點的左右值,得到該節點的父節點,子孫節點數量,及自己在樹中的層數。
採用左右值編碼的設計方案,在進行類別樹的遍歷時,由於只需進行2次查詢,消除了遞歸,再加上查詢條件都爲數字比較,效率極高,類別樹的記錄條目越多,執行效率越高。
應用
某個節點到底有多少子孫節點?
子孫總數=(父節點的右值 - 父節點的左值-1)/2
以節點“食品”舉例,其子孫總數=(11-2-1)/ 2 = 4
如何判斷某一節點下有沒有子節點?
當 該節點左值-1 等於 其右值 時,其下沒有子節點。
檢索某一父節點的所有子節點?
假定我們要對節點“食品”及其子孫節點進行先序遍歷的列表,只需使用如下一條sql語句:
SELECT * FROM `tree` WHERE `left_node` BETWEEN 2 AND 11 ORDER BY `left_node` ASC
如何取得父類?
SELECT * FROM `tree` WHERE `left_node`<$left_node AND `right_node`>$right_node
檢索之後如何列表?
當左值+1==右值時,該節點沒有子節點,則下一節點不爲其子節點
若下一節點的左值==上一節點右值+1,則2個節點是同級關係
若下一節點的左值==上一節點的左值+1時,則第2個節點應是第一個節點的子節點
若下一節點的左值-上一節點的右值>1時,則下一節點比上一節點高
(下一節點的左值-上一節點的右值)
級
在某一父節點下添加一個子節點?
1. 要求該子節點爲該父節點下排序第一的節點,則$left_node = 父節點left_node+1, $right_node = $left_node+1;
2. 要求該節點位於父節點下一個子節點A後面,則$left_node = 節點A的right_node+1, $right_node = $left_node+1;
3. 要求該節點是位於父節點下排序最後一位的節點,則$left_node = 父節點right_node, $right_node = $left_node+1;
Sql:
UPDATE `tree` SET `right_node`=`right_node`+2 WHERE `right_node`>=$left_node
UPDATE `tree` SET `left_node`=`left_node`+2 WHERE `left_node`>=$left_node
INSERT INTO `tree` (`name` , `left_node` , `right_node`) VALUES
(`名字` , $left_node , $right_node)
移動節點,包括其子節點至節點A下?
設該節點左值$left_node , 右值$right_node
其子節點的數目爲$count = ($right_node - $left_node -1 )/2 , 節點A左值爲$A_left_node ,
UPDATE `tree` SET `right_node`=`right_node`-$right_node-$left_node-1 WHERE `right_node`>$right_node AND `right_node`<$A_left_node
UPDATE `tree` SET `left_node`=`left_node`-$right_node-$left_node-1 WHERE `left_node`>$right_node AND `left_node`<=$A_left_node
UPDATE `tree` SET `left_node`=`left_node`+$A_left_node-$right_node , `right_node`=`right_node`+$A_left_node-$right_node WHERE `left_node`>=$left_node
AND `right_node`<=$right_node
刪除所有子節點?
DELETE FROM `tree` WHERE `left_node`>父節點的左值 AND `right_node`>父節點的右值
刪除一個節點及其子節點?
在上例中的<號>號後面各加一個=號