如何在数据库中存储一棵树

树形结构的数据在项目开发中比较常见,好比比较典型的是论坛主题留言。node

每个主题(节点)能够有n个留言(子节点)。这些留言又能够有本身的留言。所以这种结构就是一颗树。本文讨论的是数据库中如何存储这种树形结构。sql

假设有以下一棵树:数据库

无标题

方法一数据结构

注意:本例中的数据库是SQLite,所以SQL语句只对SQLite有效,其余数据库能够参考该写法。闭包

要存储于数据库中,最简单直接的方法,就是存储每一个元素的父节点ID。spa

暂且把这种方法命名依赖父节点法,所以表结构设计以下:设计

598F8C3BAEC249C7B7C21FCAE42C097F

存储的数据以下格式:3d

D91E5117473F4F75B42E8542953BE78C

这种结构下,若是查询某一个节点的直接子节点,十分容易,好比要查询D节点的子节点。code

1
select  from  tree1  where  parentid=4

若是要插入某个节点,好比在D节点下,再次插入一个M节点。blog

只须要以下SQL:

1
INSERT  INTO  tree1 (value,parentid)  VALUES ( 'M' ,4);

这种结构在查找某个节点的全部子节点,就稍显复杂,不管是SELECT仍是DELETE均可能涉及到获取全部子节点的问题。好比要删除一个节点而且该节点的子节点也要所有删除,那么首先要得到全部子节点的ID,由于子节点并不仅是直接子节点,还可能包含子节点的子节点。好比删除D节点及其子节点,必须先查出D节点下的全部子节点,而后再作删除,SQL以下:

1
2
3
4
select  nodeid  from  tree1  where  parentid=4  --返回8,9
select  nodeid  from  tree1  where  parentid  in  (8,9)  --返回10,11,12
select  nodeid  from  tree1  where  parentid  in  (10,11,12)  --返回空
delete  from  tree1  where  nodeid  in  (4,8,9,10,11,12)

若是是只删除D节点,对于其它节点不作删除而是作提高,那么必须先修改子节点的parentid,而后才能删除D节点。

正如上面演示的,对于这种依赖父节点法,最大的缺点就是没法直接得到某个节点的全部子节点。所以若是要select全部的子节点,须要繁琐的步骤,这不利于作聚合操做。

对于某些数据库产品,支持递归查询语句的,好比微软的SQL Server,可使用CTE技术实现递归查询。好比,要查询D节点的全部子节点。只须要以下语句:

1
2
3
4
5
6
WITH  tmp  AS (
SELECT  FROM  Tree1  WHERE  nodeid = 4
UNION  ALL
SELECT  a.*  FROM  Tree1  AS  a,tmp  AS  WHERE  a.parentid = b. nodeid
)
SELECT  FROM  tmp

可是对于那些不支持递归查询的数据库来讲,实现起来就比较复杂了。

 

方法二

还有一种比较土的方法,就是存储路径。暂且命名为路径枚举法。

这种方法,将存储根结点到每一个节点的路径。

55778B9842DC47279FFCFF48B54ABDA1

这种数据结构,能够一眼就看出子节点的深度。

若是要查询某个节点下的子节点,只须要根据path的路径去匹配,好比要查询D节点下的全部子节点。

1
select  from  tree2  where  path  like  '%/4/%'

或者出于效率考虑,直接写成

1
select  from  tree2  where  path  like  '1/4/%'

214EF7DB11684064ABB9C4FCBDDD5CD4

若是要作聚合操做,也很容易,好比查询D节点下一共有多少个节点。

select count(*) from tree2 where path like '1/4/%';

要插入一个节点,则稍微麻烦点。要插入本身,而后查出父节点的Path,而且把本身生成的ID更新到path中去。好比,要在L节点后面插入M节点。

首先插入本身M,而后获得一个nodeid好比nodeid=13,而后M要插入到L后面,所以,查出L的path为1/4/8/12/,所以update M的path为1/4/8/12/13

1
2
3
4
5
update  tree2  set
path=( select  path  from  tree2  where  nodeid=12)  --此处开始拼接
||last_insert_rowid()|| '/'
where
nodeid= last_insert_rowid();

