[译] 在 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
相关文章
相关标签/搜索