MySQL中存儲與管理樹型數據結構

原載地址: 

http://www.wzjs163.com/tangshan/knowledge/mysql/38.html

介紹
大多數用戶經常在某些場合需要在SQL數據庫中存儲樹形結構數據,也清楚地知道管理樹型結構不是關係型數據庫的目標所在。關係型數據庫的表不是分層次的(像XML),而一個簡單平坦的列表。樹型層次數據有一個父節點關係,在關係型數據庫的表中不能自然的表示。

對我們來說,樹型結構數據是一個數據集合,每一個節點項目有一個單一的父節點、零個或多個子節點(根節點例外,沒有父節點)。樹型結構數據存在於很多數據庫應用當中,像論壇、郵件列表、商業組織圖、內容管理中的目錄,和產品目錄。我們將使用一個虛擬商店的電子產品目錄層次來說明問題:

 MySQL存儲樹型結構

這些目錄形成一個像上面介紹的樹型結構。本文我們將解釋兩種模型用以面對在MySQL中存儲樹型結構的問題,從傳統的“鄰接表(adjacency list)”模型開始。
 

鄰接表(adjacency list)模型
上面的目錄例子將像下面一樣存儲在表中: (其中包含所有CREATE 和 INSERT 語句,可以跟着做):

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)
在鄰接表(adjacency list)模型中, 表中的每一項都包含指向父親節點中的指針。本例中最頂級的元素,有一個 NULL 值做父元素節點。鄰接表(adjacency list)模型有一個好處就是非常簡單,很容易可以看到 FLASH 是 mp3 的一個子節點,mp3是 portable electronics的子節點, portable electronics是electronics的一個子節點。鄰接表(adjacency list)模型相對於客戶端代碼(client-side code)比較容易,但是使用純SQL有些問題。

獲取全部樹結構
第一個常見的任務是獲取整個樹型結構,通常用一定形式縮進。最常見的方式使用純粹的SQL是使用self-jion:

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 |
+--------------+

取得單條路徑
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' AND t4.name = 'FLASH';

+-------------+----------------------+-------------+-------+
| lev1        | lev2                 | lev3        | lev4  |
+-------------+----------------------+-------------+-------+
| ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS | FLASH |
+-------------+----------------------+-------------+-------+
1 row in set (0.01 sec)
此法的主要限制是層次中的每一級都要self-join,同時性能因爲每一級都要join變得低下。

鄰接表模型(Adjacency List Model)的限制
在純SQL中使用鄰接表模型是很困難的。在我們能夠看見目錄的完整路徑之前,我們必須知道它處於哪一級。另外,刪除節點時必須特別注意,因爲可能產生無雙親的完整的子樹(刪除portable electronics目錄時,它所有子節點變成無雙親的)。這些限制可通過客戶端代碼(client-side code)或存儲進程(stored procedures)來解決。通過一種過程式語言,我們可以在樹的底端開始向上遍歷,然後返回整個樹或一個完整的路徑。同樣可以使用過程式語言編程來刪除節點,爲了不使子樹失去雙親可以刪節點後對留下的子節點重新排序使它們指向新的父節點。

嵌套集合模型(The Nested Set Model)
下面關注一種完全不同的方法,一般被稱爲嵌套集合模型(the Nested Set Model),我們可以用一種新的方式看到層次結構,不是節點和行,而是嵌套的容器。試用下邊的方式畫出電子產品分類:
 MySQL存儲樹型結構

注意我們的層級是怎樣維護的:父目錄包含了它的孩子。我們通過在表中使用left和right值代表嵌套關係來描述這種層級結構:

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 |
+-------------+----------------------+-----+-----+
我們使用lft和rgt,因爲left和right是MySQL中的保留字,詳見: http://dev.mysql.com/doc/mysql/en/reserved-words.html 查看所有的保留字。

我們怎樣指定left和right值?從最外層的最左邊開始數,一直往右:

 MySQL存儲樹型結構

此設計同樣可以應用於一個典型的樹:

 MySQL存儲樹型結構

這個樹工作時,從左到右,每次一層,在指定右手值時向每個節點的子節點延伸,並移到右邊。這種方法叫做預排序遍歷樹算法 (the modified preorder tree traversal algorithm )。

獲取整個樹
我們可以通過使用一個自連結(self-join)獲取整個樹,根據連結父節點的節點其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         |
+----------------------+

不像我們之前的使用鄰接表模型(adjacency list model)的例子,此查詢工作時不考慮樹的深度。我們不必自己關心BETWEEN 子句中的rgt值,因爲the rgt value will always fall within the same parent as the lft values。

