发现几种树结构数据库存储方案

最近在开发jSqlBox过程当中,研究树形结构的操做,忽然发现一种新的树结构数据库存储方案,在网上找了一下,没有找到雷同的(也多是花的时间不够),现介绍以下: 目前常见的树形结构数据库存储方案有如下四种,可是都存在必定问题:
1)Adjacency List::记录父节点。优势是简单,缺点是访问子树须要遍历,发出许多条SQL,对数据库压力大。
2)Path Enumerations:用一个字符串记录整个路径。优势是查询方便,缺点是插入新记录时要手工更改此节点如下全部路径,很容易出错。
3)Closure Table:专门一张表维护Path,缺点是占用空间大,操做不直观。
4)Nested Sets:记录左值和右值,缺点是复杂难操做。
以上方法都存在一个共同缺点:操做不直观,不能直接看到树结构,不利于开发和调试。
本文介绍的方法我暂称它为“简单粗暴多列存储法”,它与Path Enumerations有点相似,但区别是用不少的数据库列来存储一个占位符(1或空值),以下图(https://github.com/drinkjava2/Multiple-Columns-Tree/blob/master/treemapping.jpg) 左边的树结构,映射在数据库里的结构见右图表格:
java

各类SQL操做以下:git

1.获取(或删除)指定节点下全部子节点,已知节点的行号为"X",列名"cY":
select *(or delete) from tb where 
  line>=X and line<(select min(line) from tb where line>X and  (cY=1 or c(Y-1)=1 or c(Y-2)=1 ... or c1=1))
例如获取D节点及其全部子节点:
select * from tb where line>=7 and line< (select min(line) from tb where line>7 and (c2=1 or c1=1)) 
删除D节点及其全部子节点:
delete from tb where line>=7 and line< (select min(line) from tb where line>7 and (c2=1 or c1=1)) 

仅获取D节点的次级全部子节点:
select * from tb where line>=7 and c3=1 and line< (select min(line) from tb where line>7 and (c2=1 or c1=1)) 

2.查询指定节点的根节点, 已知节点的行号为"X",列名"cY":
select * from tb where line=(select max(line) from tb where line<=X and c1=1)
例如查I节点的根节点:
select * from tb where line=(select max(line) from tb where line<=12 and c1=1) 

3.查询指定节点的上一级父节点, 已知节点的行号为"X",列名"cY":
select * from tb where line=(select max(line) from tb where line<X and c(Y-1)=1)
例如查L节点的上一级父节点:
select * from tb where line=(select max(line) from tb where line<11 and c3=1) 

3.查询指定节点的全部父节点, 已知节点的行号为"X",列名"cY":
select * from tb where line=(select max(line) from tb where line<X and c(Y-1)=1)
union select * from tb where line=(select max(line) from tb where line<X and c(Y-2)=1)
...
union select * from tb where line=(select max(line) from tb where line<X and c1=1)
例如查I节点的全部父节点:
select * from tb where line=(select max(line) from tb where line<12 and c2=1)
union  select * from tb where line=(select max(line) from tb where line<12 and c1=1) 

4.插入新节点:
视需求而定,例如在J和K之间插入一个新节点T:
update tb set line=line+1 where line>=10;
insert into tb (line,id,c4) values (10,'T',1)
这是与Path Enumerations模式最大的区别,插入很是方便,只须要利用SQL将后面的全部行号加1便可,无须花很大精力维护path字串,  
不容易出错。
另外若是表很是大,为了不update tb set line=line+1 形成全表更新,影响性能,能够考虑增长
一个GroupID字段,同一个根节点下的全部节点共用一个GroupID,全部操做均在groupID组内进行,例如插入新节点改成:
update tb set line=line+1 where groupid=2 and line>=8;
insert into tb (groupid,line,c4) values (2, 8,'T')
由于一个groupid下的操做不会影响到其它groupid,对于复杂的增删改操做甚至能够在内存中完成操做后,一次性删除整个group的内容  
并从新插入一个新group便可。

总结:
以上介绍的这种方法优势有:
1)直观易懂,方便调试,是全部树结构数据库方案中惟一所见即所得,可以直接看到树的形状的方案,空值的采用使得树形结构一目了然。
2)SQL查询、删除、插入很是方便,没有用到Like语法。
3)只须要一张表
4)兼容全部数据库
5)占位符即为实际要显示的内容应出现的地方,方便编写Grid之类的表格显示控件 github

