1. 程式人生 > >Managing Hierarchical Data in MySQL(鄰接表模型)[轉載]

Managing Hierarchical Data in MySQL(鄰接表模型)[轉載]

wid 復雜 轉載 似的 之前 adjacency 標識 目的 縮進

原文在:http://dev.mysql.com/tech-resources/articles/hierarchical-data.html

來源: http://www.cnblogs.com/phaibin/archive/2009/06/09/1499687.html

譯文:Yimin

引言

大多數用戶都曾在數據庫中處理過分層數據(hierarchical data),認為分層數據的管理不是關系數據庫的目的。之所以這麽認為,是因為關系數據庫中的表沒有層次關系,只是簡單的平面化的列表;而分層數據具有父-子關系,顯然關系數據庫中的表不能自然地表現出其分層的特性。

我們認為,分層數據是每項只有一個父項和零個或多個子項(根項除外,根項沒有父項)的數據集合。分層數據存在於許多基於數據庫的應用程序中,包括論壇和郵件列表中的分類、商業組織圖表、內容管理系統的分類、產品分類。我們打算使用下面一個虛構的電子商店的產品分類:

技術分享

這些分類層次與上面提到的一些例子中的分類層次是相類似的。在本文中我們將從傳統的鄰接表(adjacency list)模型出發,闡述2種在MySQL中處理分層數據的模型。

鄰接表模型

上述例子的分類數據將被存儲在下面的數據表中(我給出了全部的數據表創建、數據插入的代碼,你可以跟著做):

CREATE TABLE category(
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
parent INT DEFAULT NULL);


INSERT INTO category
VALUES(1,‘ELECTRONICS‘,NULL),(2,‘TELEVISIONS‘,1),(3,‘TUBE‘,2),
(4,‘LCD‘,2),(5,‘PLASMA‘,2),(6,‘PORTABLE ELECTRONICS‘,1),
(7,‘MP3 PLAYERS‘,6),(8,‘FLASH‘,7),
(9,‘CD PLAYERS‘,6),(10,‘2 WAY RADIOS‘,6);

SELECT * FROM category ORDER BY category_id;

+-------------+----------------------+--------+
| category_id | name                 | parent |
+-------------+----------------------+--------+
|           1 | ELECTRONICS          |   NULL |
|           2 | TELEVISIONS          |      1 |
|           3 | TUBE                 |      2 |
|           4 | LCD                  |      2 |
|           5 | PLASMA               |      2 |
|           6 | PORTABLE ELECTRONICS |      1 |
|           7 | MP3 PLAYERS          |      6 |
|           8 | FLASH                |      7 |
|           9 | CD PLAYERS           |      6 |
|          10 | 2 WAY RADIOS         |      6 |
+-------------+----------------------+--------+
10 rows in set (0.00 sec)

在鄰接表模型中,數據表中的每項包含了指向其父項的指示器。在此例中,最上層項的父項為空值(NULL)。鄰接表模型的優勢在於它很簡單,可以很容易地看出FLASH是MP3 PLAYERS的子項,哪個是portable electronics的子項,哪個是electronics的子項。雖然,在客戶端編碼中鄰接表模型處理起來也相當的簡單,但是如果是純SQL編碼的話,該模型會有很多問題。

檢索整樹

通常在處理分層數據時首要的任務是,以某種縮進形式來呈現一棵完整的樹。為此,在純SQL編碼中通常的做法是使用自連接(self-join):

SELECT t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
LEFT JOIN category AS t4 ON t4.parent = t3.category_id
WHERE t1.name = ‘ELECTRONICS‘;

+-------------+----------------------+--------------+-------+
| lev1        | lev2                 | lev3         | lev4  |
+-------------+----------------------+--------------+-------+
| ELECTRONICS | TELEVISIONS          | TUBE         | NULL  |
| ELECTRONICS | TELEVISIONS          | LCD          | NULL  |
| ELECTRONICS | TELEVISIONS          | PLASMA       | NULL  |
| ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS  | FLASH |
| ELECTRONICS | PORTABLE ELECTRONICS | CD PLAYERS   | NULL  |
| ELECTRONICS | PORTABLE ELECTRONICS | 2 WAY RADIOS | NULL  |
+-------------+----------------------+--------------+-------+
6 rows in set (0.00 sec)

