原文:http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/

引言

大多数用户都曾在数据库中处理过分层数据(hierarchical data),认为分层数据的管理不是关系数据库的目的。之所以这么认为,是因为关系数据库中的表没有层次关系,只是简单的平面化的列表;而分层数据具有父-子关系,显然关系数据库中的表不能自然地表现出其分层的特性。

我们认为,分层数据是每项只有一个父项和零个或多个子项(根项除外,根项没有父项)的数据集合。分层数据存在于许多基于数据库的应用程序中,包括论坛和邮件列表中的分类、商业组织图表、内容管理系统的分类、产品分类。我们打算使用下面一个虚构的电子商店的产品分类:

这些分类层次与上面提到的一些例子中的分类层次是相类似的。在本文中我们将从传统的邻接表(adjacency list)模型出发,阐述2种在MySQL中处理分层数据的模型。

邻接表模型

上述例子的分类数据将被存储在下面的数据表中(我给出了全部的数据表创建、数据插入的代码,你可以跟着做):、

  1. CREATE TABLE category(
  2. category_id INT AUTO_INCREMENT PRIMARY KEY,
  3. name VARCHAR(20) NOT NULL,
  4. parent INT DEFAULT NULL);
  5.  
  6. INSERT INTO category
  7. VALUES(1,'ELECTRONICS',NULL),(2,'TELEVISIONS',1),(3,'TUBE',2),
  8. (4,'LCD',2),(5,'PLASMA',2),(6,'PORTABLE ELECTRONICS',1),
  9. (7,'MP3 PLAYERS',6),(8,'FLASH',7),
  10. (9,'CD PLAYERS',6),(10,'2 WAY RADIOS',6);
  11.  
  12. SELECT * FROM category ORDER BY category_id;
  13.  
  14. +-------------+----------------------+--------+
  15. | category_id | name | parent |
  16. +-------------+----------------------+--------+
  17. | 1 | ELECTRONICS | NULL |
  18. | 2 | TELEVISIONS | 1 |
  19. | 3 | TUBE | 2 |
  20. | 4 | LCD | 2 |
  21. | 5 | PLASMA | 2 |
  22. | 6 | PORTABLE ELECTRONICS | 1 |
  23. | 7 | MP3 PLAYERS | 6 |
  24. | 8 | FLASH | 7 |
  25. | 9 | CD PLAYERS | 6 |
  26. | 10 | 2 WAY RADIOS | 6 |
  27. +-------------+----------------------+--------+
  28. 10 rows in set (0.00 sec)

在邻接表模型中,数据表中的每项包含了指向其父项的指示器。在此例中,最上层项的父项为空值(NULL)。邻接表模型的优势在于它很简单,可以很容易地看出FLASH是MP3 PLAYERS的子项,哪个是portable electronics的子项,哪个是electronics的子项。虽然,在客户端编码中邻接表模型处理起来也相当的简单,但是如果是纯SQL编码的话,该模型会有很多问题。

检索整树

通常在处理分层数据时首要的任务是,以某种缩进形式来呈现一棵完整的树。为此,在纯SQL编码中通常的做法是使用自连接(self-join):

  1. SELECT t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
  2. FROM category AS t1
  3. LEFT JOIN category AS t2 ON t2.parent = t1.category_id
  4. LEFT JOIN category AS t3 ON t3.parent = t2.category_id
  5. LEFT JOIN category AS t4 ON t4.parent = t3.category_id
  6. WHERE t1.name = 'ELECTRONICS';
  7.  
  8. +-------------+----------------------+--------------+-------+
  9. | lev1 | lev2 | lev3 | lev4 |
  10. +-------------+----------------------+--------------+-------+
  11. | ELECTRONICS | TELEVISIONS | TUBE | NULL |
  12. | ELECTRONICS | TELEVISIONS | LCD | NULL |
  13. | ELECTRONICS | TELEVISIONS | PLASMA | NULL |
  14. | ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS | FLASH |
  15. | ELECTRONICS | PORTABLE ELECTRONICS | CD PLAYERS | NULL |
  16. | ELECTRONICS | PORTABLE ELECTRONICS | 2 WAY RADIOS | NULL |
  17. +-------------+----------------------+--------------+-------+
  18. 6 rows in set (0.00 sec)

