树形结构的数据在项目开发中比较常见,好比比较典型的是论坛主题留言。node
每个主题(节点)能够有n个留言(子节点)。这些留言又能够有本身的留言。所以这种结构就是一颗树。本文讨论的是数据库中如何存储这种树形结构。sql
假设有以下一棵树:数据库
方法一数据结构
注意:本例中的数据库是SQLite,所以SQL语句只对SQLite有效,其余数据库能够参考该写法。闭包
要存储于数据库中,最简单直接的方法,就是存储每一个元素的父节点ID。spa
暂且把这种方法命名依赖父节点法,所以表结构设计以下:设计
存储的数据以下格式:3d
这种结构下,若是查询某一个节点的直接子节点,十分容易,好比要查询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
b
WHERE
a.parentid = b. nodeid
)
SELECT
*
FROM
tmp
|
可是对于那些不支持递归查询的数据库来讲,实现起来就比较复杂了。
方法二
还有一种比较土的方法,就是存储路径。暂且命名为路径枚举法。
这种方法,将存储根结点到每一个节点的路径。
这种数据结构,能够一眼就看出子节点的深度。
若是要查询某个节点下的子节点,只须要根据path的路径去匹配,好比要查询D节点下的全部子节点。
1
|
select
*
from
tree2
where
path
like
'%/4/%'
|
或者出于效率考虑,直接写成
1
|
select
*
from
tree2
where
path
like
'1/4/%'
|
若是要作聚合操做,也很容易,好比查询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表:
NodeRelation表:
如例子中的树,插入的数据以下:
Tree3表的数据
NodeRelation表的数据
能够看到,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节点自身子节点的关系,如上面的例子,实际上删除的节点关系为:
若是要删除节点H,则为
总结:
上面主要讲了3种方式,各有优势缺点。能够根据实际须要,选择合适的数据模型。
本文出自 “一只博客” 博客,请务必保留此出处http://cnn237111.blog.51cto.com/2359144/1226911