[Sql Server2008]树结构的递归算法

http://blog.csdn.net/tonyzhou2008/article/details/5100683

本文主要讲述三个内容: 
1.如何创建hierarychyid的表,插入数据及基本递归查询。 
2.介绍hierarchyid的10种专有函数。 
3.介绍hierarchyid特有的深度优先索引(Depth-First Indexing)和广度优先索引(Breadth-First Indexing)

在上一节中

http://blog.csdn.net/tjvictor/archive/2009/07/30/4395677.aspx
我们已经演示了如何在SQL Server中通过主键和外键来存储如下图所示的树型结构数据

虽然通过主键和外键的相互搭配可以满足我们的查询、存储需求,但是这种方式并不易于管理和维护,幸运的是,在SQL Server 2008中提供了一种新的数据类型hierarchyid和相关的操作方法来存储和查询这种树型层次关系数据。

首先创建数据表: 
create database TestDb 
go 
use TestDb 
go 
Create table EmployeeTreeTable 

NodeId        hierarchyid PRIMARY KEY, 
NodeLevel     AS NodeId.GetLevel(), 
EmployeeId    int UNIQUE NOT NULL, 
EmployeeName  nvarchar(32) NOT NULL, 

NodeId是记录树型层次的Id,是hierarchyid类型。NodeLevel是个计算列,用于存储当前树是深度值,根节点为0。关于NodeId.GetLevel()方法将在下面章节中详细介绍。

按照上图所示的层次关系为表插入数据: 
--插入数据 
declare @DepthNode hierarchyid;--深度Id 
declare @BreadthNode hierarchyid;--广度Id 
--插入根节点 
insert into EmployeeTreeTable values(hierarchyid::GetRoot(),1,‘项目经理‘) 
--计算深度并插入子节点2 
select @DepthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 1; 
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(null,null),2,‘技术经理‘); 
--计算节点2广度,在节点2右边插入节点3 
select @BreadthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 2; 
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(@BreadthNode,null),3,‘产品经理‘); 
--计算节点3广度,在节点3右边插入节点4 
select @BreadthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 3; 
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(@BreadthNode,null),4,‘测试经理‘); 
--计算节点2深度并插入子节点5 
select @DepthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 2; 
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(null,null),5,‘技术组长1‘); 
--计算节点5广度,在节点5右边插入节点6 
select @BreadthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 5; 
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(@BreadthNode,null),6,‘技术组长2‘); 
--计算节点4深度并插入子节点7 
select @DepthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 4; 
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(null,null),7,‘测试员工1‘); 
--计算节点5深度并插入子节点8 
select @DepthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 5; 
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(null,null),8,‘技术员工1‘); 
--计算节点8广度,在节点8右边插入节点9 
select @BreadthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 8; 
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(@BreadthNode,null),9,‘技术员工2‘); 
--计算节点9广度,在节点9右边插入节点10 
select @BreadthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 9; 
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(@BreadthNode,null),10,‘技术员工3‘); 
go 
select * from EmployeeTreeTable 
结果集为: 
NodeId    NodeLevel    EmployeeId   EmployeeName 
0x           0                   1                    项目经理 
0x58       1                   2                    技术经理 
0x5AC0   2                   5                    技术组长1 
0x5AD6   3                   8                    技术员工1 
0x5ADA   3                   9                    技术员工2 
0x5ADE   3                   10                  技术员工3 
0x5B40   2                   6                    技术组长2 
0x68       1                   3                    产品经理 
0x78       1                   4                    测试经理 
0x7AC0   2                   7                    测试员工1

1.查询技术组长1所有子节点的员工信息 
select * from EmployeeTreeTable 
    where NodeId.IsDescendantOf(0x5AC0)=1--0x5AC0是技术组长1的NodeId

2.查询技术组长1所有父节点的员工信息 
with c as 

    select * from EmployeeTreeTable where EmployeeId = 5 
    union all 
    select a.* from EmployeeTreeTable as a 
    join c on a.NodeId = c.NodeId.GetAncestor(1) 

select * from c

上面的例子中,使用了很多hierarchyid专有的函数,可能大家还不熟悉,下面我将具体介绍一下hierarchyid的10个函数,分别为: 
GetRoot,GetLevel,GetAncestor,GetDescendant,IsDescendantOf,ToString,Parse,GetReparentedValue,Read,Write。 
1.GetRoot。返回层次结构树的根节点。注意GetRoot() 是静态方法。 
关于SQL中静态方法和实例方法的区别请参见:http://blog.csdn.net/tjvictor/archive/2009/07/29/4390673.aspx 
SQL:select * from EmployeeTreeTable where NodeId = hierarchyid::GetRoot() 
结果集: 
NodeId    NodeLevel    EmployeeId    EmployeeName 
0x            0                  1                     项目经理

