`
drinkjava2
  • 浏览: 41804 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

发明了一种新的树结构数据库存储方案

阅读更多
最近在开发jSqlBox过程中,想研究一下树形结构和VO对象树的转换,突然发现一种新的树结构数据库存储方案,在网上搜索了一下,没有找到雷同的(也可能是我花的时间不够)方案,现介绍如下:
目前常见的树形结构数据库存储方案有以下四种,但是都存在一定问题:
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) 左边的树结构,映射在数据库里的结构见右图表格:



各种SQL操作如下:

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)

4.查询指定节点的所有父节点, 已知节点的行号为"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)

5.插入新节点:
视需求而定,例如在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之类的表格显示控件。

缺点有:
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)左边的树结构,映射在数据库里的结构见右图表格,注意这种方法是按groupid来分组,每个组的最后一行必须有一个END标记,level设为0,见图:



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,level为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))
...(行数=Y-1)
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一次性批量插入数据库。

2017年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方案可能更合适一些。

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



drop table if exists tb3;

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('E','no','B');
insert into tb3 (id,comments,Pid) values('F','is a bug','B');
insert into tb3 (id,comments,Pid) values('C','oh, a bug','A');
insert into tb3 (id,comments,Pid) values('G','need solve it','C');
insert into tb3 (id,comments,Pid) values('D','careful it bites','A');
insert into tb3 (id,comments,Pid) values('H','it does not bite','D');
insert into tb3 (id,comments,Pid) values('J','found the reason','H');
insert into tb3 (id,comments,Pid) values('K','solved','H');
insert into tb3 (id,comments,Pid) values('L','uploaded','H');
insert into tb3 (id,comments,Pid) values('I','well done!','D');

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模式设计了一个查询索引。
  • 大小: 12.7 KB
  • 大小: 24.5 KB
  • 大小: 39.9 KB
分享到:
评论
5 楼 drinkjava2 2018-05-30  
再补充:为了避免插入新节点时对后续节点进行加1操作影响性能,可以将行号跳号设计,例如按100000跳号,新节点插入在两个节点的空号区中段,这样可以在很大概率上消除加1操作。另外一个表只需一个End标记,可以设为一个很大的常量值(要大于所有行号)即可。

另外,这篇文修修补补,太啰嗦了,不太好看,下面这个链接是精简版:
https://my.oschina.net/drinkjava2/blog/1818631
4 楼 drinkjava2 2017-08-15  
anxin1225 写道
   大大,你果然骨骼精异。我简单考虑了一下,发现这种实现思路,确实能够解决问题,不过还是像是你说的,在整棵树很大的情况下,在修改前面的内容的时候,后边受影响行数很多。

这个没办法,其它三种方法Path Enumerations、Closure Table、Nested Sets也都存在这个问题。想要马儿跑,又要马儿不吃草是不可能的,这是以插入时以有序方式插入(或大的改动后全树或子树的重排序)为代价换取查询性能的提高。
本文算法实际上可以抽象成是一种给多叉树建查询索引的算法,即使在没有数据库存在的情况下,也是有可能在程序中应用到这种算法的,只要将SQL中的查询功能改成手工进行数组遍历即可。
3 楼 anxin1225 2017-07-12  
   大大,你果然骨骼精异。我简单考虑了一下,发现这种实现思路,确实能够解决问题,不过还是像是你说的,在整棵树很大的情况下,在修改前面的内容的时候,后边受影响行数很多。
2 楼 drinkjava2 2017-03-18  
怎么都反映看不懂? 算你狠,拿把尺来: 按"简单粗暴多列存储法"存储的树结构,判断它的所有子结点,只要拿把尺从这个节点开始往下划竖线,竖线右边的全是它的子节点,直到碰到竖线上或竖线左边出现非空值则终止。V2.0和V3.0都是建立在同一个道理上。
你要是做Java的,可以看一下jSqlBox项目(baidu之)中test/examples/orm/TreeORMTest.java这个示例,有很详细的子树查找、子树移动、重排序的演示。
1 楼 gaoyan2011 2017-02-06  
表示看不太懂

相关推荐

    嵌入式数据库中数据恢复的方法和装置

    开了一种嵌入式数据库中数据...本发明提供的一种嵌入式数据库中数据恢复的方法和装置,对数据文件和/或数据文件结构错误的嵌入式数据库中的数据进行恢复,节约了嵌入式系统的存储资源,提高了嵌入式系统中数据库的容错性。

    史上最全数据库笔记(上).docx

    非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合,可以是文档或者键值对等。 优点:格式灵活、速度快、高扩展性、成本低。 缺点:不提供 SQL 支持,学习和使用成本较高,无事务处理,数据...

    行业分类-设备装置-一种将XML文件的内容导入数据库的方法和装置.zip

    标题中的“行业分类-设备装置-一种将XML文件的内容导入数据库的方法和装置”表明这是一个关于IT行业中设备装置领域的技术,具体涉及XML数据处理和数据库管理。这个技术可能是一种自动化工具或者系统,旨在高效地将...

    数据库课件

    接着,关系数据库被描述为数据库系统中最为核心的一种类型,它采用单一的数据结构——关系——来表示实体和它们之间的联系。关系模型由关系数据库、关系操作集合和关系完整性约束组成。关系操作的特点是一次处理一个...

    五笔编码Access数据库

    Access是Microsoft Office套件中的一个关系型数据库管理系统,它提供了强大的数据存储和查询功能,而五笔编码则是中文输入法中的一种,通过拆分汉字为基本的字根来快速输入汉字。 在描述中提到,这个数据库包含了自...

    TokuDB 高科扩展性 MySQL 和 MariaDB 数据库

    TokuDB不仅仅是一个存储引擎,它是一种革命性的数据库技术,其Fractal Tree索引算法提供了一种全新的数据处理方法,这种创新方法不仅提高了数据库在读写操作上的效率,还为处理大数据集提供了一个更加高效和可扩展的...

    一种知识图谱自动构建方法及系统[发明专利].pdf

    知识图谱是一种结构化的知识表示形式,它将实体(如人、地点、事件等)及其之间的关系以图形的形式展示,便于计算机理解和处理。在“一种知识图谱自动构建方法及系统”的发明专利中,主要探讨了如何高效自动化地构建...

    一种基于MYSQL数据库的SQL信息采集审计系统.docx

    SQL语句语法解析模块则对捕获到的SQL语句进行结构化处理,将其拆解成易于理解的形式,存储在SQL操作日志库中。这一过程有助于理解SQL语句的实际意图,便于进一步分析可能存在的安全风险。 MYSQL通讯错误代码模块...

    Python访问Mysql数据库

    Python是一种高级编程语言,由Guido van Rossum于1989年发明,首次发布于1991年。它以其简单易学、功能强大著称。Python的设计哲学强调代码的可读性和简洁的语法,这让程序员能够更专注于解决实际问题而非语言细节。...

    数据库原理课件第章数据库基本理论.pptx

    数据库系统解决了文件系统的问题,提供了一种有组织、可共享、高效率的数据管理方式。 二、数据库的概念 数据库是一个长期存储在计算机上的、有组织的、可共享的数据集合。数据按照特定的数据模型组织,具有较低的...

    2022年度数据库最常用的语言SQL面试题汇总和答案.docx

    SQL 最初发明于 1970 年,是一种用于数据库创建、删除、获取和修改行等的数据库语言。 SQL 的用途是什么?SQL 负责维护数据库中存在的关系数据和数据结构。常见的用法包括对数据库执行查询、从数据库中检索数据、在...

    一种基于精准AI技术的健康数据分析方法和分析系统发明专利.docx

    一种基于精准AI技术的健康数据分析方法和分析系统是健康管理领域的一项创新发明,旨在解决现有健康管理服务的局限性,如单一的健康问题处理、缺乏系统性的建议、无法跟踪用户执行情况和远程服务不足等问题。该发明...

    本发明涉及一种自动控制系统监控平台-包括数据采集系统、.docx

    该发明提出了一种自动控制系统监控平台,其核心是构建了一个高效、稳定且具有广泛适用性的监控解决方案,尤其针对工业自动化领域。平台主要包括三个关键组成部分:数据采集系统、数据处理及过程控制系统、数据管理...

    MPP环境下数据库查询方法、装置、服务器及存储介质.pdf

    本发明涉及的数据库查询方法是针对这种环境设计的,旨在优化哈希半连接操作,这是一种在关系数据库中常用于联接两个表的运算。 哈希半连接操作通常涉及到两个数据集,一个作为“左表”,另一个作为“右表”。在传统...

    一种基于精准AI技术的健康数据分析方法和分析系统发明专利.pdf

    本文介绍的是一种基于精准AI技术的健康数据分析方法和分析系统,该系统旨在改进健康管理服务,提供全面的健康评估和个性化的治疗方案。该发明主要解决了现有健康管理服务中缺乏系统性、个性化以及远程服务的问题。 ...

    一种基于Hadoop的海量流数据存储和查询方法及系统的制作方法.pdf

    一种基于 Hadoop 的海量流数据存储和查询方法及系统的制作方法 本资源摘要信息介绍了一种基于 Hadoop 的海量流数据存储和查询方法及系统的制作方法,旨在解决海量流数据管理面临的挑战,如数据加载性能、存储容量、...

    一种公共数据中心平台.pdf

    本发明提出的一种公共数据中心平台,通过整合业务资源数据库、元数据库和社会资源数据库,实现了数据的集中管理、整理和交换,以提高数据的利用效率和服务质量。 【技术方案】: 1. **数据资源库**:平台包括三个...

    数据库系统工程师分类模拟19有答案.pdf

    关系数据库是一种使用关系模型来组织数据的数据库系统。关系模型使用表格结构来表示实体集和实体集之间的联系。因此,关系数据库是表的集合,其结构是由关系模式定义的。 高速缓冲存储器 题3考查了高速缓冲存储器...

Global site tag (gtag.js) - Google Analytics