檢索所有葉子節點

我們可以用左連接(LEFT JOIN)來檢索出樹中所有葉子節點(沒有孩子節點的節點):

SELECT t1.name FROM
category AS t1 LEFT JOIN category as t2
ON t1.category_id = t2.parent
WHERE t2.category_id IS NULL;


+--------------+
| name         |
+--------------+
| TUBE         |
| LCD          |
| PLASMA       |
| FLASH        |
| CD PLAYERS   |
| 2 WAY RADIOS |
+--------------+

檢索單一路徑

通過自連接,我們也可以檢索出單一路徑:

SELECT t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
LEFT JOIN category AS t4 ON t4.parent = t3.category_id
WHERE t1.name = ‘ELECTRONICS‘ AND t4.name = ‘FLASH‘;

+-------------+----------------------+-------------+-------+
| lev1        | lev2                 | lev3        | lev4  |
+-------------+----------------------+-------------+-------+
| ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS | FLASH |
+-------------+----------------------+-------------+-------+
1 row in set (0.01 sec)

這種方法的主要局限是你需要為每層數據添加一個自連接,隨著層次的增加,自連接變得越來越復雜,檢索的性能自然而然的也就下降了。

鄰接表模型的局限性

用純SQL編碼實現鄰接表模型有一定的難度。在我們檢索某分類的路徑之前,我們需要知道該分類所在的層次。另外,我們在刪除節點的時候要特別小心,因為潛在的可能會孤立一棵子樹(當刪除portable electronics分類時,所有他的子分類都成了孤兒)。部分局限性可以通過使用客戶端代碼或者存儲過程來解決,我們可以從樹的底部開始向上叠代來獲得一顆樹或者單一路徑,我們也可以在刪除節點的時候使其子節點指向一個新的父節點,來防止孤立子樹的產生。

嵌套集合(Nested Set)模型

我想在這篇文章中重點闡述一種不同的方法,俗稱為嵌套集合模型。在嵌套集合模型中,我們將以一種新的方式來看待我們的分層數據,不再是線與點了,而是嵌套容器。我試著以嵌套容器的方式畫出了electronics分類圖:

技術分享

從上圖可以看出我們依舊保持了數據的層次,父分類包圍了其子分類。在數據表中,我們通過使用表示節點的嵌套關系的左值(left value)和右值(right value)來表現嵌套集合模型中數據的分層特性:

CREATE TABLE nested_category (
 category_id INT AUTO_INCREMENT PRIMARY KEY,
 name VARCHAR(20) NOT NULL,
 lft INT NOT NULL,
 rgt INT NOT NULL
);


INSERT INTO nested_category
VALUES(1,‘ELECTRONICS‘,1,20),(2,‘TELEVISIONS‘,2,9),(3,‘TUBE‘,3,4),
(4,‘LCD‘,5,6),(5,‘PLASMA‘,7,8),(6,‘PORTABLE ELECTRONICS‘,10,19),
(7,‘MP3 PLAYERS‘,11,14),(8,‘FLASH‘,12,13),
(9,‘CD PLAYERS‘,15,16),(10,‘2 WAY RADIOS‘,17,18);


SELECT * FROM nested_category ORDER BY category_id;


+-------------+----------------------+-----+-----+
| category_id | name                 | lft | rgt |
+-------------+----------------------+-----+-----+
|           1 | ELECTRONICS          |   1 |  20 |
|           2 | TELEVISIONS          |   2 |   9 |
|           3 | TUBE                 |   3 |   4 |
|           4 | LCD                  |   5 |   6 |
|           5 | PLASMA               |   7 |   8 |
|           6 | PORTABLE ELECTRONICS |  10 |  19 |
|           7 | MP3 PLAYERS          |  11 |  14 |
|           8 | FLASH                |  12 |  13 |
|           9 | CD PLAYERS           |  15 |  16 |
|          10 | 2 WAY RADIOS         |  17 |  18 |
+-------------+----------------------+-----+-----+