2.返回一个表示节点在树中的深度的整数。 
前面建表时我们已经使用了这个函数,NodeLevel字段就是用这个函数自动创建的。 
SQL:select EmployeeName,NodeId.GetLevel() as TreeLevel from EmployeeTreeTable 
结果集为: 
EmployeeName    TreeLevel 
项目经理                0 
技术经理                1 
技术组长1              2 
技术员工1              3 
技术员工2              3 
技术员工3              3 
技术组长2              2 
产品经理                1 
测试经理                1 
测试员工1              2

3.GetAncestor返回表示本节点为的第 n 个父节点的 hierarchyid。 
SQL: 
declare @NodeId hierarchyid 
select @NodeId=NodeId from EmployeeTreeTable where EmployeeId = 5 
select EmployeeName,NodeLevel from EmployeeTreeTable where NodeId = @NodeId.GetAncestor(0) 
select EmployeeName,NodeLevel from EmployeeTreeTable where NodeId = @NodeId.GetAncestor(1) 
select EmployeeName,NodeLevel from EmployeeTreeTable where NodeId = @NodeId.GetAncestor(2) 
结果集为: 
EmployeeName    NodeLevel 
技术组长1              2 
技术经理                1 
项目经理                0 
@NodeId.GetAncestor(0) 取自己节点的Id,@NodeId.GetAncestor(1)取父节点的Id,@NodeId.GetAncestor(2)取爷节点的Id,以此类推。

4.GetDescendant返回父级的一个子节点

如果父级为 NULL,则返回 NULL。 
如果父级不为 NULL,而 child1 和 child2 为 NULL,则返回父级的子级。 
如果父级和 child1 不为 NULL,而 child2 为 NULL,则返回一个大于 child1 的父级的子级。 
如果父级和 child2 不为 NULL,而 child1 为 NULL,则返回一个小于 child2 的父级的子级。 
如果父级、child1 和 child2 都不为 NULL,则返回一个大于 child1 且小于 child2 的父级的子级。 
如果 child1 不为 NULL 且不是父级的子级,则引发异常。 
如果 child2 不为 NULL 且不是父级的子级,则引发异常。 
如果 child1 >= child2,则引发异常。 
我们在插入的SQL语句中已经使用过了这个方法,这里就不再给出SQL示例,请大家参考前面的插入SQL语句。 
5.IsDescendantOf如果子节点为本节点的后代,则返回 true 
SQL:select * from EmployeeTreeTable where NodeId.IsDescendantOf(0x58)=1 
结果集为: 
NodeId    NodeLevel    EmployeeId    EmployeeName 
0x58        1                  2                  技术经理 
0x5AC0    2                  5                  技术组长1 
0x5AD6    3                  8                  技术员工1 
0x5ADA    3                  9                  技术员工2 
0x5ADE    3                  10                技术员工3 
0x5B40    2                  6                  技术组长2

6.ToString返回具有本节点逻辑表示形式的字符串 
SQL:select *,NodeId.ToString() as Path from EmployeeTreeTable 
结果集为: 
NodeId    NodeLevel    EmployeeId    EmployeeName    Path 
0x            0                  1                     项目经理                / 
0x58        1                  2                    技术经理               /1/ 
0x5AC0    2                  5                     技术组长1             /1/1/ 
0x5AD6    3                  8                     技术员工1             /1/1/1/ 
0x5ADA    3                 9                     技术员工2             /1/1/2/ 
0x5ADE    3                  10                   技术员工3             /1/1/3/ 
0x5B40    2                 6                     技术组长2             /1/2/ 
0x68        1                 3                     产品经理               /2/ 
0x78        1                 4                     测试经理               /3/ 
0x7AC0    2                 7                    测试员工1             /3/1/

7.Parse将hierarchyid 的规范字符串表示形式转换为hierarchyid值。即与ToString()函数是相反函数。Parse是静态函数。 
SQL: 
declare @Path varchar(32) = ‘/1/2/5/6/‘ 
select hierarchyid::Parse(@Path) 
结果集为:0x5B6394

8.GetReparentedValue把当前节点从旧路径更新到新路径 
下面的SQL是把技术员工3,从技术组长1节点更新到技术组长2下面。 
SQL: 
declare @OldNode hierarchyid=0x5AC0; 
declare @NewNode hierarchyid=0x5B40; 
update EmployeeTreeTable set NodeId = NodeId.GetReparentedValue(@OldNode,@NewNode) 
    where EmployeeId = 10 
结果集中技术员工3的路径从/1/1/3/变成了/1/2/3/。 
关于GetReparentedValue的用法比较复杂,我在介绍索引后,会更加详细的说明各种替换情况。

