树形层次结构(Hierarchy)经常出现在有结构的数据中,T-SQL新增数据类型HierarchyID, 其长度可变,用于存储层次结构中的路径。HierarchyID表示的层次结构是树形的,由应用程序来生成和分配 HierarchyID的值,建立父子节点之间的关系。
HierarchyID数据类型支持深度优先顺序的比较,对于两个HierarchyID值 a和b,a<b意味着,在深度优先遍历时,先遍历到a,后遍历到b,也就是说,值越小,越接近根节点。对Hierarchy数据类型创建索引,是按照深度优先,先左后右的顺序来排序的。左和右是根据节点的值来判断的,在同一深度上,值较小的节点在父节点的左边。
一,类型的赋值
HierarchyID数据类型存储的是单个节点在树形结构中的路径(Path),路径从根节点(Root Node)开始,根节点是“/”,路径以“/”结尾,使用整数表示一个节点。这意味着HierarchyID的值必须以“/”开头,以“/”结尾,“/”之间使用数值(正整数或正小数)标识一个元素,例如:“/”,“/1/2/”,“/1/2/3/”,”/1/2.1/3″。
有3种赋值方式,通过字符串赋值,字符串转换和通过整数赋值。
declare @ha HierarchyID declare @hb HierarchyID declare @hc HierarchyID set @ha='/1/2/3/' set @hb=HierarchyID::Parse('/1/2/3/') set @hc=0x5B5E select @ha as ha,@hb.ToString() as hb,@hc.ToString() as hc
二,按深度优先顺序进行比较
给定两个 hierarchyid 值 a 和 b,a<b 表示在对树进行深度优先遍历时,先找到 a,后找到 b。hierarchyid 数据类型的索引按深度优先顺序排序,在深度优先遍历中相邻的节点的存储位置也相邻。同级别的节点,左边节点小于右边节点,表示左边先被遍历到。
declare @ha HierarchyID declare @hb HierarchyID declare @hc HierarchyID set @ha=HierarchyID::Parse('/1/2/') set @hb=HierarchyID::Parse('/1/2/3/') set @hc=HierarchyID::Parse('/1/2/4/') select iif(@ha>=@hb,'>=','<'),iif(@hb>=@hc,'>=','<')
三,用于HierarchyID数据类型的函数
1,获取当前值的级数(Level)
调用GetLevel()查看HierarchyID的Level,值是从root节点开始的层数
declare @ha HierarchyID set @ha=HierarchyID::Parse('/1/2/3/') select @ha.GetLevel() as Level
2,获取根节点
静态方法GetRoot(),静态方法的调用格式:HierarchyID::GetRoot()
select HierarchyID::GetRoot().ToString() as TootString,HierarchyID::GetRoot() as RootHierarchyID
3,返回子节点
GetDescendant(childleft,childright)用以返回父级的一个子节点,返回的子节点和child是同level的。
declare @sa Nvarchar(100) declare @sb Nvarchar(100) declare @sr Nvarchar(100) declare @ha HierarchyID declare @hb HierarchyID declare @hr HierarchyID set @sa='/1/2/3/' set @sb='/1/2/6/' set @sr='/1/2/' set @ha=HierarchyID::Parse(@sa) set @hb=HierarchyID::Parse(@sb) set @hr=HierarchyID::Parse(@sr) select @hr.GetDescendant(null,null).ToString(), @hr.GetDescendant(@ha,null).ToString(), @hr.GetDescendant(@ha,@hb).ToString()
如果LeftChild是‘/1/2/3’,RightChild是‘/1/2/4’,需要在这两个节点之间插入一个新的节点,需要如何处理?表示节点的数字,并不一定必须是正整数,小数也可以,如下,NewChild=’/1/2/3.1/‘;
declare @sa Nvarchar(100) declare @sb Nvarchar(100) declare @sr Nvarchar(100) declare @ha HierarchyID declare @hb HierarchyID declare @hr HierarchyID set @sa='/1/2/3/' set @sb='/1/2/4/' set @sr='/1/2/' set @ha=HierarchyID::Parse(@sa) set @hb=HierarchyID::Parse(@sb) set @hr=HierarchyID::Parse(@sr) select @hr.GetDescendant(null,null).ToString(), @hr.GetDescendant(@ha,null).ToString(), @hr.GetDescendant(@ha,@hb).ToString()
4,判断两个节点之间的父子关系
判断是否是节点的后代,child.IsDescendantOf(parent),如果是,返回1,如果不是,返回0
declare @sa Nvarchar(100) declare @sb Nvarchar(100) declare @sr Nvarchar(100) declare @ha HierarchyID declare @hb HierarchyID declare @hr HierarchyID set @sa='/1/2/3/' set @sb='/1/2/6/' set @sr='/1/2/' set @ha=HierarchyID::Parse(@sa) set @hb=HierarchyID::Parse(@sb) set @hr=HierarchyID::Parse(@sr) select @ha.IsDescendantOf(@hr), @hb.IsDescendantOf(@hr), @ha.IsDescendantOf(@hb)
四,HierarchyID的值的更新
更新HierarchyID的值,必须级联地更新与该节点相关的子节点的值,这是由于HierarchyID类型自身的局限性导致的。
HierarchyID数据类型具有以下局限性:
- 类型为 HierarchyID的列不会自动表示树。由应用程序来生成和分配 hierarchyid 值,使行与行之间的所需关系反映在这些值中。 某些应用程序可能具有 hierarchyid 类型的列,该列指示在另一个表中定义的层次结构中的位置。
- 由应用程序来管理生成和分配 hierarchyid 值时的并发情况。不能保证列中的 hierarchyid 值是唯一的,除非应用程序使用唯一键约束或应用程序自身通过自己的逻辑来强制实现唯一性。
- 由 hierarchyid 值表示的层次结构关系不是像外键关系那样强制实现的。 可能会出现下面这种层次结构关系而且有时这种关系是合理的:A 具有子级 B,然后删除了 A,导致 B 与一条不存在的记录之间存在关系。 如果这种行为不可接受,应用程序在删除父级之前必须先查询其是否有后代
1,创建数据源
create table dbo.emph2 ( idpath hierarchyid not null primary key, id int not null, parentid as idpath.GetAncestor(1) persisted foreign key references dbo.emph2(idpath), descr varchar(100) )
idpath=’/1/2/6/‘的子孙节点如下图
select e.idpath.ToString() as IDPath,e.id,e.parentid.ToString() as ParentIDPath,e.descr from dbo.emph2 e where e.idpath.IsDescendantOf(HierarchyID::Parse('/1/2/6/'))=1
2,把子节点变成另一个节点的父节点
例如,把idpath=’/1/2/6/‘ 的节点删除,并将其子节点的父节点变更为idpath=’/1/2/7/‘
由于存在外键关系,必须先变更子节点的父节点,然后再删除idpath=’/1/2/6/‘ 的节点。
--delete child notes --select e.idpath.ToString() as IDPath,e.id,e.parentid.ToString() as ParentIDPath,e.descr update e set e.idpath=HierarchyID::Parse('/1/2/7/'+cast(e.id as varchar)+'/') from dbo.emph2 e where e.idpath.IsDescendantOf(HierarchyID::Parse('/1/2/6/'))=1 and e.idpath!=HierarchyID::Parse('/1/2/6/') --delete parent note delete dbo.emph2 where idpath=HierarchyID::Parse('/1/2/6/') --check select e.idpath.ToString() as IDPath,e.id,e.parentid.ToString() as ParentIDPath,e.descr from dbo.emph2 e where e.idpath.IsDescendantOf(HierarchyID::Parse('/1/2/7/'))=1
3,变更父节点
例如,把idpath=’/1/2/6/‘的节点的父节点变更,其子节点仍然是其子节点。
思路是新建一个节点,并将子节点都挂在新节点下。
--create new node insert into dbo.emph2(idpath,id,descr) select HierarchyID::Parse('/1/3/6/'),id,descr from dbo.emph2 e where e.idpath=HierarchyID::Parse('/1/2/6/') --delete child notes --select e.idpath.ToString() as IDPath,e.id,e.parentid.ToString() as ParentIDPath,e.descr update e set e.idpath=HierarchyID::Parse('/1/3/6/'+cast(e.id as varchar)+'/') from dbo.emph2 e where e.idpath.IsDescendantOf(HierarchyID::Parse('/1/2/6/'))=1 and e.idpath!=HierarchyID::Parse('/1/2/6/') --delete parent note delete dbo.emph2 where idpath=HierarchyID::Parse('/1/2/6/') --check select e.idpath.ToString() as IDPath,e.id,e.parentid.ToString() as ParentIDPath,e.descr from dbo.emph2 e where e.idpath.IsDescendantOf(HierarchyID::Parse('/1/3/6/'))=1
4,定向插入新的节点
由于节点之间存在先后顺序,使用GetDescendant(ChildLeft,ChildRight)保证顺序。
在节点 idpath=’/1/2/6/‘ 的子节点 id=15,id=16之间插入一个新的子节点,新的子节点的id=36,descr=‘E1136’,思路是使用GetDescendant(ChildLeft,ChildRight)获取新的IDPath,然后插入到表中。
declare @id int declare @descr Nvarchar(100) declare @sa Nvarchar(100) declare @sb Nvarchar(100) declare @sr Nvarchar(100) declare @hnew HierarchyID set @id=36 set @descr='E1136' set @sa='/1/2/6/15/' set @sb='/1/2/6/16/' set @sr='/1/2/6/' set @hnew= HierarchyID::Parse(@sr).GetDescendant(HierarchyID::Parse(@sa),HierarchyID::Parse(@sb)) insert into dbo.emph2(idpath,id,descr) values(@hnew,@id,@descr)
select e.idpath.ToString() as IDPath,e.id,e.parentid.ToString() as ParentIDPath,e.descr from dbo.emph2 e where e.idpath.IsDescendantOf(HierarchyID::Parse('/1/2/6/'))=1 order by e.idpath
从排序的结果集中可以看出,id=36的节点,处于id=15和id=16的节点之间,通过GetDescendant(ChildLeft,ChildRight)实现了顺序。
五, 遍历
HierarchyID类型的数据,很容易实现广度优先遍历和深度优先遍历
1,广度优先遍历是指查询层次结构中相同级别的节点
select idpath.ToString() as IDPath,id,parentid.ToString() as ParentIDPath,descr from dbo.emph2 where idpath.GetLevel()=2
2,深度优先遍历是指遍历一个节点的所有子节点
select idpath.ToString() as IDPath,id,parentid.ToString() as ParentIDPath,descr from dbo.emph2 where idpath.IsDescendantOf(HierarchyID::Parse('/1/2/6/'))=1
参考文档:
hierarchyid data type method reference