我們使用了lftrgt來代替left和right,是因為在MySQL中left和right是保留字。http://dev.mysql.com/doc/mysql/en/reserved-words.html,有一份詳細的MySQL保留字清單。

那麽,我們怎樣決定左值和右值呢?我們從外層節點的最左側開始,從左到右編號:

技術分享

這樣的編號方式也同樣適用於典型的樹狀結構:

技術分享

當我們為樹狀的結構編號時,我們從左到右,一次一層,為節點賦右值前先從左到右遍歷其子節點給其子節點賦左右值。這種方法被稱作改進的先序遍歷算法

檢索整樹

我們可以通過自連接把父節點連接到子節點上來檢索整樹,是因為子節點的lft值總是在其父節點的lft值和rgt值之間:

SELECT node.name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND parent.name = ‘ELECTRONICS‘
ORDER BY node.lft;


+----------------------+
| name                 |
+----------------------+
| ELECTRONICS          |
| TELEVISIONS          |
| TUBE                 |
| LCD                  |
| PLASMA               |
| PORTABLE ELECTRONICS |
| MP3 PLAYERS          |
| FLASH                |
| CD PLAYERS           |
| 2 WAY RADIOS         |
+----------------------+

不像先前鄰接表模型的例子,這個查詢語句不管樹的層次有多深都能很好的工作。在BETWEEN的子句中我們沒有去關心node的rgt值,是因為使用node的rgt值得出的父節點總是和使用lft值得出的是相同的。

檢索所有葉子節點

檢索出所有的葉子節點,使用嵌套集合模型的方法比鄰接表模型的LEFT JOIN方法簡單多了。如果你仔細得看了nested_category表,你可能已經註意到葉子節點的左右值是連續的。要檢索出葉子節點,我們只要查找滿足rgt=lft+1的節點:

SELECT name
FROM nested_category
WHERE rgt = lft + 1;


+--------------+
| name         |
+--------------+
| TUBE         |
| LCD          |
| PLASMA       |
| FLASH        |
| CD PLAYERS   |
| 2 WAY RADIOS |
+--------------+

檢索單一路徑

在嵌套集合模型中,我們可以不用多個自連接就可以檢索出單一路徑:

SELECT parent.name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = ‘FLASH‘
ORDER BY parent.lft;

+----------------------+
| name                 |
+----------------------+
| ELECTRONICS          |
| PORTABLE ELECTRONICS |
| MP3 PLAYERS          |
| FLASH                |
+----------------------+

檢索節點的深度

我們已經知道怎樣去呈現一棵整樹,但是為了更好的標識出節點在樹中所處層次,我們怎樣才能檢索出節點在樹中的深度呢?我們可以在先前的查詢語句上增加COUNT函數和GROUP BY子句來實現:

SELECT node.name, (COUNT(parent.name) - 1) AS depth
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;

+----------------------+-------+
| name                 | depth |
+----------------------+-------+
| ELECTRONICS          |     0 |
| TELEVISIONS          |     1 |
| TUBE                 |     2 |
| LCD                  |     2 |
| PLASMA               |     2 |
| PORTABLE ELECTRONICS |     1 |
| MP3 PLAYERS          |     2 |
| FLASH                |     3 |
| CD PLAYERS           |     2 |
| 2 WAY RADIOS         |     2 |
+----------------------+-------+

我們可以根據depth值來縮進分類名字,使用CONCAT和REPEAT字符串函數:

SELECT CONCAT( REPEAT(‘ ‘, COUNT(parent.name) - 1), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;

+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  PORTABLE ELECTRONICS |
|   MP3 PLAYERS         |
|    FLASH              |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
+-----------------------+

當然,在客戶端應用程序中你可能會用depth值來直接展示數據的層次。Web開發者會遍歷該樹,隨著depth值的增加和減少來添加<li></li>和<ul></ul>標簽。

檢索子樹的深度

當我們需要子樹的深度信息時,我們不能限制自連接中的node或parent,因為這麽做會打亂數據集的順序。因此,我們添加了第三個自連接作為子查詢,來得出子樹新起點的深度值:

SELECT node.name, (COUNT(parent.name) - (sub_tree.depth + 1)) AS depth
FROM nested_category AS node,
	nested_category AS parent,
	nested_category AS sub_parent,
	(
		SELECT node.name, (COUNT(parent.name) - 1) AS depth
		FROM nested_category AS node,
		nested_category AS parent
		WHERE node.lft BETWEEN parent.lft AND parent.rgt
		AND node.name = ‘PORTABLE ELECTRONICS‘
		GROUP BY node.name
		ORDER BY node.lft
	)AS sub_tree
WHERE node.lft BETWEEN parent.lft AND parent.rgt
	AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
	AND sub_parent.name = sub_tree.name
GROUP BY node.name
ORDER BY node.lft;


+----------------------+-------+
| name                 | depth |
+----------------------+-------+
| PORTABLE ELECTRONICS |     0 |
| MP3 PLAYERS          |     1 |
| FLASH                |     2 |
| CD PLAYERS           |     1 |
| 2 WAY RADIOS         |     1 |
+----------------------+-------+

這個查詢語句可以檢索出任一節點子樹的深度值,包括根節點。這裏的深度值跟你指定的節點有關。

檢索節點的直接子節點

可以想象一下,你在零售網站上呈現電子產品的分類。當用戶點擊分類後,你將要呈現該分類下的產品,同時也需列出該分類下的直接子分類,而不是該分類下的全部分類。為此,我們只呈現該節點及其直接子節點,不再呈現更深層次的節點。例如,當呈現PORTABLEELECTRONICS分類時,我們同時只呈現MP3 PLAYERS、CD PLAYERS和2 WAY RADIOS分類,而不呈現FLASH分類。

要實現它非常的簡單,在先前的查詢語句上添加HAVING子句:

SELECT node.name, (COUNT(parent.name) - (sub_tree.depth + 1)) AS depth
FROM nested_category AS node,
	nested_category AS parent,
	nested_category AS sub_parent,
	(
		SELECT node.name, (COUNT(parent.name) - 1) AS depth
		FROM nested_category AS node,
		nested_category AS parent
		WHERE node.lft BETWEEN parent.lft AND parent.rgt
		AND node.name = ‘PORTABLE ELECTRONICS‘
		GROUP BY node.name
		ORDER BY node.lft
	)AS sub_tree
WHERE node.lft BETWEEN parent.lft AND parent.rgt
	AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
	AND sub_parent.name = sub_tree.name
GROUP BY node.name
HAVING depth <= 1
ORDER BY node.lft;

+----------------------+-------+
| name                 | depth |
+----------------------+-------+
| PORTABLE ELECTRONICS |     0 |
| MP3 PLAYERS          |     1 |
| CD PLAYERS           |     1 |
| 2 WAY RADIOS         |     1 |
+----------------------+-------+

如果你不希望呈現父節點,你可以更改HAVING depth <= 1HAVING depth = 1

嵌套集合模型中集合函數的應用

讓我們添加一個產品表,我們可以使用它來示例集合函數的應用:

CREATE TABLE product(
product_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(40),
category_id INT NOT NULL
);


INSERT INTO product(name, category_id) VALUES(‘20" TV‘,3),(‘36" TV‘,3),
(‘Super-LCD 42"‘,4),(‘Ultra-Plasma 62"‘,5),(‘Value Plasma 38"‘,5),
(‘Power-MP3 5gb‘,7),(‘Super-Player 1gb‘,8),(‘Porta CD‘,9),(‘CD To go!‘,9),
(‘Family Talk 360‘,10);

SELECT * FROM product;

+------------+-------------------+-------------+
| product_id | name              | category_id |
+------------+-------------------+-------------+
|          1 | 20" TV            |           3 |
|          2 | 36" TV            |           3 |
|          3 | Super-LCD 42"     |           4 |
|          4 | Ultra-Plasma 62"  |           5 |
|          5 | Value Plasma 38"  |           5 |
|          6 | Power-MP3 128mb   |           7 |
|          7 | Super-Shuffle 1gb |           8 |
|          8 | Porta CD          |           9 |
|          9 | CD To go!         |           9 |
|         10 | Family Talk 360   |          10 |
+------------+-------------------+-------------+

現在,讓我們寫一個查詢語句,在檢索分類樹的同時,計算出各分類下的產品數量:

SELECT parent.name, COUNT(product.name)
FROM nested_category AS node ,
nested_category AS parent,
product
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.category_id = product.category_id
GROUP BY parent.name
ORDER BY node.lft;


+----------------------+---------------------+
| name                 | COUNT(product.name) |
+----------------------+---------------------+
| ELECTRONICS          |                  10 |
| TELEVISIONS          |                   5 |
| TUBE                 |                   2 |
| LCD                  |                   1 |
| PLASMA               |                   2 |
| PORTABLE ELECTRONICS |                   5 |
| MP3 PLAYERS          |                   2 |
| FLASH                |                   1 |
| CD PLAYERS           |                   2 |
| 2 WAY RADIOS         |                   1 |
+----------------------+---------------------+

這條查詢語句在檢索整樹的查詢語句上增加了COUNT和GROUP BY子句,同時在WHERE子句中引用了product表和一個自連接。

新增節點

到現在,我們已經知道了如何去查詢我們的樹,是時候去關註一下如何增加一個新節點來更新我們的樹了。讓我們再一次觀察一下我們的嵌套集合圖:

技術分享

當我們想要在TELEVISIONS和PORTABLE ELECTRONICS節點之間新增一個節點,新節點的lft和rgt 的 值為10和11,所有該節點的右邊節點的lft和rgt值都將加2,之後我們再添加新節點並賦相應的lft和rgt值。在MySQL 5中可以使用存儲過程來完成,我假設當前大部分讀者使用的是MySQL 4.1版本,因為這是最新的穩定版本。所以,我使用了鎖表(LOCK TABLES)語句來隔離查詢:

LOCK TABLE nested_category WRITE;


SELECT @myRight := rgt FROM nested_category
WHERE name = ‘TELEVISIONS‘;



UPDATE nested_category SET rgt = rgt + 2 WHERE rgt > @myRight;
UPDATE nested_category SET lft = lft + 2 WHERE lft > @myRight;

INSERT INTO nested_category(name, lft, rgt) VALUES(‘GAME CONSOLES‘, @myRight + 1, @myRight + 2);

UNLOCK TABLES;

我們可以檢驗一下新節點插入的正確性:

SELECT CONCAT( REPEAT( ‘ ‘, (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;


+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  GAME CONSOLES        |
|  PORTABLE ELECTRONICS |
|   MP3 PLAYERS         |
|    FLASH              |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
+-----------------------+

如果我們想要在葉子節點下增加節點,我們得稍微修改一下查詢語句。讓我們在2 WAYRADIOS葉子節點下添加FRS節點吧:

LOCK TABLE nested_category WRITE;

SELECT @myLeft := lft FROM nested_category

WHERE name = ‘2 WAY RADIOS‘;

UPDATE nested_category SET rgt = rgt + 2 WHERE rgt > @myLeft;
UPDATE nested_category SET lft = lft + 2 WHERE lft > @myLeft;

INSERT INTO nested_category(name, lft, rgt) VALUES(‘FRS‘, @myLeft + 1, @myLeft + 2);

UNLOCK TABLES;

在這個例子中,我們擴大了新產生的父節點(2 WAY RADIOS節點)的右值及其所有它的右邊節點的左右值,之後置新增節點於新父節點之下。正如你所看到的,我們新增的節點已經完全融入了嵌套集合中:

SELECT CONCAT( REPEAT( ‘ ‘, (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;


+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  GAME CONSOLES        |
|  PORTABLE ELECTRONICS |
|   MP3 PLAYERS         |
|    FLASH              |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
|    FRS                |
+-----------------------+

刪除節點

最後還有個基礎任務,刪除節點。刪除節點的處理過程跟節點在分層數據中所處的位置有關,刪除一個葉子節點比刪除一個子節點要簡單得多,因為刪除子節點的時候,我們需要去處理孤立節點。

刪除一個葉子節點的過程正好是新增一個葉子節點的逆過程,我們在刪除節點的同時該節點右邊所有節點的左右值和該父節點的右值都會減去該節點的寬度值:

LOCK TABLE nested_category WRITE;


SELECT @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
FROM nested_category
WHERE name = ‘GAME CONSOLES‘;


DELETE FROM nested_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;

UNLOCK TABLES;

我們再一次檢驗一下節點已經成功刪除,而且沒有打亂數據的層次:

SELECT CONCAT( REPEAT( ‘ ‘, (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;


+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  PORTABLE ELECTRONICS |
|   MP3 PLAYERS         |
|    FLASH              |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
|    FRS                |
+-----------------------+

這個方法可以完美地刪除節點及其子節點:

LOCK TABLE nested_category WRITE;


SELECT @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
FROM nested_category
WHERE name = ‘MP3 PLAYERS‘;


DELETE FROM nested_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;

UNLOCK TABLES;

再次驗證我們已經成功的刪除了一棵子樹:

SELECT CONCAT( REPEAT( ‘ ‘, (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;


+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  PORTABLE ELECTRONICS |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
|    FRS                |
+-----------------------+

有時,我們只刪除該節點,而不刪除該節點的子節點。在一些情況下,你希望改變其名字為占位符,直到替代名字的出現,比如你開除了一個主管(需要更換主管)。在另外一些情況下,你希望子節點掛到該刪除節點的父節點下:

LOCK TABLE nested_category WRITE;


SELECT @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
FROM nested_category
WHERE name = ‘PORTABLE ELECTRONICS‘;


DELETE FROM nested_category WHERE lft = @myLeft;


UPDATE nested_category SET rgt = rgt - 1, lft = lft - 1 WHERE lft BETWEEN @myLeft AND @myRight;
UPDATE nested_category SET rgt = rgt - 2 WHERE rgt > @myRight;
UPDATE nested_category SET lft = lft - 2 WHERE lft > @myRight;

UNLOCK TABLES;

在這個例子中,我們對該節點所有右邊節點的左右值都減去了2(因為不考慮其子節點,該節點的寬度為2),對該節點的子節點的左右值都減去了1(彌補由於失去父節點的左值造成的裂縫)。我們再一次確認,那些節點是否都晉升了:

SELECT CONCAT( REPEAT( ‘ ‘, (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;


+---------------+
| name          |
+---------------+
| ELECTRONICS   |
|  TELEVISIONS  |
|   TUBE        |
|   LCD         |
|   PLASMA      |
|  CD PLAYERS   |
|  2 WAY RADIOS |
|   FRS         |
+---------------+

有時,當刪除節點的時候,把該節點的一個子節點掛載到該節點的父節點下,而其他節點掛到該節點父節點的兄弟節點下,考慮到篇幅這種情況不在這裏解說了。

最後的思考

我希望這篇文章對你有所幫助,SQL中的嵌套集合的觀念大約有十年的歷史了,在網上和一些書中都能找到許多相關信息。在我看來,講述分層數據的管理最全面的,是來自一本名叫《Joe Celko‘s Trees and Hierarchies in SQL for Smarties》的書,此書的作者是在高級SQL領域倍受尊敬的Joe Celko。Joe Celko被認為是嵌套集合模型的創造者,更是該領域內的多產作家。我把Celko的書當作無價之寶,並極力地推薦它。在這本書中涵蓋了在此文中沒有提及的一些高級話題,也提到了其他一些關於鄰接表和嵌套集合模型下管理分層數據的方法。

在隨後的參考書目章節中,我列出了一些網絡資源,也許對你研究分層數據的管理會有所幫助,其中包括一些PHP相關的資源(處理嵌套集合的PHP庫)。如果你還在使用鄰接表模型,你該去試試嵌套集合模型了,在Storing Hierarchical Data in a Database 文中下方列出的一些資源鏈接中能找到一些樣例代碼,可以去試驗一下。

Managing Hierarchical Data in MySQL(鄰接表模型)[轉載]