查詢所有葉子結點
查詢所有嵌套集合模型中的葉子節點比相鄰表模型(the adjacency list model)中的LEFT JOIN方法更簡單。如果你看一下nested_category表,你會注意到葉子節點的lft和rgt值是連續的。要查詢葉子節點,我們查看條件爲rgt = lft + 1的節點:

SELECT name
FROM nested_category
WHERE rgt = lft + 1;


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

獲取一個路徑
通過嵌套集合模型(the nested set model),我們可以不用多個自連結(self-joins)取得一個路徑:

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 |
+----------------------+-------+

我們可以使用深度值來縮進目錄名稱,通過使用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        |
+-----------------------+

當然,在一個客戶端應用中你很有可能直接顯示深度值。Web開發人員能夠循環整個樹,隨着深度值的增加和減少來增加<li></li>和 <ul></ul>標籤。

子樹的深度
當我們需要一個子樹的深度信息時,我們不能在self-join中限定子節點或父節點,那樣會“污染”查詢結果。而是,加入第三個self-join,連同一個子查詢一起決定子樹起點的深度:

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 |
+----------------------+-------+

此功能可以用於任何節點名稱,包括根節點。深度值總是相對於給定的節點。

查詢某個節點的子樹
現在假設你要顯示一個零售網站中的電子產品目錄,當用戶點擊一個目錄,你可能想顯示該目錄中的產品,同時顯示子目錄, 但不是全部的子樹。爲此,我們需要顯示本節點和它的子節點,但不繼續顯示它下邊的整個樹。例如,當顯示 PORTABLE ELECTRONICS 目錄時,我們想顯示 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 <= 1 改成 HAVING depth = 1。

嵌套集合中的集合功能(Aggregate Functions in a Nested Set)
加入一個產品表來顯示集合功能:

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 子句的整樹查詢,連同一個到product表的引用,和一個在 WHERE 子句中node、product表的連結。如你所見,每個目錄都有一個統計並且所有子目錄的統計數據出現在父目錄中。

添加新節點(Adding New Nodes)
現在我們知道如何查詢我們的樹,來看一下怎樣一個新節點。再來看一下我們的嵌套集合圖: 

MySQL存儲樹型結構

如果我們想在 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 WAY RADIOS節點下邊添加一個 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右邊所有節點的rgt值和lft值,再把新節點放在父親節點(left-hand value)的右邊。如下所示,新節點已正確的嵌入:

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                |
+-----------------------+

刪除節點(Deleting Nodes)
最後一個在嵌套集合使用中涉及的基本工作是節點的刪除。刪除節點的操作過程依賴於節點在層次中的位置,刪除葉子比刪除有孩子的節點更容易,因爲不需要處理變爲孤立節點的孩子節點。

當我們刪除葉子節點時,其過程恰恰是插入節點的反向操作,我們刪除節點和它的寬度(width)(原文:When deleting a leaf node, the process if just the opposite of adding a new node, we delete the node and its width from every node to its right):

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                |
+-----------------------+

我們需要處理的其它情況是刪除父節點而不是子節點。在一些情況下你可能希望把名稱設置爲一個點位符,在需要的時候再進行替換。諸如一個管理員人被解僱了(such as when a supervisor is fired)?另外有一些情況,子節點應該全部向上移到刪除的父節點的上一級:

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;

In this case we subtract two from all elements to the right of the node (since without children it would have a width of two), and one from the nodes that are its children (to close the gap created by the loss of the parent's left value). Once again, we can confirm our elements have been promoted:

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         |
+---------------+

其它的情況是刪除節點同時把一個子節點提升到父節點的位置,或把子節點移到父節點的兄弟姊妹之下,限於篇幅這些內容不在本文中討論。

最後的思考(Final Thoughts)
很希望本文的內容對你有用,SQL中嵌套集合的概念已經有了10幾年,在圖書中和互聯網上有很多相關的信息。在我看了,管理層次信息的最全面、綜合的是一本叫做 Joe Celko's Trees and Hierarchies in SQL for Smarties 的書,由在高級SQL領域很受尊敬的Joe Celko編寫。Joe Celko經常使用嵌套集合模型工作,在這方面是一位多產的作家。我發現Celko的書對我的學習研究來說是非常寶貴的資源,我強烈推薦它。書中包含了本文沒有包含的高級主題,也提供除了鄰接表(Adjacency List)和嵌套集合(Nested Set models)以外的其它方法管理層次數據。

在參考文獻/資源一節我列出了一些web資源,你可能在管理層次數據時用到它們。包一對PHP相關的資源,包含了一些pre-built PHP庫用於處理MySQL中的嵌套集合。那些正在使用鄰接表模型(adjacency list model)並且願意嘗試嵌套集合模型(the netsted set model)的人,可以在下邊的Storing Hierarchical Data in a Database 資源列表中發現一些用於轉換的例子代碼。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章