检索所有叶子节点

我们可以用左连接(LEFT JOIN)来检索出树中所有叶子节点(没有孩子节点的节点):

  1. SELECT t1.name FROM
  2. category AS t1 LEFT JOIN category as t2
  3. ON t1.category_id = t2.parent
  4. WHERE t2.category_id IS NULL;
  5.  
  6. +--------------+
  7. | name |
  8. +--------------+
  9. | TUBE |
  10. | LCD |
  11. | PLASMA |
  12. | FLASH |
  13. | CD PLAYERS |
  14. | 2 WAY RADIOS |
  15. +--------------+

为什么加where,不加where

检索单一路径

通过自连接,我们也可以检索出单一路径:

  1. SELECT t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
  2. FROM category AS t1
  3. LEFT JOIN category AS t2 ON t2.parent = t1.category_id
  4. LEFT JOIN category AS t3 ON t3.parent = t2.category_id
  5. LEFT JOIN category AS t4 ON t4.parent = t3.category_id
  6. WHERE t1.name = 'ELECTRONICS' AND t4.name = 'FLASH';
  7.  
  8. +-------------+----------------------+-------------+-------+
  9. | lev1 | lev2 | lev3 | lev4 |
  10. +-------------+----------------------+-------------+-------+
  11. | ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS | FLASH |
  12. +-------------+----------------------+-------------+-------+
  13. 1 row in set (0.01 sec)

这种方法的主要局限是你需要为每层数据添加一个自连接,随着层次的增加,自连接变得越来越复杂,检索的性能自然而然的也就下降了。

邻接表模型的局限性

用纯SQL编码实现邻接表模型有一定的难度。在我们检索某分类的路径之前,我们需要知道该分类所在的层次。另外,我们在删除节点的时候要特别小心,因为潜在的可能会孤立一棵子树(当删除portable electronics分类时,所有他的子分类都成了孤儿)。部分局限性可以通过使用客户端代码或者存储过程来解决,我们可以从树的底部开始向上迭代来获得一颗树或者单一路径,我们也可以在删除节点的时候使其子节点指向一个新的父节点,来防止孤立子树的产生。

嵌套集合(Nested Set)模型

我想在这篇文章中重点阐述一种不同的方法,俗称为嵌套集合模型。在嵌套集合模型中,我们将以一种新的方式来看待我们的分层数据,不再是线与点了,而是嵌套容器。我试着以嵌套容器的方式画出了electronics分类图:

从上图可以看出我们依旧保持了数据的层次,父分类包围了其子分类。在数据表中,我们通过使用表示节点的嵌套关系的左值(left value)和右值(right value)来表现嵌套集合模型中数据的分层特性:

  1. CREATE TABLE nested_category (
  2. category_id INT AUTO_INCREMENT PRIMARY KEY,
  3. name VARCHAR(20) NOT NULL,
  4. lft INT NOT NULL,
  5. rgt INT NOT NULL
  6. );
  7.  
  8. INSERT INTO nested_category
  9. VALUES(1,'ELECTRONICS',1,20),(2,'TELEVISIONS',2,9),(3,'TUBE',3,4),
  10. (4,'LCD',5,6),(5,'PLASMA',7,8),(6,'PORTABLE ELECTRONICS',10,19),
  11. (7,'MP3 PLAYERS',11,14),(8,'FLASH',12,13),
  12. (9,'CD PLAYERS',15,16),(10,'2 WAY RADIOS',17,18);
  13.  
  14. SELECT * FROM nested_category ORDER BY category_id;
  15.  
  16. +-------------+----------------------+-----+-----+
  17. | category_id | name | lft | rgt |
  18. +-------------+----------------------+-----+-----+
  19. | 1 | ELECTRONICS | 1 | 20 |
  20. | 2 | TELEVISIONS | 2 | 9 |
  21. | 3 | TUBE | 3 | 4 |
  22. | 4 | LCD | 5 | 6 |
  23. | 5 | PLASMA | 7 | 8 |
  24. | 6 | PORTABLE ELECTRONICS | 10 | 19 |
  25. | 7 | MP3 PLAYERS | 11 | 14 |
  26. | 8 | FLASH | 12 | 13 |
  27. | 9 | CD PLAYERS | 15 | 16 |
  28. | 10 | 2 WAY RADIOS | 17 | 18 |
  29. +-------------+----------------------+-----+-----+

