锋利的SQL2014:层次结构操作之Hierarchyid

在8.6.4节介绍了使用递归CTE查询层次结构数据的方法,本节将介绍一种使用hierarchyid数据类型解决此问题的方法。Hierarchyid数据类型是从SQL Server2008开始提供的,专门用于解决层次结构问题。

hierarchyid使用“/”符号来表示层次结构,如顶层(根节点)为“/”,其后的子节点可以是“/1/”、“/2/”等。再之后的节点可以是“/1/1/”、“/2/1/”,其中,“/1/1/”的父级是“/1/”,“/2/1/”的父级是“/2/”。在表中,这种层次结构是以16进制方式存储的。

hierarchyid实际上是CLR数据类型,因此与其他数据类型不同的是,它具有一些进行层次节点检索的方法,如表19-26所示。

表19-26                                                    hierarchyid的方法


stu_name


exam_date


child.GetAncestor(n)


返回child的第n层节点的hierarchyid。


parent.GetDescendant(child1  , child2)


返回作为父节点后代的一个子节点hierarchyid。

如果父级为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,则引发异常。


node.GetLevel( )


用于确定当前层次的深度(级别),最顶层(根节点)为0,然后依次加1。


hierarchyid::GetRoot( )


返回最顶层(根节点)的hierarchyid。


child.IsDescendantOf ( parent )


用于判断child节点是否是parent的后代,如果是,返回1;否则,返回0。


node.ToString( )与node.Parse(  )


在表中,hierarchyid是以16进制方式表示的。ToString( )用于将16进制转换为表示层次结构的字符串,Parse( )则反之。


node.GetReparentedValue ( oldRoot, newRoot )


用于通过将节点从oldRoot移动到newRoot来修改层次树。

首先使用下面的代码来创建示例表。注意,其中的Org_Level属于计算列,它使用GetLevel方法计算Org_Node中的当前层次的深度。在插入层次数据时,可以直接使用“/1/”形式的字符串,SQL Server会自动转换存储为16进制格式。

IFOBJECT_ID(‘dbo.Employees‘, ‘U‘) IS NOT NULL

DROP TABLE dbo.Employees;

CREATE TABLE dbo.Employees

(

Org_Node hierarchyid NOT NULL,

EmployeeId INT NOT NULL,

Title VARCHAR(50) NOT NULL,

Org_Level AS Org_Node.GetLevel()

);

GO

--插入员工数据,注意第一个列的格式,用于说明层级结构,以/开始和结束

INSERT INTO dbo.EmployeesVALUES

(‘/‘,1,‘总经理‘),

(‘/1/‘,2,‘副总经理A‘),

(‘/2/‘,3,‘副总经理B‘),

(‘/1/1/‘,4,‘部门经理A‘),

(‘/1/2/‘,5,‘部门经理B‘),

(‘/1/1/1/‘,6,‘员工A‘);

执行下面的语句,查看dbo.Employees中数据,可以看到Org_Node列中以16进制存储的节点数据,如表19-27所示。

SELECT *,

Org_Node.ToString() AS Org_Node_Str

FROM dbo.Employees;

表19-27                                              dbo.Employees中的数据


Org_Node


EmployeeId


Title


Org_Level


Org_Node_Str


0x


1


总经理


0


/


0x58


2


副总经理A


1


/1/


0x68


3


副总经理B


1


/2/


0x5AC0


4


部门经理A


2


/1/1/


0x5B40


5


部门经理B


2


/1/2/


0x5AD6


6


员工A


3


/1/1/1/

19.8.1  检索祖先节点

IsDescendantOf方法可以判断指定节点是否是另一个节点的后代,如果是,则返回1。下面的示例包含两个步骤,第一条SELECT语句用于获得EmployeeId为4雇员(即部门经理A)的hierarchyid,将其存储在@EmpNode变量中。@EmpNode的数据类型为hierarchyid,因此可以使用IsDescendantOf方法。第二条SELECT语句则是根据获得hierarchyid(即“/1/1/”),从表中检索“/1/1/”是其后代的节点。