缺点有 1)不是无限深度树,数据库最大容许列数有限制,一般最多为1000,这致使了树的深度不能超过1000,并且考虑到列数过多对性能也有影响, 使用时建议定一个比较小的深度限制例如100。
2)SQL语句比较长,不少时候会出现c9=1 or c8=1 or c7=1 ... or c1=1这种n阶乘式的查询条件
3)树的节点总体移动操做比较麻烦,须要将整个子树平移或上下称动,当节点需要常常移动时,不建议采用这种方案。对于一些只增减,不常移动节点的应用如论坛贴子和评论倒比较合适。
4)列很是多时,空间占用有点大。 算法

如下为追加内容,是在前述基础上,一种更简单的无限深度树方案

突 然发现上面的方法仍是太笨了,若是不用多列而是只用一个列来存储深度等级,则能够不受数据库列数限制,从而进化为无限深度树,虽然再也不具备所见即所得的效 果,可是在性能和简单性上要远远超过上述“简单粗暴多列存储法”,暂时给它取名"朱氏深度树V2.0法"(备注:若是已有人发明了这个方法,删掉前两个字 就行了),方法以下: 以下图 (https://github.com/drinkjava2/Multiple-Columns-Tree/blob/master/treemappingv2.png) 左边的树结构,映射在数据库里的结构见右图表格,注意每一个表格的最后一行必须有一个END标记,level设为0: sql

1.获取指定节点下全部子节点,已知节点的行号为X,level为Y, groupID为Z
select * from tb2 where groupID=Z and 
  line>=X and line<(select min(line) from tb where line>X and level<=Y and groupID=Z)
例如获取D节点及其全部子节点:
select * from tb2 where groupID=1 and 
  line>=7 and line< (select min(line) from tb2 where groupid=1 and line>7 and level<=2)
删除和获取类似,只要将sql中select * 换成delete便可。

仅获取D节点的次级全部子节点:(查询条件加一个level=Y+1便可):
select * from tb2 where groupID=1 and 
  line>=7 and level=3 and line< (select min(line) from tb2 where groupid=1 and line>7 and level<=2) 

2.查询任意节点的根节点, 已知节点的groupid为Z
select * from tb2 where groupID=Z and line=1 (或level=1) 

3.查询指定节点的上一级父节点, 已知节点的行号为X,level为Y, groupID为Z
select * from tb2 where groupID=Z and 
  line=(select max(line) from tb2 where groupID=Z and line<X and level=(Y-1))
例如查L节点的上一级父节点:
select * from tb2 where groupID=1 
  and line=(select max(line) from tb2 where groupID=1 and line<11 and level=3) 

4.查询指定节点的全部父节点, 已知节点的行号为X,深度为Y:
select * from tb2 where groupID=Z and 
  line=(select max(line) from tb2 where groupID=Z and line<X and level=(Y-1))
union select * from tb2 where groupID=Z and 
  line=(select max(line) from tb2 where groupID=Z and line<X and level=(Y-2))
...
union select * from tb2 where groupID=Z and 
  line=(select max(line) from tb2 where groupID=Z and line<X and level=1)
例如查I节点的全部父节点:
select * from tb2 where groupID=1 and 
  line=(select max(line) from tb2 where groupID=1 and line<12 and level=2)
union  select * from tb2 where groupID=1 and 
  line=(select max(line) from tb2 where groupID=1 and line<12 and level=1)

5.插入新节点:例如在J和K之间插入一个新节点T:
update tb2 set line=line+1 where  groupID=1 and line>=10;
insert into tb (groupid,line,id,level) values (1,10,'T',4);

总结: 此方法优势有:
1) 是无限深度树
2) 虽然不象第一种方案那样具备所见即所得的效果,可是依然具备直观易懂,方便调试的特色。
3) 能充分利用SQL,查询、删除、插入很是方便,SQL比第一种方案简单多了,也没有用到like模糊查询语法。
4) 只须要一张表。
5) 兼容全部数据库。
6) 占用空间小 数据库

缺点有:
1)树的节点总体移动操做有点麻烦, 适用于一些只增减,不常移动节点的场合如论坛贴子和评论等。当确实须要进行复杂的移动节点操做时,一种方案是在内存中进行整个树的操做并完成排序,操做完成后删除整个旧group再总体将新group一次性批量插入数据库。app

 

