[譯] 在 MySQL 中管理層級數據 —— 兩種方法介紹

原文: mikehillyer.com/articles/ma…php

介紹

大多數開發者都或多或少地在SQL數據庫當中處理過層級結構數據,而且意識到管理層級數據並非一個關係型數據庫所擅長的。一個關係型數據庫中的表不是層級結構的(好比XML),而是一個簡單的平鋪列表。層級數據擁有一個父-子關係,然而一張關係型數據庫表不能天然地表示它。html

在咱們的這篇主題介紹當中,層級數據是一個這樣的集合,每項有一個單一的父親以及0個或者更多的孩子(根節點是例外。它沒有父親)。層級數據在許多的數據庫應用中被普遍使用。包括論壇和郵件列表,商業組織圖,內容管理分類和產品分類。咱們將使用下面的來自一個虛擬電子商店的產品分類層級結構來介紹咱們的主題。node

這些分類構成了一個與以前所說起的例子至關相似的層級結構。在本篇文章當中,咱們將檢查兩種在MySQL中用於處理層級數據的模型。首先咱們從傳統的鄰接表模型開始。mysql

鄰接列表模型(The Adjacency List Model)

一般上面例子中的分類將會被存儲在以下的一張表當中(我會把完整的建立和插入語句包含進去,以便你能夠跟着操做)算法

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)
複製代碼

在鄰接列表模型中,表中的每一項都包含一個指向它父節點的指針。在這個例子當中最頂層的元素electronics父節點的值是NULL。鄰接列表模型擁有簡單易理解的優勢,你能夠很容易地看出FLASHMP3 PLAYERS的孩子,而後MP3 PLAYERS又是PORTABLE ELECTRONICS的孩子,PORTABLE ELECTRONICS又是ELECTRONICS的孩子。儘管鄰接列表模型在客戶端代碼當中能夠至關容易地被處理,可是使用純SQL則會產生許多難題。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)查詢找到全部的葉子節點(沒有任何孩子的節點)bash

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)
複製代碼

這樣一種方法主要的缺陷在於你須要對層級結構中的每一級作自聯接,這樣將會致使當層級增多,自聯接變得複雜時,查詢性能會天然地下降。electron

鄰接列表模型的缺陷

在純SQL中使用鄰接列表模型多是比較困難的。在查看一個分類的完整路徑以前,咱們須要知道它在哪一層級。除此以外,咱們須要注意刪除節點。由於有可能在處理過程中一整課子樹會成爲孤兒(刪除portable electronics分類,它的孩子將會成爲孤兒)。這其中的一些缺陷能夠經過使用客戶端代碼或者存儲過程解決。經過使用程序語言,咱們能夠從一棵樹的底部向上遍從來返回完整的樹或者一條單一路徑。咱們也能夠經過提高一個孩子元素,而後從新安置剩下的孩子指向新的父親,來刪除節點而不會致使孤兒的產生。

嵌套集模型(The Nested Set Model)

我在這篇文章想着重介紹的是一種另外的方法,一般它被稱之爲嵌套集模型(The Nested Set Model)。在嵌套集模型當中,咱們以一種新的視角來看待咱們的層級結構。不是以節點和線的方式,而是以嵌套容器的方式。試着用這種方法描述電子產品分類:

請注意咱們的層級結構是如何被繼續維持住的,由於父分類包含了他們的孩子。咱們經過使用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 |
+-------------+----------------------+-----+-----+
複製代碼

因爲leftright是MySQL裏面的保留字,所以咱們使用lftrgt。請在dev.mysql.com/doc/mysql/e…查看完整的保留字列表。

那麼咱們如何決定left和right的值呢?咱們從外部節點最左側開始,而後一直向右編號。

這種設計也能夠應用到一棵典型的樹:

當咱們處理一棵樹時,咱們從左往右,一層一層地進行。在給節點分配一個右手邊數字和向右移動以前,咱們先降低到它的孩子節點。這種方法被稱之爲先序遍歷算法

獲取一棵完整的樹

咱們能夠經過使用將父節點與子節點連接起來的自聯接來檢索完整的樹。它的原理基於一個節點的lft值必定在它父親lftrgt值之間。

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子句注意節點的rgt值,由於它和lft值同樣也必定會落在同一個父親內。

查找全部的葉子節點

相比在鄰接列表模型當中使用左聯結方法,在嵌套集模型當中查找全部的葉子節點是更加簡單的。若是你仔細觀察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 |
+--------------+
複製代碼

獲取一條路徑

經過使用嵌套集模型,咱們能夠在不使用多條自聯結的狀況下獲取一條路徑:

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>標籤。

一棵子樹的深度

當咱們須要子樹的深度信息時,咱們不能限制自鏈接中的節點或父表,由於它會破壞咱們的結果。相反,咱們添加第三個自鏈接以及一個子查詢來肯定將成爲子樹的新起點的深度:

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

嵌套集中的聚合函數

讓咱們添加一個產品以用來演示聚合函數:

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子句中節點和產品表之間的聯結。 正如你所看到的那樣,每個分類都有對應的數量,同時子分類的數量被反映到了父分類當中。

添加新節點

既然咱們已經學習瞭如何查詢樹,咱們如今應該看看如何經過添加一個新節點來更新樹。讓咱們再來看看嵌套集圖:

若是咱們想要在TELEVISIONS和PORTABLE ELECTRONICS節點之間添加新節點,它的lft和rgt值應該分別是10和11,而且它的全部右邊的節點的lft和rgt值都須要增長2。而後,咱們將使用適當的lft和rgt值添加新節點。儘管在MYSQL 5中可使用存儲過程完成這些,但我仍是假設大多數的讀者正在使用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;
複製代碼

在這一例中,咱們擴展了新的父節點右邊的節點數值,而後把節點。正如你所見,咱們的新節點如今被嵌套住了:

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(爲了填補父親lft值減小所形成的差距)。如今讓咱們再一次確認元素已經被提高了:

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嵌套集的概念已經出現超過10年了,因此網上有許多書中包含許多額外可用的信息。我的認爲管理層級結構最全面詳盡的介紹是一本叫作Joe Celko’s Trees and Hierarchies in SQL for Smarties的書。它是由一位在高級SQL領域很是值得尊敬的做者Joe Celko編寫。嵌套集模型常常被歸功於Joe Celko,而且他是至今爲止在這個主題上產量最高的做者。我發現Celko的書是在個人研究當中無價的資源,所以我強烈推薦它。這本書涵蓋了許多我在本篇文章我沒有涉及的高級主題,同時它提供了許多包括鄰接列表模型和嵌套集模型的管理層級結構的方法。

在接下來的引用/資源部分,我列出一些可能對你研究管理層級結構數據有用的網絡資源,它包括一對PHP相關的資源以及在MySQL中處理嵌套集的PHP預構建庫。那些目前使用鄰接列表模型並想要試驗嵌套集模型的人能夠在下面列出的資源中的Storing Hierarchical Data in a Database 找到用於在二者之間進行轉換的示例代碼。

引用 / 資源

  • Joe Celko’s Trees and Hierarchies in SQL for Smarties – ISBN 1-55860-920-2
  • Storing Hierarchical Data in a Database
  • A Look at SQL Trees
  • SQL Lessons
  • Nontraditional Databases
  • Trees in SQL
  • Trees in SQL (2)
  • Converting an adjacency list model to a nested set model Nested Sets in PHP Demonstration (German) A Nested Set Library in PHP
相關文章
相關標籤/搜索