以往咱們在關係數據庫中創建樹狀結構的時候,一般使用ID+ParentID來實現兩條 紀錄間的父子關係。但這種方式只能標示其相對位置。解決這類問題在SqlServer2005出現以前一般是採用遊標來操做,但熟悉數據庫內部機制的人都 知道使用遊標帶來的性能問題和其餘問題是比較嚴重的。node
到了SqlServer2005下,能夠選擇用CTE來作遞歸查詢,這種方式查詢比 較簡練,但因爲數據庫內部是採用遞歸查詢的方式,其效率依舊不高;爲了可以實現既簡練又高效的查詢,一般的作法是增長冗餘字段,好比增長一個"Path" 字段,查詢時用模糊查詢來進行左匹配。對Path建索引後,這種查詢的效率仍是至關高的,所以這種方式也是一種常規的設計方式;web
SQL SERVER 2008引入了新的hierarchyid數據類型,能夠用它來作本地存儲而且在樹層次結構中管理其位置.只用這個函數能簡潔地表示層次結構中的位置.該 函數提供的一些內置的函數方法能夠操做和遍歷層次結構,使得存儲和查詢分層數據更爲容易,而不須要像那樣經過CTE遞歸來得到.數據庫
Hierarchyid類型實際上是一個CLR自定義數據類型依次打開:數據庫->系統數據庫->master->可編程性->類型->系統數據類型->CLR數據類型->hierarchyid,能夠看到該數據類型.編程
於hierarchyid有關的一些函數主要有:併發
hierarchyid 數據類型的值表示樹層次結構中的位置。hierarchyid 的值具備如下屬性:app
很是緊湊函數
在具備 n 個節點的樹中,表示一個節點所需的平均位數取決於平均端數(節點的平均子級數)。端數較小時 (0-7),大小約爲 6*logAn 位,其中 A 是平均端數。對於平均端數爲 6 級、包含 100,000 我的的組織層次結構,一個節點大約佔 38 位。存儲時,此值向上舍入爲 40 位,即 5 字節。性能
按深度優先順序進行比較this
給定兩個 hierarchyid 值 a 和 b,a<b 表示在對樹進行深度優先遍歷時,先找到 a,後找到 b。hierarchyid 數據類型的索引按深度優先順序排序,在深度優先遍歷中相鄰的節點的存儲位置也相鄰。例如,一條記錄的子級的存儲位置與該記錄的存儲位置是相鄰的。spa
支持任意插入和刪除
經過使用 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
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 ;
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
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 | 容許經過更改父節點來移動層次結構裏的節點 |
警告:全部的函數都是區分大小寫的。