我们使用了lftrgt来代替left和right,是因为在MySQL中left和right是保留字。http://dev.mysql.com/doc/mysql/en/reserved-words.html,有一份详细的MySQL保留字清单。

那么,我们怎样决定左值和右值呢?我们从外层节点的最左侧开始,从左到右编号:

这样的编号方式也同样适用于典型的树状结构:

当我们为树状的结构编号时,我们从左到右,一次一层,为节点赋右值前先从左到右遍历其子节点给其子节点赋左右值。这种方法被称作改进的先序遍历算法

检索整树

我们可以通过自连接把父节点连接到子节点上来检索整树,是因为子节点的lft值总是在其父节点的lft值和rgt值之间:

  1. SELECT node.name
  2. FROM nested_category AS node,
  3. nested_category AS parent
  4. WHERE node.lft BETWEEN parent.lft AND parent.rgt
  5. AND parent.name = 'ELECTRONICS'
  6. ORDER BY node.lft;
  7.  
  8. +----------------------+
  9. | name |
  10. +----------------------+
  11. | ELECTRONICS |
  12. | TELEVISIONS |
  13. | TUBE |
  14. | LCD |
  15. | PLASMA |
  16. | PORTABLE ELECTRONICS |
  17. | MP3 PLAYERS |
  18. | FLASH |
  19. | CD PLAYERS |
  20. | 2 WAY RADIOS |
  21. +----------------------+

不像先前邻接表模型的例子,这个查询语句不管树的层次有多深都能很好的工作。在BETWEEN的子句中我们没有去关心node的rgt值,是因为使用node的rgt值得出的父节点总是和使用lft值得出的是相同的。

检索所有叶子节点

检索出所有的叶子节点,使用嵌套集合模型的方法比邻接表模型的LEFT JOIN方法简单多了。如果你仔细得看了nested_category表,你可能已经注意到叶子节点的左右值是连续的。要检索出叶子节点,我们只要查找满足rgt=lft+1的节点:

  1. SELECT name
  2. FROM nested_category
  3. WHERE rgt = lft + 1;
  4.  
  5. +--------------+
  6. | name |
  7. +--------------+
  8. | TUBE |
  9. | LCD |
  10. | PLASMA |
  11. | FLASH |
  12. | CD PLAYERS |
  13. | 2 WAY RADIOS |
  14. +--------------+

检索单一路径

在嵌套集合模型中,我们可以不用多个自连接就可以检索出单一路径:

  1. SELECT parent.name
  2. FROM nested_category AS node,
  3. nested_category AS parent
  4. WHERE node.lft BETWEEN parent.lft AND parent.rgt
  5. AND node.name = 'FLASH'
  6. ORDER BY parent.lft;
  7.  
  8. +----------------------+
  9. | name |
  10. +----------------------+
  11. | ELECTRONICS |
  12. | PORTABLE ELECTRONICS |
  13. | MP3 PLAYERS |
  14. | FLASH |
  15. +----------------------+

检索节点的深度

我们已经知道怎样去呈现一棵整树,但是为了更好的标识出节点在树中所处层次,我们怎样才能检索出节点在树中的深度呢?我们可以在先前的查询语句上增加COUNT函数和GROUP BY子句来实现:

  1. SELECT node.name, (COUNT(parent.name) - 1) AS depth
  2. FROM nested_category AS node,
  3. nested_category AS parent
  4. WHERE node.lft BETWEEN parent.lft AND parent.rgt
  5. GROUP BY node.name
  6. ORDER BY node.lft;
  7.  
  8. +----------------------+-------+
  9. | name | depth |
  10. +----------------------+-------+
  11. | ELECTRONICS | 0 |
  12. | TELEVISIONS | 1 |
  13. | TUBE | 2 |
  14. | LCD | 2 |
  15. | PLASMA | 2 |
  16. | PORTABLE ELECTRONICS | 1 |
  17. | MP3 PLAYERS | 2 |
  18. | FLASH | 3 |
  19. | CD PLAYERS | 2 |
  20. | 2 WAY RADIOS | 2 |
  21. +----------------------+-------+