DECLARE @EmpNode AShierarchyid;

SELECT @EmpNode = Org_Node

FROM dbo.Employees

WHERE EmployeeId = 4;

SELECT *,Org_Node.ToString() AS Org_Node_Str

FROM dbo.Employees

[email protected](Org_Node) = 1;

查询结果如表19-28所示。

表19-28                                                部门经理A的祖先节点


Org_Node


EmployeeId


Title


Org_Level


Org_Node_Str


0x


1


总经理


0


/


0x58


2


副总经理A


1


/1/


0x5AC0


4


部门经理A


2


/1/1/

19.8.2  检索子树节点

将上面第二条SELECT语句中@EmpNode和Org_Node调换一下位置,就可以检索指定节点的子树节点。下面的语句判断@EmpNode中的节点是否为当前Org_Node的祖先节点,即检索“/1/1/”节点的子树节点。

DECLARE @EmpNode AS hierarchyid;

SELECT @EmpNode = Org_Node

FROM dbo.Employees

WHERE EmployeeId = 4;   --节点为“/1/1/”

SELECT *,Org_Node.ToString() AS Org_Node_Str

FROM dbo.Employees

WHERE Org_Node.IsDescendantOf(@EmpNode)= 1;

查询结果如表19-29所示。

表19-29                                                部门经理A的子树节点


Org_Node


EmployeeId


Title


Org_Level


Org_Node_Str


0x5AC0


4


部门经理A


2


/1/1/


0x5AD6


6


员工A


3


/1/1/1/

在实际应用环境中,处于性能考虑,通常是仅读取必要层次的数据,而不是像上面这样全部读取。例如,为了填充一个组织的树形结构,我们可能仅填充树形的主干,当用户单击主干的某个节点时再读取其中的数据。hierarchyid的GetAncestor方法就提供了这种分层检索功能。例如,下面的语句用于返回根节点的第一层数据,即副总经理级,查询结果如表19-30所示。

DECLARE @EmpNode AShierarchyid;

SELECT @EmpNode = CAST(‘/‘AS hierarchyid);

SELECT *,Org_Node.ToString() AS Org_Node_Str

FROM dbo.Employees

WHEREOrg_Node.GetAncestor(1) = @EmpNode;

表19-30                                                   根节点的第一层数据


Org_Node


EmployeeId


Title


Org_Level


Org_Node_Str


0x58


2


副总经理A


1


/1/


0x68


3


副总经理B


1


/2/

使用GetAncestor(0)会返回当前层级的数据。对于上面的语句而言,返回的是根节点的数据。

19.8.3  插入新节点

可以像前面创建表时那样,直接指定节点层次来插入节点,例如,下面的语句在部门经理B下新增了一名雇员。

INSERT INTO dbo.EmployeesVALUES

(‘/1/2/1/‘,7,‘员工B‘);

但是在层次比较多时,这种方式很有可能因为疏忽而造成节点隶属关系错误。比较有效的方法是使用GetDescendant方法。例如,下面的示例中第一条SELECT语句用于获得部门经理B的节点(即“/1/2/”),第二条SELECT语句用于获得部门经理B下面员工B的节点(即“/1/2/1/”),INSERT语句的作用是在部门经理B下面、员工B的后面新增一个节点(即“/1/2/2/”)。

DECLARE @Manager AShierarchyid, @Child1 AS hierarchyid;

SELECT @Manager = Org_Node

FROM dbo.Employees

WHERE EmployeeId = 5;  --获取部门经理B的节点

SELECT @Child1 = Org_Node

FROM dbo.Employees

WHERE EmployeeId = 7;  --获取上面语句新增员工B的节点

INSERT INTO dbo.EmployeesVALUES  --在“/1/2/1/”后面新增一个节点