9.Read和Write 
Read和Write是供CLS调用的,不能在T-SQL中直接使用。所以这里就不具体介绍两个函数的使用方法了。

hierarchyid有深度优先索引和广度优先索引 
当递归查询父子节点时,会利用到深度优先索引;当平行查询兄弟节点时,会利用到广度优先索引。 
深度优先索引图: 
 
广度优先索引图:

1.建立深度优先索引: 
深度优先索引是hierarchyid默认的索引,只要在hierarchyid列上建立主键,那么就会自动建立hierarchyid索引。

2.建立广度优先索引 
广度优先索引必须是个唯一索引且包括NodeLevel和NodeId两列: 
CREATE UNIQUE INDEX IX_EmployeeBreadth ON Employee(NodeLevel, NodeId)

需要注意的是采用深度优先、广度优先还是结合使用这两种索引,以及将哪一种设为聚集键(如果有),取决于上述两种查询类型的相对重要性以及 SELECT 与 DML 操作的相对重要性,本文不代表一定要如此建立hierarchyid索引。

最后我们讨论一下hierarchyid的GetReparentedValue几种使用方法。 
下面我们先看一个有问题的节点更新:把技术组长1从技术经理更新到产品经理。 
SQL: 
declare @OldNode hierarchyid=0x58; 
declare @NewNode hierarchyid=0x68; 
update EmployeeTreeTable set NodeId = NodeId.GetReparentedValue(@OldNode,@NewNode) 
    where EmployeeId = 5 
go 
select NodeId.ToString(),* from EmployeeTreeTable 
结果集为: 
路径               NodeId      NodeLevel    EmployeeId    EmployeeName 
/                    0x             0                    1                    项目经理 
/1/                 0x58         1                    2                    技术经理 
/1/1/1/           0x5AD6    3                    8                    技术员工1 
/1/1/2/           0x5ADA    3                    9                    技术员工2 
/1/1/3/           0x5ADE    3                    10                  技术员工3 
/1/2/              0x5B40     2                    6                    技术组长2 
/2/                 0x68         1                    3                    产品经理 
/2/1/              0x6AC0     2                    5                    技术组长1 
/3/                 0x78         1                    4                    测试经理 
/3/1/              0x7AC0     2                    7                    测试员工1 
从结果里面可以看到技术组长已经变成了/2/1,成功更新到产品经理节点下。但是技术组长1下面的子节点技术员工1,2,3却没有相应的更新过来,还是原来的/1/1/1,2,3,但是原先的技术组长1的/1/1节点已经没有了,所以出现了所谓的“断层”现象。 
下面提出几种常用更新需求,并且给出相应的SQL实现语句。

1.职位变更。例如技术经理与产品经理职位互换。 
针对这种情况,有两种方法。一是把技术经理下面的所有节点Id都更新成产品经理节点下。这种情况变动比较大,不推荐使用。第二种方法是把技术经理的NodeId和产品经理的NodeId互换。下面使用第二种方法: 
declare @TechNode hierarchyid=0x58; 
declare @ProductNode hierarchyid=0x68; 
declare @TempNode hierarchyid=0x59; 
update EmployeeTreeTable set NodeId = @TempNode where NodeId = @TechNode; 
update EmployeeTreeTable set NodeId = @TechNode where NodeId = @ProductNode; 
update EmployeeTreeTable set NodeId = @ProductNode where NodeId = @TempNode;

2.职位升降级。例如技术组长2降级成为技术员工,被挂在技术组长1节点下: 
declare @TechTeamLeadNode1 hierarchyid=0x5AC0; 
declare @TechEmployeeNode3 hierarchyid=0x5ADE; 
update EmployeeTreeTable set NodeId = @TechTeamLeadNode1.GetDescendant(@TechEmployeeNode3,null) 
    where EmployeeId = 6 
部分结果集为: 
Path        NodeId    NodeLevel    EmployeeId    EmployeeName 
/1/1/4/    0x5AE1    3                  6                     技术组长2 
可见,技术组长2从/1/2变成了/1/1/4

总结: 
SQL Server 2008提供的hierarchyid类型使我们能够灵活、方便的操作树型结构。关于hierarchyid还有很多深入的知识,很多灵活的用法,本文不可能一一涉及,这里仅是介绍一些基本用法,抛砖引玉,如果大家在以后的使用中发现什么问题或是更好的解决方案,请联系我。

本文来自CSDN博客:http://blog.csdn.net/tjvictor/archive/2009/07/30/4395681.aspx

时间: 2024-08-30 11:08:11

[Sql Server2008]树结构的递归算法的相关文章

SQL SERVER2008数据库常识