我们可以根据depth值来缩进分类名字,使用CONCAT和REPEAT字符串函数:

  1. SELECT CONCAT( REPEAT(' ', COUNT(parent.name) - 1), node.name) AS name
  2. FROM nested_category AS node,
  3. nested_category AS parent
  4. WHERE node.lft BETWEEN parent.lft AND parent.rgt
  5. GROUP BY node.name
  6. ORDER BY node.lft;
  7.  
  8. +-----------------------+
  9. | name |
  10. +-----------------------+
  11. | ELECTRONICS |
  12. | TELEVISIONS |
  13. | TUBE |
  14. | LCD |
  15. | PLASMA |
  16. | PORTABLE ELECTRONICS |
  17. | MP3 PLAYERS |
  18. | FLASH |
  19. | CD PLAYERS |
  20. | 2 WAY RADIOS |
  21. +-----------------------+

当然,在客户端应用程序中你可能会用depth值来直接展示数据的层次。Web开发者会遍历该树,随着depth值的增加和减少来添加<li></li>和<ul></ul>标签。

检索子树的深度

当我们需要子树的深度信息时,我们不能限制自连接中的node或parent,因为这么做会打乱数据集的顺序。因此,我们添加了第三个自连接作为子查询,来得出子树新起点的深度值:

  1. SELECT node.name, (COUNT(parent.name) - (sub_tree.depth + 1)) AS depth
  2. FROM nested_category AS node,
  3. nested_category AS parent,
  4. nested_category AS sub_parent,
  5. (
  6. SELECT node.name, (COUNT(parent.name) - 1) AS depth
  7. FROM nested_category AS node,
  8. nested_category AS parent
  9. WHERE node.lft BETWEEN parent.lft AND parent.rgt
  10. AND node.name = 'PORTABLE ELECTRONICS'
  11. GROUP BY node.name
  12. ORDER BY node.lft
  13. )AS sub_tree
  14. WHERE node.lft BETWEEN parent.lft AND parent.rgt
  15. AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
  16. AND sub_parent.name = sub_tree.name
  17. GROUP BY node.name
  18. ORDER BY node.lft;
  19.  
  20. +----------------------+-------+
  21. | name | depth |
  22. +----------------------+-------+
  23. | PORTABLE ELECTRONICS | 0 |
  24. | MP3 PLAYERS | 1 |
  25. | FLASH | 2 |
  26. | CD PLAYERS | 1 |
  27. | 2 WAY RADIOS | 1 |
  28. +----------------------+-------+

这个查询语句可以检索出任一节点子树的深度值,包括根节点。这里的深度值跟你指定的节点有关。

检索节点的直接子节点

可以想象一下,你在零售网站上呈现电子产品的分类。当用户点击分类后,你将要呈现该分类下的产品,同时也需列出该分类下的直接子分类,而不是该分类下的全部分类。为此,我们只呈现该节点及其直接子节点,不再呈现更深层次的节点。例如,当呈现PORTABLEELECTRONICS分类时,我们同时只呈现MP3 PLAYERS、CD PLAYERS和2 WAY RADIOS分类,而不呈现FLASH分类。

要实现它非常的简单,在先前的查询语句上添加HAVING子句:

  1. SELECT node.name, (COUNT(parent.name) - (sub_tree.depth + 1)) AS depth
  2. FROM nested_category AS node,
  3. nested_category AS parent,
  4. nested_category AS sub_parent,
  5. (
  6. SELECT node.name, (COUNT(parent.name) - 1) AS depth
  7. FROM nested_category AS node,
  8. nested_category AS parent
  9. WHERE node.lft BETWEEN parent.lft AND parent.rgt
  10. AND node.name = 'PORTABLE ELECTRONICS'
  11. GROUP BY node.name
  12. ORDER BY node.lft
  13. )AS sub_tree
  14. WHERE node.lft BETWEEN parent.lft AND parent.rgt
  15. AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
  16. AND sub_parent.name = sub_tree.name
  17. GROUP BY node.name
  18. HAVING depth <= 1
  19. ORDER BY node.lft;
  20.  
  21. +----------------------+-------+
  22. | name | depth |
  23. +----------------------+-------+
  24. | PORTABLE ELECTRONICS | 0 |
  25. | MP3 PLAYERS | 1 |
  26. | CD PLAYERS | 1 |
  27. | 2 WAY RADIOS | 1 |
  28. +----------------------+-------+

