以往我们在关系数据库中建立树状结构的时候,通常使用ID+ParentID来实现两条 纪录间的父子关系。但这种方式只能标示其相对位置。解决这类问题在SqlServer2005出现之前通常是采用游标来操作,但熟悉数据库内部机制的人都 知道使用游标带来的性能问题和其他问题是比较严重的。
到了SqlServer2005下,可以选择用CTE来做递归查询,这种方式查询比 较简练,但由于数据库内部是采用递归查询的方式,其效率依旧不高;为了能够实现既简练又高效的查询,通常的做法是增加冗余字段,比如增加一个"Path" 字段,查询时用模糊查询来进行左匹配。对Path建索引后,这种查询的效率还是相当高的,因此这种方式也是一种常规的设计方式;
SQL SERVER 2008引入了新的hierarchyid数据类型,可以用它来做本地存储并且在树层次结构中管理其位置.只用这个函数能简洁地表示层次结构中的位置.该 函数提供的一些内置的函数方法可以操作和遍历层次结构,使得存储和查询分层数据更为容易,而不需要像那样通过CTE递归来获得.
Hierarchyid类型其实是一个CLR自定义数据类型依次打开:数据库->系统数据库->master->可编程性->类型->系统数据类型->CLR数据类型->hierarchyid,可以看到该数据类型.
于hierarchyid有关的一些函数主要有:
- GetAncestor :取得某一个级别的祖先
- GetDescendant :取得某一个级别的子代
- GetLevel :取得级别
- GetRoot :取得根
- IsDescendantOf :判断某个节点是否为某个节点的子代
- Parse :将字符串转换为hierarchyid。该字符串的格式通常都是/1/这样的
- Read :Read 从传入的BinaryReader 读取SqlHierarchyId 的二进制表示形式,并将SqlHierarchyId 对象设置为该值。不能使用Transact-SQL 调用Read。请改为使用CAST 或CONVERT。
- GetReparentedValue :可以用来移动节点(或者子树)
- ToString :将hierarchyid转换为字符串,与parse正好相反
- Write : 将SqlHierarchyId 的二进制表示形式写出到传入的BinaryWriter 中。无法通过使用Transact-SQL 来调用Write。请改为使用CAST 或CONVERT。
hierarchyid 数据类型的值表示树层次结构中的位置。hierarchyid 的值具有以下属性:
-
非常紧凑
在具有 n 个节点的树中,表示一个节点所需的平均位数取决于平均端数(节点的平均子级数)。端数较小时 (0-7),大小约为 6*logAn 位,其中 A 是平均端数。对于平均端数为 6 级、包含 100,000 个人的组织层次结构,一个节点大约占 38 位。存储时,此值向上舍入为 40 位,即 5 字节。
-
按深度优先顺序进行比较
给定两个 hierarchyid 值 a 和 b,a<b 表示在对树进行深度优先遍历时,先找到 a,后找到 b。hierarchyid 数据类型的索引按深度优先顺序排序,在深度优先遍历中相邻的节点的存储位置也相邻。例如,一条记录的子级的存储位置与该记录的存储位置是相邻的。
-
支持任意插入和删除
通过使用 GetDescendant 方法,始终可以在任意给定节点的右侧、左侧或任意两个同级节点之间生成同级节点。在层次结构中插入或删除任意数目的节点时,该比较属性保持不变。大多数插 入和删除操作都保留了紧凑性属性。但是,对于在两个节点之间执行的插入操作,所产生的 hierarchyid 值的表示形式在紧凑性方面将稍微降低。
hierarchyid 数据类型具有以下局限性:
-
类 型为 hierarchyid 的列不会自动表示树。由应用程序来生成和分配 hierarchyid 值,使行与行之间的所需关系反映在这些值中。一些应用程序甚至可能不需要用类型为 hierarchyid 的列来表示树。可能这些值为对其他表中定义的层次结构中位置的引用。
-
由应用程序来管理生成和分配 hierarchyid 值时的并发情况。不能保证列中的 hierarchyid 值是唯一的,除非应用程序使用唯一键约束或应用程序自身通过自己的逻辑来强制实现唯一性。
-
由 hierarchyid 值表示的层次结构关系不是像外键关系那样强制实现的。可能会出现下面这种层次结构关系而且有时这种关系是合理的:A 具有子级 B,然后删除了 A,导致 B 与一条不存在的记录之间存在关系。如果这种行为不可接受,应用程序在删除父级之前必须先查询其是否有后代。
用于对分层数据进行索引的策略有两种:
-
深度优先
深度优先索引,子树中各行的存储位置相邻。例如,一位经理管理的所有雇员都存储在其经理的记录附近。
-
广度优先
广度优先将层次结构中每个级别的各行存储在一起。例如,同一经理直属的各雇员的记录存储在相邻位置。
例如下面的例子是一个职员表,数据有如下关系:
Code
SERT HumanResources.EmployeeDemo (OrgNode, EmployeeID, LoginID, Title, HireDate)
LUES (hierarchyid::GetRoot(), 1,‘adventure-works\scott‘, ‘CEO‘, ‘3/11/05‘) ;
CLARE @Manager hierarchyid
LECT @Manager = hierarchyid::GetRoot() FROM HumanResources.EmployeeDemo;
SERT HumanResources.EmployeeDemo (OrgNode, EmployeeID, LoginID, Title, HireDate)
LUES (@Manager.GetDescendant(NULL,NULL), 2, ‘adventure-works\Mark‘, ‘CTO‘, ‘4/05/07‘)
CLARE @Manager hierarchyid
CLARE @FirstChild hierarchyid
LECT @Manager = hierarchyid::GetRoot() FROM HumanResources.EmployeeDemo;
lect @FirstChild = @Manager.GetDescendant(NULL,NULL)
SERT HumanResources.EmployeeDemo (OrgNode, EmployeeID, LoginID, Title, HireDate)
LUES (@Manager.GetDescendant(@FirstChild,NULL), 3, ‘adventure-works\ravi‘, ‘Director Marketing‘, ‘4/08/07‘)
Insert the First Descendant of a Child Node
CLARE @Manager hierarchyid
LECT @Manager = CAST(‘/1/‘ AS hierarchyid)
SERT HumanResources.EmployeeDemo (OrgNode, EmployeeID, LoginID, Title, HireDate)
LUES (@Manager.GetDescendant(NULL, NULL),45, ‘adventure-works\Ben‘,‘Application Developer‘, ‘6/11/07‘) ;
Insert the Second Descendant of a Child Node
CLARE @Manager hierarchyid
CLARE @FirstChild hierarchyid
LECT @Manager = CAST(‘/1/‘ AS hierarchyid)
LECT @FirstChild = @Manager.GetDescendant(NULL,NULL)
SERT HumanResources.EmployeeDemo (OrgNode, EmployeeID, LoginID, Title, HireDate)
LUES (@Manager.GetDescendant(@FirstChild, NULL),55, ‘adventure-works\Laura‘,‘Trainee Developer‘, ‘6/11/07‘) ;
Insert the first node who is the Descendant of Director Marketing
CLARE @Manager hierarchyid
CLARE @FirstChild hierarchyid
LECT @Manager = CAST(‘/2/‘ AS hierarchyid)
SERT HumanResources.EmployeeDemo (OrgNode, EmployeeID, LoginID, Title, HireDate)
LUES (@Manager.GetDescendant(NULL, NULL),551, ‘adventure-works\frank‘,‘Trainee Sales Exec.‘, ‘12/11/07‘) ;
Insert the second node who is the Descendant of Director Marketing
CLARE @Manager hierarchyid
CLARE @FirstChild hierarchyid
LECT @Manager = CAST(‘/2/‘ AS hierarchyid)
LECT @FirstChild = @Manager.GetDescendant(NULL,NULL)
SERT HumanResources.EmployeeDemo (OrgNode, EmployeeID, LoginID, Title, HireDate)
LUES (@Manager.GetDescendant(@FirstChild, NULL),531, ‘adventure-works\vijay‘,‘Manager Industrial Sales‘, ‘12/09/06‘) ;
Insert the third node who is the Descendant of Director Marketing
in between 2 existing descendants
CLARE @Manager hierarchyid
CLARE @FirstChild hierarchyid
CLARE @SecondChild hierarchyid
LECT @Manager = CAST(‘/2/‘ AS hierarchyid)
LECT @FirstChild = @Manager.GetDescendant(NULL,NULL)
LECT @SecondChild = @Manager.GetDescendant(@FirstChild,NULL)
SERT HumanResources.EmployeeDemo (OrgNode, EmployeeID, LoginID, Title, HireDate)
LUES (@Manager.GetDescendant(@FirstChild, @SecondChild),543, ‘adventure-works\james‘,‘Manager Consumer Sales‘, ‘12/04/06‘) ;
3.
DECLARE @TID hierarchyid
SELECT @TID=OrgNode FROM HumanResources.EmployeeDemo WHERE title=‘cto‘
SELECT *, OrgNode.GetLevel() as 层次,OrgNode.ToString() as 路径 FROM HumanResources.EmployeeDemo WHERE @TID.IsDescendantOf(OrgNode)=1
SELECT *, OrgNode.GetLevel() as 层次,OrgNode.ToString() as 路径 FROM HumanResources.EmployeeDemo WHERE OrgNode.IsDescendantOf(@TID)=1
4.向表里插入记录
SET QUOTED_IDENTIFIER ON
GO
--Use Serializable Transaction
CREATE PROCEDURE [dbo].[AddEmployee](@ManagerID hierarchyid, @EmpID int,
@LogID varchar(100), @JobTitle as varchar(200), @JoiningDate datetime)
AS
BEGIN
DECLARE @LastChild hierarchyid
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
SELECT @LastChild = Max(OrgNode) From HumanResources.EmployeeDemo
WHERE OrgNode = @ManagerID
INSERT HumanResources.EmployeeDemo (OrgNode, EmployeeID, LoginID, Title, HireDate)
VALUES(@LastChild, @EmpID,@LogID , @JobTitle, @JoiningDate)
COMMIT
END ;
5.移动层级关系
CREATE PROCEDURE MoveOrg(@oldMgr nvarchar(256), @newMgr nvarchar(256) )
AS
BEGIN
DECLARE @nold HierarchyID
DECLARE @nnew HierarchyID
SELECT @nold = OrgNode FROM HumanResources.EmployeeDemo WHERE LoginID = @oldMgr ;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
SELECT @nnew = OrgNode FROM HumanResources.EmployeeDemo WHERE LoginID = @newMgr ;
SELECT @nnew = @nnew.GetDescendant(max(OrgNode), NULL)
FROM HumanResources.EmployeeDemo WHERE OrgNode.GetAncestor(1)=@nnew ;
UPDATE HumanResources.EmployeeDemo
SET OrgNode = OrgNode.GetReparentedValue(@nold, @nnew)
WHERE @nold.IsDescendantOf(OrgNode) = 1
COMMIT TRANSACTION
END
6.获取最大的子节点,传递给GetDescendant() 函数获得新的子节点
Create Function GetMyMaxChild(@ManagerID as BigInt) Returns HierarchyID
BEGIN
Declare @ManagerNode HierarchyID
Declare @MaxChild HierarchyID
--Get the ManagerNode
Select @ManagerNode = OrgNode from
HumanResources.EmployeeDemo Where EmployeeID = @ManagerID
--Get the Max Child
Select @MaxChild = Max(OrgNode) from HumanResources.EmployeeDemo
Where OrgNode.GetAncestor(1) = @ManagerNode
--Return the Value
RETURN @MaxChild
END
性能
我们来比较新类型和CTE的性能。为了比较,我们来举个例子,它的需求是重新获取经理‘adventure-works \
james1‘的所有分支。表就用本文提到的表:Employee(传统模型)和Organization(HierarchyID)。
CTE脚本:
WITH
UpperHierarchy(EmployeeId, LastName, Manager, HierarchyOrder)
AS
(
SELECT emp.EmployeeId, emp.LoginId, emp.LoginId, 1 AS HierarchyOrder
FROM
HumanResources.Employee AS emp
WHERE emp.LoginId =‘adventure-works\james1‘
UNION ALL
SELECT
emp.EmployeeId, emp.LoginId, Parent.LastName, HierarchyOrder + 1
FROM HumanResources.Employee AS emp
INNER JOIN UpperHierarchy AS Parent
ON emp.ManagerId =
parent.EmployeeId
)
SELECT
EmployeeId, LastName
From
UpperHierarchy
使用HierarchyID的脚本如下所示。你会注意到有2步:第一步是获取父节点,第二步是找出分支:
Declare
@BossNode As HierarchyId
Select
@BossNode = EmployeeID From dbo.Organization Where EmployeeName =
‘adventure-works\james1‘
Select *
From dbo.Organization
Where
@BossNode.IsDescendant(EmployeeId)= 1
从SSMS里可以看到,执行计划给了我们一些性能方面的信息。
图8-性能基准
我们可以看到CTE占了批处理的63%。这意味着HierarchyID要好50%。
我们可以看到返回父节点(james1)这一步占了查询的大部分(使用了扫描),因为列没有索引。但是,由于方法2里采占用了相同的比率,我们可以忽略这一点。
我们也可以看到CTE的执行计划比HierarchyID类型要复杂得多。这是因为主键允许表的唯一扫描。
如果我们来看这些需求使用的系统资源,那么对于CTE而言,结论是灾难性的。事件探查器跟踪显示了多个执行:
图9-系统资源使用情况
我们可以看到duration列里1/3-2/3的比例。然而IO使用情况却上升到了300。CTE过多使用临时表(Table Spool),这意味着很多的读操作。CPU使用也多用了9倍。
HierarchyID取得了压倒性的胜利。
局限
唯一性
HierarchyId类型天生就不支持唯一性。例如,同一个表里可能有两个根节点。显然,在你的程序里可能会陷入完整性的问题,但是,它也不可能唯一索引树而使整个树变得能聚集。
为了解决这个局限,我们可以在HierarchyId列上添加主键(或唯一索引):
ALTER
TABLE dbo.Organization ADD CONSTRAINT
PK_Organization
PRIMARY KEY
(
EmployeeID
)
HierarchyId上的主键或唯一索引允许表的深度优先索引。
对于前面的数据填充,该DDL将报错。事实上,每个节点的子节点都有相同的索引,这不允许唯一。为了纠正这个问题,我们需要在每一级别上排序子节点来重新组织树。为了实现它,有必要给GetDescendant()函数传递参数。该操作将在稍后进行说明。
外键
与上面描述的传统建模方式相反,外键引用父记录天生也不支持。事实上,HierarchyId类型存储树里节点的路径,而不是父节点。
不过,使用GetAncestor()函数来轻易获取父节点的标识符倒是可行的,如下面的语句:
Select
EmployeeId.GetAncestor(1), EmployeeName
From
dbo.Organization
GetAncestor()返回HierarchyId。如果HierarchyId列是表的主键(如我们的例子),添加一个外键引用到自身就是可行的了。
Alter
Tabledbo.Organization
Add ParentId AS
EmployeeId.GetAncestor(1)PERSISTED
REFERENCES
dbo.Organization(EmployeeId)
现在,我们的表有了和最初模型相同的完整性规则了。
HierarchyID函数
HierarchyID数据类型通过一系列的函数来操作:
·
GetAncestor
·
GetDescendant
·
GetLevel
·
GetRoot
·
ToString
·
IsDescendant
·
Parse
·
Read
·
Reparent
·
Write
在前面的例子里我们看到了前5个函数,接下来的5个在下表作出了解释:
函数 | 描述 |
IsDescendant | 允许知道一条记录是不是层次结构里另一个的子节点 |
Parse | 它和ToString()正好相反,它使从字符串里得到HierarchyID值成为可能 |
Read | 类似Parse,但针对varbinary值 |
Write | 类似ToString,但针对varbinary值 |
Reparent | 允许通过更改父节点来移动层次结构里的节点 |
警告:所有的函数都是区分大小写的。