(@Manager.GetDescendant(@Child1, NULL), 8, ‘员工C‘);

执行下面的语句,查询结果如表19-31所示。

SELECT *, Org_Node.ToString()AS Org_Node_Str

FROM dbo.Employees;

表19-31                                  使用GetDescendant插入节点后的数据


Org_Node


EmployeeId


Title


Org_Level


Org_Node_Str


0x


1


总经理


0


/


0x58


2


副总经理A


1


/1/


0x68


3


副总经理B


1


/2/


0x5AC0


4


部门经理A


2


/1/1/


0x5B40


5


部门经理B


2


/1/2/


0x5AD6


6


员工A


3


/1/1/1/


0x5B56


7


员工B


3


/1/2/1/


0x5B5A


8


员工C


3


/1/2/2/

对于父节点具有子节点的情况,应当像上面这样为GetDescendant指定Child1参数。如果没有,则可以使用NULL代替。例如,由表19-31可以看出,副总经理B下面没有节点,如果需要在其下面插入一个子节点,就可以使用NULL。参考下面的语句:

DECLARE @Manager AShierarchyid;

SELECT @Manager = Org_Node

FROM dbo.Employees

WHERE EmployeeId = 3;  --获取副总经理B的节点

INSERT INTO dbo.EmployeesVALUES  --在“/2/”下面新增一个节点

(@Manager.GetDescendant(NULL, NULL), 9, ‘部门经理C‘);

部门经理C的节点为“/2/1/”。

如果需要在员工B和员工C之间插入一个节点,应当同时指定Child1和Child2参数。参考下面的语句:

DECLARE @Manager AShierarchyid,

@Child1 AS hierarchyid,

@Child2 AS hierarchyid;

SELECT @Manager = Org_Node

FROM dbo.Employees

WHERE EmployeeId = 5;  --获取部门经理B的节点

SELECT @Child1 = Org_Node

FROM dbo.Employees

WHERE EmployeeId = 7;  --获取员工B的节点

SELECT @Child2 = Org_Node

FROM dbo.Employees

WHERE EmployeeId = 8;  --获取员工C的节点

INSERT INTO dbo.EmployeesVALUES

(@Manager.GetDescendant(@Child1, @Child2),10, ‘员工D‘);

执行下面的语句,查询结果如表19-32所示。可以看到,节点使用了小数格式,即“/1/2/1.1/”。

SELECT *,Org_Node.ToString() AS Org_Node_Str

FROM dbo.Employees;

表19-32                                        在员工B和员工C之间插入节点


Org_Node


EmployeeId


Title


Org_Level


Org_Node_Str


0x


1


总经理


0


/


0x58


2


副总经理A


1


/1/


0x68


3


副总经理B


1


/2/


0x5AC0


4


部门经理A


2


/1/1/


0x5B40


5


部门经理B


2


/1/2/


0x5AD6


6


员工A


3


/1/1/1/


0x5B56


7


员工B


3


/1/2/1/


0x5B5A


8


员工C


3


/1/2/2/


0x6AC0


9


部门经理C


2


/2/1/


0x5B58B0


10


员工D


3


/1/2/1.1/

19.8.4  变更节点位置

变更节点位置应当使用GetReparentedValue方法,该方法接受两个参数,一个是原节点的hierarchyid,另一个是目标节点hierarchyid。由表19-32可以看出,部门经理A隶属于副总经理A,下面的语句将其调整到副总经理B下面。

DECLARE @oldRoot AShierarchyid,

@newRoot AS hierarchyid;

SELECT @oldRoot = Org_Node

FROM dbo.Employees

WHERE EmployeeId = 4;  --当前节点为“/1/1/”

SELECT @newRoot =CAST(‘/2/1/‘ AS hierarchyid);  --目标结点

UPDATE dbo.Employees

SET Org_Node =Org_Node.GetReparentedValue(@oldRoot, @newRoot)