连接数据库的方式: 1 .SQL SERVER 身份验证法:需要输入用户名和密码验证才能连接到数据库. 2 .Windows 身份验证法,点击连接,直接访问到数据库. 2.怎么在Microsoft SQL SERVER2008数据库上修改用户名和密码: 打开数据库--------点开安全性文件夹: 点开登录名的文件夹------找到自己数据库里面的所有数据库的用户名(在我本机使用的是sa),然后鼠标右击-----属性,在弹出页面修改用户名和密码即可. 3.Microsoft SQL SERVER

SQL SERVER2008历史日志查询

有需要找个工具能够查询sql server历史操作日志,比如误删除,误操作等,网上搜了好多,没有一个靠谱的.当然排除自己写sql记录操作日志,俺不懂sql语言.有可用的工具求推荐,感谢. log explorer for sql server 支持到SQL2005,以上不支持. sql server自带的管理--sql server日志 记录的登录和错误日志 强大的sql server profiler 主要作为实时分析进程或排错来用,不能查询历史日志,除非一直开着 查询transaction

php5.3.x连接MS SQL server2008

开篇 因为毕设老师需求的原因,虚拟旅游网站要求的数据库必须使用MS SQL server. 我最擅长的web编程语言是PHP,但是在PHP中链接MS SQL server是一件非常麻烦的事,我个人分析造成这种麻烦的原因:是因为使用PHP的一大优点就是免费,然而MS SQL server虽然图形化界面操作起来简单,但是其昂贵的授权费让人望而却步:加之MySQL不俗的性能和强大的社区支持,使得真正企业环境里,使用PHP + MS SQL server的人越来越少. 其实纵观网上的文章,之所以有人选择

不能连接MS Sql Server2008数据库的问题

前几天在计算机上安装了Win8企业版,又安装了MS Sql Server2008,本地开发比较顺畅,可是别的计算机的客户端却不能访问数据库. 先Ping一下,不通,可是它却能ping通别的计算机,可能是设置问题: 1.在运行里输入gpedit.msc,或者进入管理工具,找到计算机设置->windows设置->安全设置->本地策略->用户权限分配->拒绝从网络访问此计算机->删除guest用户,确定.(没有解决问题) 2.打开控制面板,进入管理工具,打开guest账户,G

Java连接Sql Server2008

参考:http://weistar.iteye.com/blog/1744871 准备工作: 1.下载JDBC驱动包:http://www.microsoft.com/zh-cn/download/details.aspx?id=21599 2.下载 完成后,点击运行,会提示你选择解压目录. 3.解压完成后,进入 <你解压到得目录>\sqljdbc_3.0\chs,有sqljdbc.jar和sqljdbc4.jar,这里使用sqljdbc4.jar 4.配置Sql Server2008端口:

sql server2008笔记

系统表和临时表 在sqlserver2008中数据表分为普通表,分区表,系统表和临时表 (1)系统表 在创建好的每个数据库中,系统都会自动添加一张系统表,该表存储了与系统有关的各种信息 例如sql server2008服务器配置,数据库设置,用户和表对象的描述信息 通常只有DBO权限的用户才能对该表进行操作 (2)临时表 临时表就是临时创建,不能永久保存,临时表分为两种,本地临时表和全局临时表 本地临时表的表名通常带有#标识符,它只对当前用户可见,当用户断开sql server2008 实例连接

sql server2008的游标

sql server2008中的游标包括游标结果集和游标位置,游标结果集由定义select语句返回的行的集合,游标位置 则是指向这个结果集的某一行的指针 在使用游标之前要先声明游标,定义Transact-SQL服务器的属性,例如游标的滚动行为用于生成游标所操作的结果 集的查询 例如在学生成绩管理系统数据库中为学生信息表定义一个游标 Declare cursor1 cursor for select * from 学生信息 1.打开游标 open cursor1 2.检索游标 fetch next

使用设置sa用户登录sql server2008

今天在net项目中添加数据库过程中出现了小问题,就是使用sql server身份验证没登录成功,经过一番调试,终于解决问题. 使用sa账户登录sql server 2008 的方法步骤如下: 1.首先打开电脑,打开SQL server 2008,使用windows身份验证. 2.成功登陆SQL server2008,找到安全性-登录名,"sa"右击选择"属性",先设置sa 的密码,将"强制密码实施策略"勾上. 3.然后再选择属性页下的"

介绍 .Net工具Code Snippet 与 Sql Server2008工具SSMS Tools Pack

不久前,某某在微软写了一个很酷的工具:Visual Stuido2008可视化代码片断工具,这个工具可以在http://www.codeplex.com/SnippetDesigner上免费下载,用它可以方便地定制一批代码片段. 如何使用   1.如果要添加一个新文件,“文件”---“新建文件”---“Code Snippet File”   2.如果想要直接导入现有的代码,只需右键点击所选代码,按下“Export as Snippet”    3.编辑Snippet(可以使用$$符号表明占位符