1月22日补充:
节点的移动操做有点麻烦,只是相对于查询/删除/插入来讲,并非说难上天了。例如在MySQL下移动整个B节点树到H节点下,并位于J和K之间的操做以下:
update tb2 set tempno=line*1000000 where groupid=1;  
set @nextNodeLine=(select min(line) from tb2 where groupid=1 and line>2 and level<=2);  
update tb2 set tempno=9*1000000+line, level=level+2 where groupID=1 and line>=2 and line< @nextNodeLine;  
set @mycnt=0;  
update tb2 set line=(@mycnt := @mycnt + 1) where groupid=1 order by tempno;  
上例须要在表中新增一个名为tempno的整数类型列, 这是个懒人算法,虽然简单明了,可是对整棵树进行了从新排序,因此效率并不高。 在须要频繁移动节点的场合下,用Adjacency List方案可能更合适一些。性能

如 果须要频繁移动节点的场合,又想保留方案2高效查询的优势,还有一种方案就是再添加一个父节点pid字段和两个辅助字段tempno和 temporder用于排序,(暂时称其为“深度树V3.0法"), 这样至关于V2.0法和Adjacency List模式的合并了,优势是每次移动节点,只须要更改PID便可,不须要复杂的算法,一次能够任意移动、增长、删除多个节点,最后统一调用如下算法简单 地进行一下重排序便可,下面这个示例完整演示了一个Adjacency List模式到V2.0模式的转换,这至关于一个从新给树建查询索引的过程: 设计

create table tb3 (
id varchar(10),
comments varchar(55),
pid varchar(10),
line integer,
level integer,
tempno bigint,
temporder integer
)

insert into tb3 (id,comments,Pid) values('A','found a bug',null);
insert into tb3 (id,comments,Pid) values('B','is a worm','A');
insert into tb3 (id,comments,Pid) values('C','no','A');
insert into tb3 (id,comments,Pid) values('D','is a bug','A');
insert into tb3 (id,comments,Pid) values('E','oh, a bug','B');
insert into tb3 (id,comments,Pid) values('F','solve it','B');
insert into tb3 (id,comments,Pid) values('G','careful it bites','C');
insert into tb3 (id,comments,Pid) values('H','it does not bit','D');
insert into tb3 (id,comments,Pid) values('I','found the reason','D');
insert into tb3 (id,comments,Pid) values('J','solved','H');
insert into tb3 (id,comments,Pid) values('K','uploaded','H');
insert into tb3 (id,comments,Pid) values('L','well done!','H');

set @mycnt=0;
update tb3 set  line=0,level=0, tempno=0, temporder=(@mycnt := @mycnt + 1) order by id;
update tb3 set level=1, line=1 where pid is null;

update tb3 set tempno=line*10000000 where line>0; 
update tb3 a, tb3 b set a.level=2, a.tempno=b.tempno+a.temporder where a.level=0 and 
a.pid=b.id and b.level=1;
set @mycnt=0;
update tb3 set line=(@mycnt := @mycnt + 1) where level>0 order by tempno;

update tb3 set tempno=line*10000000 where line>0; 
update tb3 a, tb3 b set a.level=3, a.tempno=b.tempno+a.temporder where a.level=0 and 
a.pid=b.id and b.level=2;
set @mycnt=0;
update tb3 set line=(@mycnt := @mycnt + 1) where level>0 order by tempno;

update tb3 set tempno=line*10000000 where line>0; 
update tb3 a, tb3 b set a.level=4, a.tempno=b.tempno+a.temporder where a.level=0 and 
a.pid=b.id and b.level=3;
set @mycnt=0;
update tb3 set line=(@mycnt := @mycnt + 1) where level>0 order by tempno;

 
以上算法利用了SQL的功能,将原来可能须要很是多SQL递归查询的过程转变成了有限次数(=树最大深度)的SQL操做,为了突出算法,以上示例 假设只有一个根节点,删除了groupid和endtag,实际使用中要完善一下这个细节, order by id也可改为以其它字段排序。因时间关系我就不给出V2.0模式到Adjacency List模式逆推的算法了(也即pid为空,根据V2.0表格倒过来给pid赋值的过程),不过这个算法倒不重要,由于一般v3.0表中每一行会一直保存 着一个pid)。
总结一下:
Adjacency List模式:移/增/删节点方便,查询不方便
深度树V2.0模式:查询方便,增/删节点方便,但存在效率问题,移动节点不方便
深度树V3.0模式:移/增/删节点方便,查询方便,缺点是每次移/增/删节点后要重建line和level值以供查询用。它是结合了上两种模式 的合并体,并能够根据侧重,随时在这两种模式(修改模式和查询模式)间切换。v3.0法至关于给Adjacency List模式设计了一个查询索引。调试

相关文章
相关标签/搜索