WHEREOrg_Node.IsDescendantOf(@oldRoot) = 1;

执行下面的语句,查询结果如表19-33所示。

SELECT *,Org_Node.ToString() AS Org_Node_Str

FROM dbo.Employees;

表19-33                                    将部门经理A移动到副总经理B下面


Org_Node


EmployeeId


Title


Org_Level


Org_Node_Str


0x


1


总经理


0


/


0x58


2


副总经理A


1


/1/


0x68


3


副总经理B


1


/2/


0x6AC0


4


部门经理A


2


/2/1/


0x5B40


5


部门经理B


2


/1/2/


0x5AD6


6


员工A


3


/2/1/1/


0x5B56


7


员工B


3


/1/2/1/


0x5B5A


8


员工C


3


/1/2/2/


0x6AC0


9


部门经理C


2


/2/1/


0x5B58B0


10


员工D


3


/1/2/1.1/

可以看到,部门经理A的节点变成了“/2/1/”,成功地变更到副总经理B下面。但是,这里存在一个问题,就是部门经理A与部门经理C的hierarchyid是相同的,出现了重复值,hierarchyid数据类型虽然用于表示层次结构,但它并不强制实现层次结构,层次的准确性由用户自己掌控。当然,可以通过为hierarchyid类型列建立唯一索引的方式避免出现重复条目。当然,出现此问题的原因是我们没有判断副总经理B下面层级中的最大hierarchyid值,错误地指定了一个已存在hierarchyid。下面来看一下正确的解决方法。

首先使用下面的语句恢复数据的原状。

UPDATE dbo.Employees

SET Org_Node = ‘/1/1/‘

WHERE EmployeeId = 4;

UPDATE dbo.Employees

SET Org_Node = ‘/1/1/1/‘

WHERE EmployeeId = 6;

下面的语句创建了一个存储过程,可以实现节点的变更。该过程接受两个参数,一个是需要调整雇员的D,另一个是其目标管理人员的ID。注意第三条SELECT语句中MAX函数和GetAncestor方法的使用,该语句的作用是在目标管理人员下一层级最大hierarchyid值的基础上获得一个新的hierarchyid值。

CREATE PROCEDUREMoveOrg(@EmpID int, @newMgrID int )

AS

BEGIN

DECLARE @nold hierarchyid, @nnew hierarchyid

SELECT @nold = Org_Node FROM dbo.EmployeesWHERE EmployeeId = @EmpID ;

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

BEGIN TRANSACTION

SELECT @nnew = Org_Node FROM dbo.EmployeesWHERE EmployeeId = @newMgrID ;

SELECT @nnew = @nnew.GetDescendant(MAX(Org_Node), NULL)

FROM dbo.Employees WHERE Org_Node.GetAncestor(1)[email protected] ;

UPDATE dbo.Employees

SET Org_Node =Org_Node.GetReparentedValue(@nold, @nnew)

WHERE Org_Node.IsDescendantOf(@nold) = 1 ;

COMMIT TRANSACTION

END ;

执行存储过程,可以看到部门经理A成功地调整到了副总经理B下面,并且部门经理A的hierarchyid值是在层级最大值“/2/1/”的基础上递增获得“/2/2/”。查询结果如表19-34所示。

EXECUTE  dbo.MoveOrg 4, 3;  --执行存储过程

SELECT *,Org_Node.ToString() AS Org_Node_Str

FROM dbo.Employees;

表19-34                        使用存储过程将部门经理A移动到副总经理B下面


Org_Node


EmployeeId


Title


Org_Level


Org_Node_Str


0x


1


总经理


0


/


0x58


2


副总经理A


1


/1/


0x68


3


副总经理B


1


/2/


0x6B40


4


部门经理A


2


/2/2/


0x5B40


5


部门经理B


2


/1/2/


0x6B56


6


员工A


3


/2/2/1/


0x5B56


7


员工B


3