如果你不希望呈现父节点,你可以更改HAVING depth <= 1HAVING depth = 1

嵌套集合模型中集合函数的应用

让我们添加一个产品表,我们可以使用它来示例集合函数的应用:

  1. CREATE TABLE product(
  2. product_id INT AUTO_INCREMENT PRIMARY KEY,
  3. name VARCHAR(40),
  4. category_id INT NOT NULL
  5. );
  6.  
  7. INSERT INTO product(name, category_id) VALUES('20" TV',3),('36" TV',3),
  8. ('Super-LCD 42"',4),('Ultra-Plasma 62"',5),('Value Plasma 38"',5),
  9. ('Power-MP3 5gb',7),('Super-Player 1gb',8),('Porta CD',9),('CD To go!',9),
  10. ('Family Talk 360',10);
  11.  
  12. SELECT * FROM product;
  13.  
  14. +------------+-------------------+-------------+
  15. | product_id | name | category_id |
  16. +------------+-------------------+-------------+
  17. | 1 | 20" TV | 3 |
  18. | 2 | 36" TV | 3 |
  19. | 3 | Super-LCD 42" | 4 |
  20. | 4 | Ultra-Plasma 62" | 5 |
  21. | 5 | Value Plasma 38" | 5 |
  22. | 6 | Power-MP3 128mb | 7 |
  23. | 7 | Super-Shuffle 1gb | 8 |
  24. | 8 | Porta CD | 9 |
  25. | 9 | CD To go! | 9 |
  26. | 10 | Family Talk 360 | 10 |
  27. +------------+-------------------+-------------+

现在,让我们写一个查询语句,在检索分类树的同时,计算出各分类下的产品数量:

  1. SELECT parent.name, COUNT(product.name)
  2. FROM nested_category AS node ,
  3. nested_category AS parent,
  4. product
  5. WHERE node.lft BETWEEN parent.lft AND parent.rgt
  6. AND node.category_id = product.category_id
  7. GROUP BY parent.name
  8. ORDER BY node.lft;
  9.  
  10. +----------------------+---------------------+
  11. | name | COUNT(product.name) |
  12. +----------------------+---------------------+
  13. | ELECTRONICS | 10 |
  14. | TELEVISIONS | 5 |
  15. | TUBE | 2 |
  16. | LCD | 1 |
  17. | PLASMA | 2 |
  18. | PORTABLE ELECTRONICS | 5 |
  19. | MP3 PLAYERS | 2 |
  20. | FLASH | 1 |
  21. | CD PLAYERS | 2 |
  22. | 2 WAY RADIOS | 1 |
  23. +----------------------+---------------------+

这条查询语句在检索整树的查询语句上增加了COUNT和GROUP BY子句,同时在WHERE子句中引用了product表和一个自连接。

新增节点

到现在,我们已经知道了如何去查询我们的树,是时候去关注一下如何增加一个新节点来更新我们的树了。让我们再一次观察一下我们的嵌套集合图:

当我们想要在TELEVISIONS和PORTABLE ELECTRONICS节点之间新增一个节点,新节点的lft和rgt 的 值为10和11,所有该节点的右边节点的lft和rgt值都将加2,之后我们再添加新节点并赋相应的lft和rgt值。在MySQL 5中可以使用存储过程来完成,我假设当前大部分读者使用的是MySQL 4.1版本,因为这是最新的稳定版本。所以,我使用了锁表(LOCK TABLES)语句来隔离查询:

  1. LOCK TABLE nested_category WRITE;
  2.  
  3. SELECT @myRight := rgt FROM nested_category
  4. WHERE name = 'TELEVISIONS';
  5.  
  6. UPDATE nested_category SET rgt = rgt + 2 WHERE rgt > @myRight;
  7. UPDATE nested_category SET lft = lft + 2 WHERE lft > @myRight;
  8.  
  9. INSERT INTO nested_category(name, lft, rgt) VALUES('GAME CONSOLES', @myRight + 1, @myRight + 2);
  10.  
  11. UNLOCK TABLES;
  12.  
  13. 我们可以检验一下新节点插入的正确性:
  14.  
  15. SELECT CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
  16. FROM nested_category AS node,
  17. nested_category AS parent
  18. WHERE node.lft BETWEEN parent.lft AND parent.rgt
  19. GROUP BY node.name
  20. ORDER BY node.lft;
  21.  
  22. +-----------------------+
  23. | name |
  24. +-----------------------+
  25. | ELECTRONICS |
  26. | TELEVISIONS |
  27. | TUBE |
  28. | LCD |
  29. | PLASMA |
  30. | GAME CONSOLES |
  31. | PORTABLE ELECTRONICS |
  32. | MP3 PLAYERS |
  33. | FLASH |
  34. | CD PLAYERS |
  35. | 2 WAY RADIOS |
  36. +-----------------------+

