CREATE TABLE XMLOrg
Orgid int,
Orgdata xml
层次结构数据的索引策略
用于对层次结构数据进行索引的策略有两种:
深度优先索引在子树中相邻位置存储行。 例如,一位经理管理的所有雇员都存储在其经理的记录附近。
在深度优先索引中,一个节点的子树中的所有节点存储在一起。 因此,深度优先索引能高效地响应有关子树的查询,“查找此文件夹及其子文件夹中的所有文件”就是这样的一个示例。
广度优先索引将层次结构中每个级别的各行存储在一起。 例如,同一经理直属的各雇员的记录存储在相邻位置。
在广度优先索引中,一个节点的所有直属子级存储在一起。 因此,广度优先索引在响应有关直属子级的查询方面效率很高,“查找此经理直属的所有雇员”就属于这类查询。
采用深度优先、广度优先还是结合使用这两种索引,以及将哪一种设为聚集键(如果有),取决于上述两种查询类型的相对重要性以及 SELECT 与DML 操作的相对重要性。 有关索引策略的详细示例,请参阅 Tutorial: Using the hierarchyid Data Type。
GetLevel() 方法可用于创建广度优先排序。 在下例中,既创建了广度优先索引,又创建了深度优先索引:
USE AdventureWorks2022; -- wmimof
CREATE TABLE Organization
BusinessEntityID hierarchyid,
OrgLevel as BusinessEntityID.GetLevel(),
EmployeeName nvarchar(50) NOT NULL
CREATE CLUSTERED INDEX Org_Breadth_First
ON Organization(OrgLevel,BusinessEntityID) ;
CREATE UNIQUE INDEX Org_Depth_First
ON Organization(BusinessEntityID) ;
以下示例特意进行了简化,以帮助您入门。 首先创建一个表以保存一些地理数据。
CREATE TABLE SimpleDemo
Level hierarchyid NOT NULL,
Location nvarchar(30) NOT NULL,
LocationType nvarchar(9) NULL
现在插入一些洲、国家/地区、州和城市的数据。
INSERT SimpleDemo
VALUES
('/1/', 'Europe', 'Continent'),
('/2/', 'South America', 'Continent'),
('/1/1/', 'France', 'Country'),
('/1/1/1/', 'Paris', 'City'),
('/1/2/1/', 'Madrid', 'City'),
('/1/2/', 'Spain', 'Country'),
('/3/', 'Antarctica', 'Continent'),
('/2/1/', 'Brazil', 'Country'),
('/2/1/1/', 'Brasilia', 'City'),
('/2/1/2/', 'Bahia', 'State'),
('/2/1/2/1/', 'Salvador', 'City'),
('/3/1/', 'McMurdo Station', 'City');
选择数据,从而添加将级别数据转换为易于理解的文本值的列。 此查询还按 hierarchyid 数据类型对结果排序。
SELECT CAST(Level AS nvarchar(100)) AS [Converted Level], *
FROM SimpleDemo ORDER BY Level;
下面是结果集:
Converted Level Level Location LocationType
/1/ 0x58 Europe Continent
/1/1/ 0x5AC0 France Country
/1/1/1/ 0x5AD6 Paris City
/1/2/ 0x5B40 Spain Country
/1/2/1/ 0x5B56 Madrid City
/2/ 0x68 South America Continent
/2/1/ 0x6AC0 Brazil Country
/2/1/1/ 0x6AD6 Brasilia City
/2/1/2/ 0x6ADA Bahia State
/2/1/2/1/ 0x6ADAB0 Salvador City
/3/ 0x78 Antarctica Continent
/3/1/ 0x7AC0 McMurdo Station City
请注意,层次结构具有有效的结构,即使其内部不一致。 Bahia 是唯一的州。 它作为城市 Brasilia 的对等方出现在层次结构中。 同样,McMurdo Station 没有父级国家/地区。 用户必须确定此类型的层次结构是否适合于其用途。
添加另一行并选择结果。
INSERT SimpleDemo
VALUES ('/1/3/1/', 'Kyoto', 'City'), ('/1/3/1/', 'London', 'City');
SELECT CAST(Level AS nvarchar(100)) AS [Converted Level], * FROM SimpleDemo ORDER BY Level;
这演示更多可能的问题。 Kyoto 可以作为级别 /1/3/1/
插入,即使没有父级别 /1/3/
。 London 和 Kyoto 具有相同的 hierarchyid值。 用户必须再次确定此类型的层次结构是否适合于其用途,并阻止对其用途无效的值。
此外,此表未使用层次结构顶层 '/'
。 该层被省略,因为没有所有州的公共父级。 可以通过添加整个星球来添加一个顶层。
INSERT SimpleDemo
VALUES ('/', 'Earth', 'Planet');
从父/子迁移到 hierarchyid
大多数树都使用父/子结构来表示。 从父/子结构迁移到使用 hierarchyid 的表的最简单方法是,使用临时列或临时表来跟踪各个层次结构级别的节点数。 有关迁移父/子表的示例,请参阅 教程:使用 hierarchyid 数据类型的第 1 课。
使用 hierarchyid 管理树
尽管 hierarchyid 列不一定表示树,但应用程序可以很容易地确保此列表示树。
生成新的值时,请执行下列操作之一:
跟踪父行中的最后一个子级编号。
计算最后一个子级。 若要高效地执行此操作,需要使用广度优先索引。
通过对列创建可能属于聚集键的唯一索引,强制实现唯一性。 若要确保插入的值是唯一的,请执行下列操作之一:
检测唯一键冲突故障并重试。
确定每个新子节点的唯一性,并将其作为可序列化事务的一部分插入。
使用错误检测的示例
在下例中,示例代码计算新子级的 EmployeeId 值,接着检测有无键冲突,然后返回 INS_EMP 标记以便重新计算新行的 EmployeeId 值:
USE AdventureWorks ;
CREATE TABLE Org_T1
EmployeeId hierarchyid PRIMARY KEY,
OrgLevel AS EmployeeId.GetLevel(),
EmployeeName nvarchar(50)
CREATE INDEX Org_BreadthFirst ON Org_T1(OrgLevel, EmployeeId);
CREATE PROCEDURE AddEmp(@mgrid hierarchyid, @EmpName nvarchar(50) )
BEGIN
DECLARE @last_child hierarchyid;
INS_EMP:
SELECT @last_child = MAX(EmployeeId) FROM Org_T1
WHERE EmployeeId.GetAncestor(1) = @mgrid;
INSERT INTO Org_T1 (EmployeeId, EmployeeName)
SELECT @mgrid.GetDescendant(@last_child, NULL), @EmpName;
-- On error, return to INS_EMP to recompute @last_child
IF @@error <> 0 GOTO INS_EMP
END ;
使用可序列化事务的示例
Org_BreadthFirst 索引可确保确定 @last_child 使用范围查找。 除了应用程序可能需要检查的其他错误情况之外,插入后出现重复键冲突表示试图添加具有同一 ID 的多个雇员,因此必须重新计算 @last_child 。 以下代码计算可序列化事务中的新节点值:
CREATE TABLE Org_T2
EmployeeId hierarchyid PRIMARY KEY,
LastChild hierarchyid,
EmployeeName nvarchar(50)
CREATE PROCEDURE AddEmp(@mgrid hierarchyid, @EmpName nvarchar(50))
BEGIN
DECLARE @last_child hierarchyid
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
SELECT @last_child = EmployeeId.GetDescendant(LastChild,NULL)
FROM Org_T2
WHERE EmployeeId = @mgrid
UPDATE Org_T2 SET LastChild = @last_child WHERE EmployeeId = @mgrid
INSERT Org_T2 (EmployeeId, EmployeeName)
VALUES(@last_child, @EmpName)
COMMIT
END ;
下面的代码使用三行数据填充表,并返回结果:
INSERT Org_T2 (EmployeeId, EmployeeName)
VALUES(hierarchyid::GetRoot(), 'David') ;
AddEmp 0x , 'Sariya'
AddEmp 0x58 , 'Mary'
SELECT * FROM Org_T2
下面是结果集。
EmployeeId LastChild EmployeeName
---------- --------- ------------
0x 0x58 David
0x58 0x5AC0 Sariya
0x5AC0 NULL Mary
强制表示树
以上示例说明了应用程序如何确保树得到维护。 若要通过使用约束强制表示树,创建定义各节点父级的计算列时可以为它创建一个反过来约束主键 ID 的外键。
CREATE TABLE Org_T3
EmployeeId hierarchyid PRIMARY KEY,
ParentId AS EmployeeId.GetAncestor(1) PERSISTED
REFERENCES Org_T3(EmployeeId),
LastChild hierarchyid,
EmployeeName nvarchar(50)
当用于维护层次结构树的不可信代码对表拥有直接 DML 访问权限时,将优先采用这种强制关系的方法。 但是,这种方法可能会降低性能,因为必须针对每个 DML 操作检查约束。
使用 CLR 查找祖先
查找级别最低的共同祖先就是一项涉及层次结构中两个节点的常用操作。 可以用 Transact-SQL 或 CLR 编写此操作,因为它们都提供 hierarchyid 类型。 建议用 CLR,因为用它时查找速度会更快。
使用下面的 CLR 代码来列出祖先,并查找级别最低的共同祖先:
using System;
using System.Collections;
using System.Text;
using Microsoft.SqlServer.Server; // SqlFunction Attribute
using Microsoft.SqlServer.Types; // SqlHierarchyId
public partial class HierarchyId_Operations
[SqlFunction(FillRowMethodName = "FillRow_ListAncestors")]
public static IEnumerable ListAncestors(SqlHierarchyId h)
while (!h.IsNull)
yield return (h);
h = h.GetAncestor(1);
public static void FillRow_ListAncestors(
Object obj,
out SqlHierarchyId ancestor
ancestor = (SqlHierarchyId)obj;
public static HierarchyId CommonAncestor(
SqlHierarchyId h1,
HierarchyId h2
while (!h1.IsDescendantOf(h2))
h1 = h1.GetAncestor(1);
return h1;
若要在以下 Transact-SQL 示例中使用 ListAncestor 和 CommonAncestor 方法,请在 SQL Server 中通过执行如下代码生成 DLL 并创建 HierarchyId_Operations 程序集:
CREATE ASSEMBLY HierarchyId_Operations
FROM '<path to DLL>\ListAncestors.dll';
创建节点的祖先列表是一项常用操作,例如可用于显示组织中的职位。 此操作的其中一种实现方式就是使用表值函数和上面定义的 HierarchyId_Operations 类:
使用 Transact-SQL:
CREATE FUNCTION ListAncestors (@node hierarchyid)
RETURNS TABLE (node hierarchyid)
EXTERNAL NAME HierarchyId_Operations.HierarchyId_Operations.ListAncestors
用法示例:
DECLARE @h hierarchyid
SELECT @h = OrgNode
FROM HumanResources.EmployeeDemo
WHERE LoginID = 'adventure-works\janice0' -- /1/1/5/2/
SELECT LoginID, OrgNode.ToString() AS LogicalNode
FROM HumanResources.EmployeeDemo AS ED
JOIN ListAncestors(@h) AS A
ON ED.OrgNode = A.Node
查找级别最低的共同祖先
使用上面定义的 HierarchyId_Operations 类创建以下 Transact-SQL 函数,以便查找涉及层次结构中两个节点的最低级别的共同上级:
CREATE FUNCTION CommonAncestor (@node1 hierarchyid, @node2 hierarchyid)
RETURNS hierarchyid
EXTERNAL NAME HierarchyId_Operations.HierarchyId_Operations.CommonAncestor
用法示例:
DECLARE @h1 hierarchyid, @h2 hierarchyid;
SELECT @h1 = OrgNode
FROM HumanResources.EmployeeDemo
WHERE LoginID = 'adventure-works\jossef0'; -- Node is /1/1/3/
SELECT @h2 = OrgNode
FROM HumanResources.EmployeeDemo
WHERE LoginID = 'adventure-works\janice0'; -- Node is /1/1/5/2/
SELECT OrgNode.ToString() AS LogicalNode, LoginID
FROM HumanResources.EmployeeDemo
WHERE OrgNode = dbo.CommonAncestor(@h1, @h2) ;
最终得到的节点为 /1/1/
另一项常用操作是移动子树。 下面的过程采用 @oldMgr 的子树作为参数,使其(包括 @oldMgr)成为 @newMgr的子树。
CREATE PROCEDURE MoveOrg(@oldMgr nvarchar(256), @newMgr nvarchar(256) )
BEGIN
DECLARE @nold hierarchyid, @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 OrgNode.IsDescendantOf(@nold) = 1 ;
COMMIT TRANSACTION;
END ;
hierarchyid 数据类型方法引用
教程:使用 hierarchyid 数据类型
hierarchyid (Transact-SQL)