/1/2/1/


0x5B5A


8


员工C


3


/1/2/2/


0x6AC0


9


部门经理C


2


/2/1/


0x5B58B0


10


员工D


3


/1/2/1.1/

19.8.5  hierarchyid的索引策略

用于对分层数据进行索引的策略有两种:深度优先和广度优先。深度优先索引,子树中各行的存储位置相邻,简而言之,就是以hierarchyid值排序的方式存储,如图19-2所示。对于本节的示例而言,就是经理管理的所有雇员都存储在其经理的记录附近。

图19-2  深度优先索引策略

下面的语句基于Org_Node列,创建了深度优先索引。

CREATE UNIQUE INDEXOrgNode_Depth_First

ON dbo.Employees(Org_Node);

下面的查询语句按Org_Node排序输出,注意其中的Org_Node_Str列,这就是深度优先的索引存储方式。如表19-35所示。

SELECT *,Org_Node.ToString() AS Org_Node_Str

FROM dbo.Employees

ORDER BY Org_Node;

表19-35                                               深度优先的索引存储方式


Org_Node


EmployeeId


Title


Org_Level


Org_Node_Str


0x


1


总经理


0


/


0x58


2


副总经理A


1


/1/


0x5B40


5


部门经理B


2


/1/2/


0x5B56


7


员工B


3


/1/2/1/


0x5B58B0


10


员工D


3


/1/2/1.1/


0x5B5A


8


员工C


3


/1/2/2/


0x68


3


副总经理B


1


/2/


0x6AC0


9


部门经理C


2


/2/1/


0x6B40


4


部门经理A


2


/2/2/


0x6B56


6


员工A


3


/2/2/1/

广度优先索引,是将层次结构中每个级别的各行存储在一起,简而言之,就是按层级排序的方式存储,如图19-3所示。对于本节的示例而言,同一经理直属的各雇员的记录存储在相邻位置。

图19-3  广度优先索引策略

下面的语句基于Org_Node列,创建了广度优先索引。

CREATE CLUSTERED INDEXOrgNode_Breadth_First

ONdbo.Employees(Org_Level);

下面的查询语句按Org_Level排序输出,注意其中的Org_Level列,这就是广度优先的索引存储方式。如表19-36所示。

SELECT *,Org_Node.ToString() AS Org_Node_Str

FROM dbo.Employees

ORDER BY Org_Level;

表19-36                                               广度优先的索引存储方式


Org_Node


EmployeeId


Title


Org_Level


Org_Node_Str


0x


1


总经理


0


/


0x58


2


副总经理A


1


/1/


0x68


3


副总经理B


1


/2/


0x6B40


4


部门经理A


2


/2/2/


0x5B40


5


部门经理B


2


/1/2/


0x6AC0


9


部门经理C


2


/2/1/


0x5B58B0


10


员工D


3


/1/2/1.1/


0x6B56


6


员工A


3


/2/2/1/


0x5B5A


8


员工C


3


/1/2/2/


0x5B56


7


员工B


3


/1/2/1/

时间: 2024-11-10 07:09:41

锋利的SQL2014:层次结构操作之Hierarchyid的相关文章

锋利的SQL2014:基于窗口的聚合计算

实际上,窗口聚合与分组聚合在功能上是相同的,唯一的差别是,分组聚合是通过GROUP BY进行分组计算,而窗口聚合是通过OVER子句定义的窗口进行计算.前面我们讲了,这个所谓的窗口,实际上也是一组数据. SQL Server提供的聚合函数包括:AVG.CHECKSUM_AGG.COUNT.COUNT_BIG.GROUPING.GROUPING_ID.MAX.MIN.SUM.STDEV.STDEVP.VAR.VARP.除了GROUPING和GROUPING_ID,都可以跟在OVER子句后面用于窗口的

锋利的SQL2014:联接算法