如果我们想要在叶子节点下增加节点,我们得稍微修改一下查询语句。让我们在2 WAYRADIOS叶子节点下添加FRS节点吧:

  1. LOCK TABLE nested_category WRITE;
  2.  
  3. SELECT @myLeft := lft FROM nested_category
  4.  
  5. WHERE name = '2 WAY RADIOS';
  6.  
  7. UPDATE nested_category SET rgt = rgt + 2 WHERE rgt > @myLeft;
  8. UPDATE nested_category SET lft = lft + 2 WHERE lft > @myLeft;
  9.  
  10. INSERT INTO nested_category(name, lft, rgt) VALUES('FRS', @myLeft + 1, @myLeft + 2);
  11.  
  12. UNLOCK TABLES;

在这个例子中,我们扩大了新产生的父节点(2 WAY RADIOS节点)的右值及其所有它的右边节点的左右值,之后置新增节点于新父节点之下。正如你所看到的,我们新增的节点已经完全融入了嵌套集合中:

  1. SELECT CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
  2. FROM nested_category AS node,
  3. nested_category AS parent
  4. WHERE node.lft BETWEEN parent.lft AND parent.rgt
  5. GROUP BY node.name
  6. ORDER BY node.lft;
  7.  
  8. +-----------------------+
  9. | name |
  10. +-----------------------+
  11. | ELECTRONICS |
  12. | TELEVISIONS |
  13. | TUBE |
  14. | LCD |
  15. | PLASMA |
  16. | GAME CONSOLES |
  17. | PORTABLE ELECTRONICS |
  18. | MP3 PLAYERS |
  19. | FLASH |
  20. | CD PLAYERS |
  21. | 2 WAY RADIOS |
  22. | FRS |
  23. +-----------------------+

删除节点

最后还有个基础任务,删除节点。删除节点的处理过程跟节点在分层数据中所处的位置有关,删除一个叶子节点比删除一个子节点要简单得多,因为删除子节点的时候,我们需要去处理孤立节点。

删除一个叶子节点的过程正好是新增一个叶子节点的逆过程,我们在删除节点的同时该节点右边所有节点的左右值和该父节点的右值都会减去该节点的宽度值:

  1. LOCK TABLE nested_category WRITE;
  2.  
  3. SELECT @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
  4. FROM nested_category
  5. WHERE name = 'GAME CONSOLES';
  6.  
  7. DELETE FROM nested_category WHERE lft BETWEEN @myLeft AND @myRight;
  8.  
  9. UPDATE nested_category SET rgt = rgt - @myWidth WHERE rgt > @myRight;
  10. UPDATE nested_category SET lft = lft - @myWidth WHERE lft > @myRight;
  11.  
  12. UNLOCK TABLES;

我们再一次检验一下节点已经成功删除,而且没有打乱数据的层次:

  1. SELECT CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
  2. FROM nested_category AS node,
  3. nested_category AS parent
  4. WHERE node.lft BETWEEN parent.lft AND parent.rgt
  5. GROUP BY node.name
  6. ORDER BY node.lft;
  7.  
  8. +-----------------------+
  9. | name |
  10. +-----------------------+
  11. | ELECTRONICS |
  12. | TELEVISIONS |
  13. | TUBE |
  14. | LCD |
  15. | PLASMA |
  16. | PORTABLE ELECTRONICS |
  17. | MP3 PLAYERS |
  18. | FLASH |
  19. | CD PLAYERS |
  20. | 2 WAY RADIOS |
  21. | FRS |
  22. +-----------------------+