这种方法有一个明显的缺点就是path字段的长度是有限的,这意味着,不能无限制的增长节点深度。所以这种方法适用于存储小型的树结构。

方法三

下面介绍一种方法,称之为闭包表。

该方法记录了树中全部节点的关系,不只仅只是直接父子关系,它须要使用2张表,除了节点表自己以外,还须要使用1张表来存储节祖先点和后代节点之间的关系(同时增长一行节点指向自身),而且根据须要,能够增长一个字段,表示深度。所以这种方法数据量不少。设计的表结构以下:

Tree3表:

E1D5EEEE05EF4188ADE17192C9B95ECC

NodeRelation表:

C3E90EA4EEBE490D87035F98DFC39EA2

如例子中的树,插入的数据以下:

Tree3表的数据

20ADFF42DB6E45CC9CA0C287DA49C5B5

NodeRelation表的数据

9F3B8EC76E0B4D67830FF29B6F6EEC4E

能够看到,NodeRelation表的数据量不少。可是查询很是方便。好比,要查询D节点的子元素

只须要

1
select  from  NodeRelation  where  ancestor=4;

要查询节点D的直接子节点,则加上depth=1

1
select  from  NodeRelation  where  ancestor=4  and  depth=1;

要查询节点J的全部父节点,SQL:

1
select  from  NodeRelation  where  descendant=10;

若是是插入一个新的节点,好比在L节点后添加子节点M,则插入的节点除了M自身外,还有对应的节点关系。即还有哪些节点和新插入的M节点有后代关系。这个其实很简单,只要和L节点有后代关系的,和M节点一定会有后代关系,而且和L节点深度为X的和M节点的深度一定为X+1。所以,在插入M节点后,找出L节点为后代的那些节点做为和M节点之间有后代关系,插入到数据表。

1
2
3
4
5
6
7
INSERT  INTO  tree3 (value)  VALUES ( 'M' ); --插入节点
INSERT  INTO   NodeRelation(ancestor,descendant,depth)
select  n.ancestor,last_insert_rowid(),n.depth+1 --此处深度+1做为和M节点的深度
from  NodeRelation n
where  n.descendant=12
Union  ALL
select   last_insert_rowid() ,last_insert_rowid(),0  --加上自身

在某些并不须要使用深度的状况下,甚至能够不须要depth字段。

若是要删除某个节点也很容易,好比,要删除节点D,这种状况下,除了删除tree3表中的D节点外,还须要删除NodeRelation表中的关系。

首先以D节点为后代的关系要删除,同时以D节点的后代为后代的这些关系也要删除:

1
2
delete  from  NodeRelation  where  descendant  in
( select  descendant  from  NodeRelation  where  ancestor=4 ); --查询以D节点为祖先的那些节点,即D节点的后代。

这种删除方法,虽然完全,可是它也删除了D节点和它本来的子节点的关系。

若是只是想割裂D节点和A节点的关系,而对于它原有的子节点的关系予以保留,则须要加入限定条件。

限制要删除的关系的祖先不以D为祖先,即若是这个关系以D为祖先的,则不用删除。所以把上面的SQL加上条件。

1
2
3
delete  from  NodeRelation  where  descendant  in
( select  descendant  from  NodeRelation  where  ancestor=4 ); --查询以D节点为祖先的那些节点,即D节点的后代。
and  ancestor  not  in  ( select  descendant  from  NodeRelation   where  ancestor =4 )

上面的SQL用文字描述就是,查询出D节点的后代,若是一个关系的祖先不属于D节点的后代,而且这个关系的后代属于D节点的后代,就删除它。

这样的删除,保留了D节点自身子节点的关系,如上面的例子,实际上删除的节点关系为:

569AD87B6E7B4F428D3521B550F9D0FF

若是要删除节点H,则为

8579EB3DB87C4175B5DAAEAA9E182395

总结:

上面主要讲了3种方式,各有优势缺点。能够根据实际须要,选择合适的数据模型。

 

本文出自 “一只博客” 博客,请务必保留此出处http://cnn237111.blog.51cto.com/2359144/1226911

相关文章
相关标签/搜索