在Microsoft SQLServer Management Studio中执行查询时,如果选定工具栏中的按钮,可以看到为查询生成的执行计划.执行计划以图形方式显示了SQL Server查询优化器选择的数据检索方法,如表扫描.排序.哈希匹配等.对于联接查询,SQL Server会根据联接表之间的数据.索引等情况,选择使用嵌套循环联接.合并联接或哈希联接. 7.7.1 嵌套循环联接 嵌套循环联接也称为"嵌套迭代",它将一个联接输入用作外部输入表(显示为图形执行计划中的顶端输入),将另一

锋利的JQuery —— DOM操作

图片猛戳链接

锋利的SQL2014:基于窗口的排名计算

从SQL Server2005开始,提供了4个排名函数,分别是:ROW_NUMBER.RANK.DENSE_RANK和NTILE.ROW_NUMBER用于按行进行编号,RANK和DENSE_RANK用于按指定顺序排名,NTILE用于对数据进行分组. 对于排名函数而言,OVER子句中可以包含PARTITION BY和ORDER BY子句,其中,ORDER BY是必选的.因为对于排名而言,没有顺序的排名没有任何意义. 本节我们将使用9.1节创建的Students表为例进行介绍.像Students表这

锋利的SQL2014:基于窗口的分布计算

从SQL Server2012开始,提供了四个排名分布函数,包括PERCENT_RANK.CUME_DIST.PERCENTILE_CONT和PERCENTILE_DISC.其中PERCENT_RANK用于计算某行的相对排名,CUME_DIST用于计算行的累积分布(即相对位置),PERCENTILE_CONT和PERCENTILE_DISC用于根据指定的比例返回组中相应的数值,如中位值等.换句话说,PERCENT_RANK和CUME_DIST是根据数值计算比例,PERCENTILE_CONT和P

锋利的SQL2014:基于窗口的偏移计算

SQL Server 2012引入了四个偏移函数:LAG和LEAD.FIRST_VALUE和LAST_VALUE,用于从当前行的某个偏移量.或是一个窗口框架的开头或结尾的行返回一个元素. LAG和LEAD支持窗口分区和窗口排序子句,FIRST_VALUE和LAST_VALUE在支持窗口分区和窗口排序子句的基础上,还支持窗口框架子句. 9.5.1 LAG和LEAD函数 LAG函数用于在当前行之前查找,LEAD函数在之后查找.函数的第一个参数(必选)指定要返回值的列,第二个参数(可选)是偏移量(如果

新书:锋利的SQL(第2版)开始发售及代码下载

为提高读者学习效率,特提供代码下载:http://download.csdn.net/detail/zhanghongju/8729369 网购京东:http://item.jd.com/11692900.html 网购当当:http://product.dangdang.com/23702533.html 前言 本书第1版是在四年前出版的,但至今仍在被众多的SQL爱好者追捧,甚至不辞辛劳地逐页扫描,上传至网络进行分享.本书第1版是基于SQL Server 2008编写的.时光荏苒,目前SQL

Poi对Excel的基本读写操作

写操作: Java代码 /** * * 层次结构就是workbook-->Sheet-->Row-->Cell * 只要按照这种层次结构操作就不会有什么大的问题 * @author Administrator * */ public class Test1 { public static void main(String args[]) throws IOException { //HSSFWorkbook对应的是2003 //XSSFWorkbook对应的是2007 //对于03和07

计算机编程基础之深入理解计算机系统1

目录 概述——<深入理解计算机系统> 计算机系统漫游 信息的表示和处理 概述——<深入理解计算机系统> Computer Systems A Programmers Perspective  英文名 计算机系统漫游 本章简介 当系统上执行hello程序时,系统发生了什么以及为什么会这样 信息就是位+上下文 源程序(或者源文件) hello.c,实际上是由值0和1组成的位(bit)序列,8个位被组织成一组,成为字节.每个字节表示程序中某个文本字符,大部分的现代系统都使用ASCII标准