by Vadim Tropashko
翻译:
Janwer Zhang
关系数据库通常被认为是在其先辈网络和分层模型上的进步发展。在每个层级查询方面,当模型转换成依赖关系时,他们结果是惊人地不完整。几乎每两三个月总有关于如何在数据库中建立树模型的问题弹出在comp.database.theory新闻组。在本文中我将探讨两者用四个众所周知的方法的实现,并展示它们之间的关联。我们将找到一个可以被看作是具体路径(materialized path)和嵌套集合(nested sets)“混合式”的新方法。
链接表(Adjacency List)
树结构是有向无环图(Directed Acyclic Graph, 简称DAG)的一个特殊案例。描绘DAG结构的方式之一:
create table emp (
ename varchar2(100),
mgrname varchar2(100
);
emp 表的每条记录通过指向上级mgrname的ename来标识。例如,假如JONES向KING报告,于是emp表中含有<ename='JONES', mgrname='KING'>的记录。假设,emp表也包含<ename='SCOTT', mgrname='JONES'>。此外,假如emp表不含有<ename='SCOTT',mgrname='KING'>记录,对于其它每对毗连的记录也是如此,那么它就是所谓的邻接表(Adjacency List)。如果正好相反,那emp表是可传递的闭包关系。
一个典型的层次查询可能会询问SCOTT是否间接向KING报告。由于我们不知道两者间的层级数字,因此我们不能告诉emp表要进行多少次自连接,以至于这个任务不能用传统的SQL解决。假如知道emp表是传递闭包tcemp,那么这个查询是小事一桩:
此外,SQL扩展:SQL3/DB2递归查询,能执行层次查询:
with tcemp as (select ename,mgrname from tcemp
union select tcemp.ename,emp.mgrname from tcemp, emp where tcemp.mgrname = emp.ename)
select 'TRUE' from tcempwhere ename = 'SCOTT' and mgrname = 'KING';
这个tcemp计算作为中间关联,或采用Oracle专有连接的语法:
select 'TRUE' from ( select ename from emp connect by prior mgrname = ename
start with ename = 'SCOTT') where ename = 'KING';
其中内查询"chases the pointers"从SCOTT节点到树的根节点,而外查询检查KING节点是否在路径上。
链接表可以说是最直观的树模型。这是我们的主要焦点,不过,接下来还有两种方法。
具体化路径(Materialized Path)
在这种做法中每条记录存储到根部为止的整个路径。在我们前面的例子中,让我们假定KING为根节点,然后,记录ename="SCOTT"
通过路径 SCOTT->JONES->KING
连接到根部。现代数据库允许描绘一个节点清单作为一个单一的值,但由于具体路径在被发明之前的长时间里,约定停留在经由一些分隔符连接的普通字符串节点,最常见的'.'或'/。在后一种情况下,尤其明显一个类似UNIX文件系统的路径名。
应用更紧凑的变量方法,是在字符路径里我们使用兄弟分子代替节点的主键。扩展我们的例子:
ENAME
|
PATH
|
KING
|
1
|
JONES
|
1.1
|
SCOTT
|
1.1.1
|
ADAMS
|
1.1.1.1
|
FORD
|
1.1.2
|
SMITH
|
1.1.2.1
|
BLAKE
|
1.2
|
ALLEN
|
1.2.1
|
WARD
|
1.2.2
|
CLARK
|
1.3
|
MILLER
|
1.3.1
|
Path 1.1.2 指示FORD是父节点JONES的第二个孩子节点
让我们写一些查询。
1. 雇员FORD和它的一系列上级节点:
select e1.ename from emp e1, emp e2 where e2.path like e1.path || '%' and e2.ename='FORD'
2. 雇员JONES及它的所有间接子节点:
select e1.ename from emp e1, emp e2 where e1.path like e2.path || '%' and e2.ename='JONES'
尽管两个查询看起来是对称的,但在它们的各自的执行中有根本性的差别。如果一颗子树的下级节点相比整体层次大小而言是较小的,那么在数据库中通过执行主键抓取e2记录,然后执行e1.path范围的扫描,这是快速的保证。
在另一方面,上级节点的查询大体上是相同的
select e1.ename from emp e1, emp e2 where e2.path > e1.path and e2.path < e1.path || 'Z'
and e2.ename='FORD'
或者,本来知道e2.path,请注意,它可以进一小减少到:
select e1.ename from emp e1 where e2.path>e1.path and e2.path<e1.path || 'Z'
这里,很显然path上的索引不会起作用(除了e2.path恰好是靠近域边界的意外情况下,以便有选择性的判定e2.path>e1.path)。明显的解决方法是,我们并没有利用数据库去计算出所有的上级路径!例如,1.1.2的上级是1.1和1。一个简单的递归字符串解析函数可以提取这些路径,那么回答上层的名称通过:
select e1.ename from emp where e1.path in ('1.1','1')
这应该是个快速级联的执行方案。
嵌套集合(Nested Sets)
具体路径和
Joe Celko的嵌套集合
均具有标准SQL语法层次查询的回应能力。在两种模型中,节点的全局位置在层次中是“编码”的,相反链接表的每个连接仅是一个近邻间的局部连接。类似于具体路径,嵌套集合模型也遭遇上层节点查询的性能问题。
此处,这问题变得比具体路径情况下更明确:我们需要找出特定点的所有间隔。这个问题很难解决。尽管有像R-Tree的专门索引表,但它们中没有一个能像B- Tree一样被普遍接受。例如,如果顶层路径仅包含10个节点,而整棵树的大小为1000000,那么没有一种索引技术能够提供 1000000/10=100000倍的性能提升。(这样的性能改进因素通常与索引扫描范围类似,非常有选择性,以数据卷为条件的典型关联。)
不像一个具体路径,这边的技巧是我们
计算所有节点而不须为查询数据库做不必要的工作。
另一个--较根本性的--嵌套集合的缺点是嵌套集编码是暂时性的。如果我们在分层结构中间插入一个
节点,插入点边界以上的所有间隔必须重新计算。换句话说,当我们插入一条记录到数据库中,大概有一半左右的其它记录需要被更新。这也是为什么嵌套集合模型仅能接收有限的静态层次。
嵌套集合间的区间为整数。为尝试使嵌套集合模型对插入更有耐性。Celko建议我们放弃每个节点总是有(rgt-lft+1)/2个孩子的特性。依我之见,这是一个朝前了半步的解决方案:在一个
带有大
区间
和扩展编号的嵌套集合模型
中的任何
区间
仍然可以覆盖为增加更多的孩子而没空间留下的
区间
。假如这些
区间
都允许仅在分散点有边界(e.g.整数)。那么其中需要使用一个密集的域来代替,像有理数或实数。
嵌套区间(Nested Intervals)
嵌套区间归纳为嵌套集合。一个节点[clft,crgt]是一个[plft,prgt]的(间接)后代,假如:
plft <= clft and crgt >= prgt
该域名的区间范围不再仅限于整数:如果需要,我们准许有理数甚至实数。现在,一个合理的规则是,增加一个孩子节点不再是问题。一个类似规则的例子在父区间 [plft,prgt]里将找到一个空段[lft1,rgt1]并插入一个孩子节点[(2*lft1+rgt1)/3, (rgt1+2*lft)/3]:
插入之后,我们仍旧有两有空段[lft1,(2*lft1+rgt1)/3]和[(rgt1+2*lft)/3,rgt1]来增加更多的孩子到父节点。
在接下来的章节我们将改进这一固有规则。
偏序(Partial Order)
让我们看一下嵌套集合的二维图。我们假定rgt为水平x轴,lft为垂直y轴。那么嵌套
区间
树看起来像这样:
每个节点[lft,rgt]在二维圆锥形里有它的子节点边界y>=lft&x<=rgt。且右区间边界总是小于左区间,所有节点均不允许超过对角线y=x。
另一种方式看这幅图片应注意父节点的子类中的一个孩子节点,无论何时一系列定义在圆锥形孩子y>=clft&x<=crgt的所有点是父节点y>=plft&x<=prgt的一个子集。这个子集与平面上的圆锥形的关系是一个偏序。
现在我们知道遵照树节点的两个制约因素,我将确切地描述如何在xy平面上放置他们。
映射(The Mapping)
树根的选择完全是随意的:我们假定根节点为区间[0,1]。在我们几何图案的解释中,所有树节点属于xy平面上的正方形单元下部的三角形。
我们会通过归纳来进一步详细的描述映射 。对于每个树节点,让我们首先在xy平面定义两个重要的点。深度优先会聚点是对角线与通过节点的垂直线之间的一个交叉点。例如,节点<x=1,y=1/2>的深度优先会聚点为<x=1,y=1>。广度优先会聚点是对角线与通过这点的水平线之间的交叉点。例如, 点<x=1,y=1/2>的广度优先点为<x=1/2,y=1/2>。
现在,为每个父节点,我们定义首个孩子的位置为一个父亲点和深度优先会聚点之间中点的一半。那么,每个兄弟节点被定义为一个前兄弟点和广度优先会聚点中点的一半:
例如,节点2.1的位置在x=1/2, y=3/8。
现在映射定义了,很显然我们正使用密集型域:它既不是有理数,也不是实数,而是一对分数(当然,尽管两个前者已足够)。
有趣的是,父节点"1.2"的子树后代是节点"1.1"子树的一个向下缩小的复制品。 同样的,节点1.1的子树是节点"1."树的一个向下缩小的复制品,一个带自相似性的结构被称为分形图。
规范化(Normalization)
接着,我们注意到x和y并没有完全独立。假如知道他们的和,就能知道x和y两者是什么?给出有理数的分子和分母代表节点坐标的和,我们能计算x和y坐标追溯到:
function x_numer( numer integer, denom integer )
RETURN integer IS
ret_num integer;
ret_den integer;
BEGIN
ret_num := numer+1;
ret_den := denom*2;
while floor(ret_num/2) = ret_num/2 loop
ret_num := ret_num/2;
ret_den := ret_den/2;
end loop;
RETURN ret_num;
END;
function x_denom( numer integer, denom integer )
...
RETURN ret_den;
END;
在这边函数x_denom主体与x_numer的不同仅在返回变量。通俗地说,number+1的递增将ret_num/ret_den点垂直地向对角线方向移动,且x的坐标是这个值的一半,所以我们仅能让分母乘2。接着,分子和分母我们均减少相同的二次幂。
当然,y坐标被定义为和的一个补数:
function y_numer( numer integer, denom integer )
RETURN integer IS
num integer;
den integer;
BEGIN
num := x_numer(numer, denom);
den := x_denom(numer, denom);
while den < denom loop
num := num*2;
den := den*2;
end loop;
num := numer - num;
while floor(num/2) = num/2 loop
num := num/2;
den := den/2;
end loop;
RETURN num;
END;
function y_denom( numer integer, denom integer )
...
RETURN den;
END;
现在测试(这里39/32的节点是1.3.1):
select x_number(39,32)||'/'||x_denom(39,32),
y_number(39,32)||'/'||y_denom(39,32) from dual
5/8 19/32
select 5/8+19/32,39/32 from dual
1.21875 1.21875
我不用一个浮数点来代表一个实数,而所有函数用整数计算来替代。说穿了,是浮点数的一般概念,和IEEE标准,尤其是仅对渲染3D游戏有益。在最后的测试中,尽管我们使用一个浮点来验证5/8和19/32,通过前查询的返回,证明确实增加到了39/32。
我们将存储这两个整数--分子和分母的x和y坐标和--做为一个编码节点路径。碰巧,Celko的嵌套集合也是是两个整数。不像嵌套集合,我们的映射是稳定的:每个节点在xy平面有一个预定义的位置,在涉及节点位置的层次查询时不需引用数据库便能回应。在这方面,我们的分层模型本质上是一个有理数编码的原型路径。
查找父编码和兄弟编号
给一个 numer/denom编码的孩子节点,我们可以这样找它的父节点:
function parent_numer( numer integer, denom integer )
RETURN integer IS
ret_num integer;
ret_den integer;
BEGIN
if numer=3 then
return NULL;
end if;
ret_num := (numer-1)/2;
ret_den := denom/2;
while floor((ret_num-1)/4) = (ret_num-1)/4 loop
ret_num := (ret_num+1)/2;
ret_den := ret_den/2;
end loop;
RETURN ret_num;
END;
function parent_denom( numer integer, denom integer )
...
RETURN ret_den;
END;
背后的算法如下:假如节点已是最顶层--则所有这些节点有一个分子等于3--且节点没有父亲。否则,我们须垂直下移xy平面到跟深度优先会聚点相等的距离。如果节点正好是第一个孩子,那么这就是回应。否则我们须平移到跟广度优先会聚点相等的距离直到见到父节点。
这里是测试方法(在这27/32的节点是2.1.2,当7/8是2.1时);
select parent_numer(27,32)||'/'||parent_denom(27,32) from dual
7/8
在前面的方法,当横向导航时节将得到兄弟编号的计算步骤:
function sibling_number( numer integer, denom integer )
RETURN integer IS
ret_num integer;
ret_den integer;
ret integer;
BEGIN
if numer=3 then
return NULL;
end if;
ret_num := (numer-1)/2;
ret_den := denom/2;
ret := 1;
while floor((ret_num-1)/4) = (ret_num-1)/4 loop
if ret_num=1 and ret_den=1 then
return ret;
end if;
ret_num := (ret_num+1)/2;
ret_den := ret_den/2;
ret := ret+1;
end loop;
RETURN ret;
END;
一个节点在最近的一级的一个特殊停止条件,ret_num=1和ret_den=1是必须的。
那测试:
select sibling_number(7,8) from dual;
1
计算具体路径和节点间的距离
严格来讲,我们没有使用具体路径,由于我们的编码是可选择的。另一方面,一个具体路径在
层次结构上能提供
一个更直觉的节点位置,这样我们能使用具体路径为数据输入/出,假如我们提供映射到我们的模型。
从上节来看实现是一个简单的方法应用。我们打印兄弟编号,跳到父节点,然后重复以上两步直到根部为止。
function path( numer integer, denom integer )
RETURN varchar2 IS
BEGIN
if numer is NULL then
return '';
end if;
RETURN path(parent_numer(numer, denom),
parent_denom(numer, denom))
|| '.' || sibling_number(numer, denom);
END;
select path(15,16) from dual
.2.1.1
现在我们准备来写主要的查询:给2个节点,P和C,何时节P是C的父节点?
如果从P可达C,
一个很普通的查询将返回P和C之间的居次编号和一些异常提示;反之:
function distance( num1 integer, den1 integer, num2 integer, den2 integer )
RETURN integer IS
BEGIN
if num1 is NULL then
return -999999;
end if;
if num1=num2 and den1=den2 then
return 0;
end if;
RETURN 1+distance(parent_numer(num1, den1), parent_denom(num1, den1), num2,den2);
END;
select distance(27,32,3,4) from dual
2
负数字都作为异常来处理。假如节点num1/den1从num2/den2不可达,那么将导向回根部,且将返回层次(num1/den1)-999999(读者建议找个更得体的解释)。
可选择方式来回答是否通过简单计算x和y坐标来连接两个节点,然后检查是否父节点闭区间于孩子。尽管没有涉及磁盘方法,检查是否偏序的节点间存在似乎更小代价。另一方面,它仅是一个比较两个整数是否为原子操作的人工打造的计算机体系结构。该方法的更完美的实现将包含一个无限区间的整数域(这些类型的数字是计算机系统所支持的),那么一个比较操作也将得循环。
我们的系统不会完全没有一个路径的反向函数,一当提供路径时,它会返回一个节点numer/denom的值。让我们介绍两个辅助函数,首先:
function child_numer
( num integer, den integer, child integer )
RETURN integer IS
BEGIN
RETURN num*power(2, child)+3-power(2, child);
END;
function child_denom
( num integer, den integer, child integer )
RETURN integer IS
BEGIN
RETURN den*power(2, child);
END;
select child_numer(3,2,3) || '/' ||
child_denom(3,2,3) from dual
19/16
例如,节点1(编码为3/2)的第三个孩子节点节点是1.3(编码为19/16)。
路径编码函数是:
function path_numer( path varchar2 )
RETURN integer IS
num integer;
den integer;
postfix varchar2(1000);
sibling varchar2(100);
BEGIN
num := 1;
den := 1;
postfix := '.' || path || '.';
while length(postfix) > 1 loop
sibling := substr(postfix, 2, instr(postfix,'.',2)-2);
postfix := substr(postfix, instr(postfix,'.',2), length(postfix) -instr(postfix,'.',2)+1);
num := child_numer(num,den,to_number(sibling));
den := child_denom(num,den,to_number(sibling));
end loop;
RETURN num;
END;
function path_denom( path varchar2 ) ...
RETURN den;
END;
select path_numer('2.1.3') || '/' || path_denom('2.1.3') from dual
51/64
最后测试
现在基础架构已完成,我们可以测试它,让我们创建一个层次结构
create table emps (
name varchar2(30),
numer integer,
denom integer
)
alter table emps
ADD CONSTRAINT uk_name UNIQUE (name) USING INDEX
(CREATE UNIQUE INDEX name_idx on emps(name))
ADD CONSTRAINT UK_node
UNIQUE (numer, denom) USING INDEX
(CREATE UNIQUE INDEX node_idx on emps(numer, denom))
然后填入一些数据:
insert into emps values ('KING',
path_numer('1'),path_denom('1'));
insert into emps values ('JONES',
path_numer('1.1'),path_denom('1.1'));
insert into emps values ('SCOTT',
path_numer('1.1.1'),path_denom('1.1.1'));
insert into emps values ('ADAMS',
path_numer('1.1.1.1'),path_denom('1.1.1.1'));
insert into emps values ('FORD',
path_numer('1.1.2'),path_denom('1.1.2'));
insert into emps values ('SMITH',
path_numer('1.1.2.1'),path_denom('1.1.2.1'));
insert into emps values ('BLAKE',
path_numer('1.2'),path_denom('1.2'));
insert into emps values ('ALLEN',
path_numer('1.2.1'),path_denom('1.2.1'));
insert into emps values ('WARD',
path_numer('1.2.2'),path_denom('1.2.2'));
insert into emps values ('MARTIN',
path_numer('1.2.3'),path_denom('1.2.3'));
insert into emps values ('TURNER',
path_numer('1.2.4'),path_denom('1.2.4'));
insert into emps values ('CLARK',
path_numer('1.3'),path_denom('1.3'));
insert into emps values ('MILLER',
path_numer('1.3.1'),path_denom('1.3.1'));
commit;
所有函数在前节已编写可方便地连接到一个单独的视图中:
create or replace
view hierarchy as
select name, numer, denom,
y_numer(numer,denom) numer_left,
y_denom(numer,denom) denom_left,
x_numer(numer,denom) numer_right,
x_denom(numer,denom) denom_right,
path (numer,denom) path,
distance(numer,denom,3,2) depth
from emps
相关推荐
- **查询效率高**:相比于递归查询,Nested Sets 可以通过简单的 SQL 语句获取整个分支或层级。 - **操作简单**:添加、删除和移动节点都只需要更新左右值,不需要重构整个树。 ### 7. 注意事项 - 保持 `lft` 和 `...
为了解决这一性能瓶颈问题,研究者们提出了一种新的技术,即NEVE(Nested Virtualization Extensions for ARM)。NEVE通过一系列简化的架构改变,允许软件合并和延迟陷阱处理,即通过记录hypervisor指令的结果,直到...
"Nested Sets Model"(嵌套集合模型)是一种在关系型数据库中存储和操作树形结构数据的有效方法,尤其适用于需要频繁进行添加、删除和遍历操作的情况。在本案例中,我们关注的是如何在ThinkPHP5(简称TP5)框架下...
《MK.Joe.Celkos.Trees.and.Hierarchies.in.SQL.for.Smarties》是一部专为SQL高手设计的书籍,深入探讨了如何在SQL中处理树状结构和层次关系。作者Joe Celko是一位资深的数据库理论专家,他以其独特且深入浅出的方式...
yii2-nested-sets, Yii框架的嵌套集行为 nest 2的行为 一种利用改进的预排序树遍历算法的Yii框架的现代嵌套。安装安装这个扩展的首选方法是通过 composer插件。运行$ composer require creocoder/yii2-nes
nested exception is: java.net.BindException: Address already in use: JVM_Bind 这里说的是1099端口被其它进程占用了. 二.解决办法 找出占用1099端口的进程,进入windows命令,查看什么进程占用了1099端口...
1.8. Chapter 8: Nested fragments 1.9. Chapter 9: Action Bars 1.10. Chapter 10: Navigation Drawers 1.11. Chapter 11: SQLite databases 1.12. Chapter 12: Cursors and AsyncTasks 1.13. Chapter 13: Services...
在Java编程中,`java.sql.SQLException: 结果集已耗尽` 是一个常见的错误提示,通常出现在处理数据库查询结果集时。这个异常表明程序试图访问已经没有数据的结果集中下一行,即所有行已经被遍历完,尝试访问超出范围...
2013-08-12 14:33:37.672:WARN::Nested in org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory' defined in URL [file:/E:/cloudwave-core/src/main/...
The top-rated visual scripting tool for Unity used in Hearthstone, INSIDE, Hollow Knight, The First Tree, Observation, Dreamfall Chapters and more: Project Showcase Artists and Designers: Realize ...
安装通过Composer安装: composer require paulzi/yii2-nested-sets 或添加" paulzi/yii2-nested-sets " : " ^1.0 " 到composer.json文件的require部分。迁移示例警告! depth属性不能被无符号! 单树迁移: class m...
首先,我们来看`table.sql`文件,这通常包含用于创建数据库表的SQL语句,可能是为了演示Nested事务如何影响数据。在Spring中,事务管理器会根据配置决定何时开始和结束事务,以及如何处理异常。当数据库表的结构与...
Discrete Choice Analysis presents these results in such a way that they are fully accessible to the range of students and professionals who are involved in modelling demand and consumer behavior in ...
- **Variables and Constants**: How to declare and use variables and constants in PL/SQL. - **Operators**: Overview of arithmetic, relational, logical, and concatenation operators. - **Control ...
首先,我们来看`NestedSets.cst`文件,这很可能是CodeSmith模板的核心,它包含了用于创建嵌套集模式的SQL语句模板。CodeSmith是一个强大的代码生成工具,允许开发者通过模板语言来定制生成的代码。`NestedSets.cst`...
启动会报错:nested exception is com.microsoft.sqlserver.jdbc.SQLServerException: The driver could not establish a secure connection to SQL Server by using Secure Sockets Layer (SSL) encryption....
Nested Spaces and Complementary Spaces Scaling Functions and Wavelets Lecture 10 Refinement Equation: Iterative and Recursive Solution Techniques Infinite Product Formula Filter Bank Approach for ...
What's new in SQL Prompt SQL Prompt is now supported in SQL Server Management Studio 18! SQL Prompt is now supported in Visual Studio 2019! SQL Prompt now requires .Net Framework 4.7.2 or later. You ...
SQL中的嵌套间隔丹·海泽尔(Dan Hazel)使用有理数来键入嵌套集的实现(2008) 存储过程插入节点删除节点移动节点助手程序重置树InsertRandomNodeDeleteRandomNode 版权所有Eric Goetschalckx,2015年