这个方法可以完美地删除节点及其子节点:

  1. LOCK TABLE nested_category WRITE;
  2.  
  3. SELECT @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
  4. FROM nested_category
  5. WHERE name = 'MP3 PLAYERS';
  6.  
  7. DELETE FROM nested_category WHERE lft BETWEEN @myLeft AND @myRight;
  8.  
  9. UPDATE nested_category SET rgt = rgt - @myWidth WHERE rgt > @myRight;
  10. UPDATE nested_category SET lft = lft - @myWidth WHERE lft > @myRight;
  11.  
  12. UNLOCK TABLES;

再次验证我们已经成功的删除了一棵子树:

  1. SELECT CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
  2. FROM nested_category AS node,
  3. nested_category AS parent
  4. WHERE node.lft BETWEEN parent.lft AND parent.rgt
  5. GROUP BY node.name
  6. ORDER BY node.lft;
  7.  
  8. +-----------------------+
  9. | name |
  10. +-----------------------+
  11. | ELECTRONICS |
  12. | TELEVISIONS |
  13. | TUBE |
  14. | LCD |
  15. | PLASMA |
  16. | PORTABLE ELECTRONICS |
  17. | CD PLAYERS |
  18. | 2 WAY RADIOS |
  19. | FRS |
  20. +-----------------------+

有时,我们只删除该节点,而不删除该节点的子节点。在一些情况下,你希望改变其名字为占位符,直到替代名字的出现,比如你开除了一个主管(需要更换主管)。在另外一些情况下,你希望子节点挂到该删除节点的父节点下:

  1. LOCK TABLE nested_category WRITE;
  2.  
  3. SELECT @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
  4. FROM nested_category
  5. WHERE name = 'PORTABLE ELECTRONICS';
  6.  
  7. DELETE FROM nested_category WHERE lft = @myLeft;
  8.  
  9. UPDATE nested_category SET rgt = rgt - 1, lft = lft - 1 WHERE lft BETWEEN @myLeft AND @myRight;
  10. UPDATE nested_category SET rgt = rgt - 2 WHERE rgt > @myRight;
  11. UPDATE nested_category SET lft = lft - 2 WHERE lft > @myRight;
  12.  
  13. UNLOCK TABLES;

在这个例子中,我们对该节点所有右边节点的左右值都减去了2(因为不考虑其子节点,该节点的宽度为2),对该节点的子节点的左右值都减去了1(弥补由于失去父节点的左值造成的裂缝)。我们再一次确认,那些节点是否都晋升了:

  1. SELECT CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
  2. FROM nested_category AS node,
  3. nested_category AS parent
  4. WHERE node.lft BETWEEN parent.lft AND parent.rgt
  5. GROUP BY node.name
  6. ORDER BY node.lft;
  7.  
  8. +---------------+
  9. | name |
  10. +---------------+
  11. | ELECTRONICS |
  12. | TELEVISIONS |
  13. | TUBE |
  14. | LCD |
  15. | PLASMA |
  16. | CD PLAYERS |
  17. | 2 WAY RADIOS |
  18. | FRS |
  19. +---------------+

有时,当删除节点的时候,把该节点的一个子节点挂载到该节点的父节点下,而其他节点挂到该节点父节点的兄弟节点下,考虑到篇幅这种情况不在这里解说了。

最后的思考

我希望这篇文章对你有所帮助,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 文中下方列出的一些资源链接中能找到一些样例代码,可以去试验一下。

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

http://shepherdwind.com/2010/07/16/hierarchical-data-show-in-html/

mysql 树形数据,层级数据Managing Hierarchical Data in MySQL的更多相关文章

  1. Managing Hierarchical Data in MySQL

    Managing Hierarchical Data in MySQL Introduction Most users at one time or another have dealt with h ...

  2. Managing Hierarchical Data in MySQL(邻接表模型)[转载]

    原文在:http://dev.mysql.com/tech-resources/articles/hierarchical-data.html 来源: http://www.cnblogs.com/p ...

  3. Web中树形数据(层级关系数据)的实现—以行政区树为例

    在Web开发中常常遇到树形数据的操作,如菜单.组织机构.行政区(省.市.县)等具有层级关系的数据. 以下以行政区为例说明树形数据(层级关系数据)的存储以及实现,效果如图所看到的. 1 数据库表结构设计 ...

  4. 【MySQL】MySQL层级数据的递归遍历

    层级的业务数据在系统中很常见,如组织机构.商品品类等. 如果要获取层级数据的全路径,除了缓存起来,就是递归访问的方式了: 将层级数据缓存在redis中,用redis递归获取层级结构.此方法效率高. 在 ...

  5. C# 利用mysql.data 在mysql中创建数据库及数据表

    C# 利用mysql.data 在mysql中创建数据库及数据表 using System; using System.Collections.Generic; using System.Linq; ...

  6. 使用MySQL的SELECT INTO OUTFILE ,Load data file,Mysql 大量数据快速导入导出

    使用MySQL的SELECT INTO OUTFILE .Load data file LOAD DATA INFILE语句从一个文本文件中以很高的速度读入一个表中.当用户一前一后地使用SELECT ...

  7. MySQL Cluster在线添加数据节点

    增加或减少数据节点的数量和 NoOfReplicas(即副本数,通过管理节点的config.ini配置文件来设置)有关,一般来说NoOfReplicas是2,那么增加或减少的数量也应该是成对的,否则要 ...

  8. MySQL 快速导入大量数据 资料收集

    一.LOAD DATA INFILE http://dev.mysql.com/doc/refman/5.5/en/load-data.html 二. 当数据量较大时,如上百万甚至上千万记录时,向My ...

  9. 烂泥:通过binlog恢复mysql备份之前的数据

    本文由秀依林枫提供友情赞助,首发于烂泥行天下. 上一篇文章,我们讲解了如何通过mysql的binlog日志恢复mysql数据库,文章连接为<烂泥:通过binlog恢复mysql数据库>.其 ...

随机推荐

  1. Qt 学习之路 :文本文件读写

    上一章我们介绍了有关二进制文件的读写.二进制文件比较小巧,却不是人可读的格式.而文本文件是一种人可读的文件.为了操作这种文件,我们需要使用QTextStream类.QTextStream和QDataS ...

  2. spring mvc DispatcherServlet详解之四---视图渲染过程

    整个spring mvc的架构如下图所示: 现在来讲解DispatcherServletDispatcherServlet的最后一步:视图渲染.视图渲染的过程是在获取到ModelAndView后的过程 ...

  3. [转] Mac OX上安装MongoDb

    https://scotch.io/tutorials/an-introduction-to-mongodb MongoDB的安装有好多种安装方法,有普通青年的HomeBrew方式,也有文艺青年的源码 ...

  4. 史上最全WebView使用,附送Html5Activity一份

    本文来自:http://www.jianshu.com/users/320f9e8f7fc9/latest_articles感谢您的关注. WebView在现在的项目中使用的频率应该还是非常高的.我个 ...

  5. Eclipse中文语言包下载

    Kepler .Juno . Indigo语言包: http://www.eclipse.org/babel/downloads.php 其他低版本Eclipse语言包下载: http://archi ...

  6. tomcat work 目录

    用tomcat作web服务器的时候,部署的程序在webApps下,这些程序都是编译后的程序(发布到tomcat的项目里含的类,会被编译成.class后才发布过来,源文件没有发布过来,但这里的jsp没有 ...

  7. jquery ui 插件------------------------->sortable

    <!doctype html><html lang="en"><head>  <meta charset="utf-8" ...

  8. java 引用资源-ClassLoader.getResource()方法

    如图,eclipse中我的包结构为:,我在 spt.app.MainFrame 中可以通过一下代码段使用资源: public static Object obj = ImageIconProxy.cl ...

  9. IE6解决固定定位代码

    有些朋友在进行网页布局时,会遇到IE6浏览器不支持固定定位的兼容性问题,本博将详细介绍此问题的解决方法,需要了解的朋友可以参考下. ie6 垂直居中固定定位,代码如下: #center {_posit ...

  10. 对数据预处理的一点理解[ZZ]

    数据预处理没有统一的标准,只能说是根据不同类型的分析数据和业务需求,在对数据特性做了充分的理解之后,再选择相关的数据预处理技术,一般会用到多种预处理技术,而且对每种处理之后的效果做些分析对比,这里面经 ...