[Sql Server2008]樹結構的遞歸算法

本文主要講述三個內容:
1.如何創建hierarychyid的表,插入數據及基本遞歸查詢。
2.介紹hierarchyid的10種專有函數。
3.介紹hierarchyid特有的深度優先索引(Depth-First Indexing)和廣度優先索引(Breadth-First Indexing)

在上一節中

http://blog.csdn.net/tjvictor/archive/2009/07/30/4395677.aspx
我們已經演示瞭如何在SQL Server中通過主鍵和外鍵來存儲如下圖所示的樹型結構數據
 

雖然通過主鍵和外鍵的相互搭配可以滿足我們的查詢、存儲需求,但是這種方式並不易於管理和維護,幸運的是,在SQL Server 2008中提供了一種新的數據類型hierarchyid和相關的操作方法來存儲和查詢這種樹型層次關係數據。

首先創建數據表:
create database TestDb
go
use TestDb
go
Create table EmployeeTreeTable
(
NodeId        hierarchyid PRIMARY KEY,
NodeLevel     AS NodeId.GetLevel(),
EmployeeId    int UNIQUE NOT NULL,
EmployeeName  nvarchar(32) NOT NULL,
)
NodeId是記錄樹型層次的Id,是hierarchyid類型。NodeLevel是個計算列,用於存儲當前樹是深度值,根節點爲0。關於NodeId.GetLevel()方法將在下面章節中詳細介紹。

按照上圖所示的層次關係爲表插入數據:
--插入數據
declare @DepthNode hierarchyid;--深度Id
declare @BreadthNode hierarchyid;--廣度Id
--插入根節點
insert into EmployeeTreeTable values(hierarchyid::GetRoot(),1,'項目經理')
--計算深度並插入子節點2
select @DepthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 1;
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(null,null),2,'技術經理');
--計算節點2廣度,在節點2右邊插入節點3
select @BreadthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 2;
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(@BreadthNode,null),3,'產品經理');
--計算節點3廣度,在節點3右邊插入節點4
select @BreadthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 3;
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(@BreadthNode,null),4,'測試經理');
--計算節點2深度並插入子節點5
select @DepthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 2;
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(null,null),5,'技術組長1');
--計算節點5廣度,在節點5右邊插入節點6
select @BreadthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 5;
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(@BreadthNode,null),6,'技術組長2');
--計算節點4深度並插入子節點7
select @DepthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 4;
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(null,null),7,'測試員工1');
--計算節點5深度並插入子節點8
select @DepthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 5;
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(null,null),8,'技術員工1');
--計算節點8廣度,在節點8右邊插入節點9
select @BreadthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 8;
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(@BreadthNode,null),9,'技術員工2');
--計算節點9廣度,在節點9右邊插入節點10
select @BreadthNode = NodeId from EmployeeTreeTable where [EmployeeId] = 9;
insert into EmployeeTreeTable values(@DepthNode.GetDescendant(@BreadthNode,null),10,'技術員工3');
go
select * from EmployeeTreeTable
結果集爲:
NodeId    NodeLevel    EmployeeId   EmployeeName
0x           0                   1                    項目經理
0x58       1                   2                    技術經理
0x5AC0   2                   5                    技術組長1
0x5AD6   3                   8                    技術員工1
0x5ADA   3                   9                    技術員工2
0x5ADE   3                   10                  技術員工3
0x5B40   2                   6                    技術組長2
0x68       1                   3                    產品經理
0x78       1                   4                    測試經理
0x7AC0   2                   7                    測試員工1

1.查詢技術組長1所有子節點的員工信息
select * from EmployeeTreeTable
    where NodeId.IsDescendantOf(0x5AC0)=1--0x5AC0是技術組長1的NodeId

2.查詢技術組長1所有父節點的員工信息
with c as
(
    select * from EmployeeTreeTable where EmployeeId = 5
    union all
    select a.* from EmployeeTreeTable as a
    join c on a.NodeId = c.NodeId.GetAncestor(1)
)
select * from c

上面的例子中,使用了很多hierarchyid專有的函數,可能大家還不熟悉,下面我將具體介紹一下hierarchyid的10個函數,分別爲:
GetRoot,GetLevel,GetAncestor,GetDescendant,IsDescendantOf,ToString,Parse,GetReparentedValue,Read,Write。
1.GetRoot。返回層次結構樹的根節點。注意GetRoot() 是靜態方法。
關於SQL中靜態方法和實例方法的區別請參見:http://blog.csdn.net/tjvictor/archive/2009/07/29/4390673.aspx
SQL:select * from EmployeeTreeTable where NodeId = hierarchyid::GetRoot()
結果集:
NodeId    NodeLevel    EmployeeId    EmployeeName
0x            0                  1                     項目經理

2.返回一個表示節點在樹中的深度的整數。
前面建表時我們已經使用了這個函數,NodeLevel字段就是用這個函數自動創建的。
SQL:select EmployeeName,NodeId.GetLevel() as TreeLevel from EmployeeTreeTable
結果集爲:
EmployeeName    TreeLevel
項目經理                0
技術經理                1
技術組長1              2
技術員工1              3
技術員工2              3
技術員工3              3
技術組長2              2
產品經理                1
測試經理                1
測試員工1              2

3.GetAncestor返回表示本節點爲的第 n 個父節點的 hierarchyid。
SQL:
declare @NodeId hierarchyid
select @NodeId=NodeId from EmployeeTreeTable where EmployeeId = 5
select EmployeeName,NodeLevel from EmployeeTreeTable where NodeId = @NodeId.GetAncestor(0)
select EmployeeName,NodeLevel from EmployeeTreeTable where NodeId = @NodeId.GetAncestor(1)
select EmployeeName,NodeLevel from EmployeeTreeTable where NodeId = @NodeId.GetAncestor(2)
結果集爲:
EmployeeName    NodeLevel
技術組長1              2
技術經理                1
項目經理                0
@NodeId.GetAncestor(0) 取自己節點的Id,@NodeId.GetAncestor(1)取父節點的Id,@NodeId.GetAncestor(2)取爺節點的Id,以此類推。

4.GetDescendant返回父級的一個子節點

如果父級爲 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,則引發異常。
我們在插入的SQL語句中已經使用過了這個方法,這裏就不再給出SQL示例,請大家參考前面的插入SQL語句。
5.IsDescendantOf如果子節點爲本節點的後代,則返回 true
SQL:select * from EmployeeTreeTable where NodeId.IsDescendantOf(0x58)=1
結果集爲:
NodeId    NodeLevel    EmployeeId    EmployeeName
0x58        1                  2                  技術經理
0x5AC0    2                  5                  技術組長1
0x5AD6    3                  8                  技術員工1
0x5ADA    3                  9                  技術員工2
0x5ADE    3                  10                技術員工3
0x5B40    2                  6                  技術組長2

6.ToString返回具有本節點邏輯表示形式的字符串
SQL:select *,NodeId.ToString() as Path from EmployeeTreeTable
結果集爲:
NodeId    NodeLevel    EmployeeId    EmployeeName    Path
0x            0                  1                     項目經理                /
0x58        1                  2                    技術經理               /1/
0x5AC0    2                  5                     技術組長1             /1/1/
0x5AD6    3                  8                     技術員工1             /1/1/1/
0x5ADA    3                 9                     技術員工2             /1/1/2/
0x5ADE    3                  10                   技術員工3             /1/1/3/
0x5B40    2                 6                     技術組長2             /1/2/
0x68        1                 3                     產品經理               /2/
0x78        1                 4                     測試經理               /3/
0x7AC0    2                 7                    測試員工1             /3/1/

7.Parse將hierarchyid 的規範字符串表示形式轉換爲hierarchyid值。即與ToString()函數是相反函數。Parse是靜態函數。
SQL:
declare @Path varchar(32) = '/1/2/5/6/'
select hierarchyid::Parse(@Path)
結果集爲:0x5B6394

8.GetReparentedValue把當前節點從舊路徑更新到新路徑
下面的SQL是把技術員工3,從技術組長1節點更新到技術組長2下面。
SQL:
declare @OldNode hierarchyid=0x5AC0;
declare @NewNode hierarchyid=0x5B40;
update EmployeeTreeTable set NodeId = NodeId.GetReparentedValue(@OldNode,@NewNode)
    where EmployeeId = 10
結果集中技術員工3的路徑從/1/1/3/變成了/1/2/3/。
關於GetReparentedValue的用法比較複雜,我在介紹索引後,會更加詳細的說明各種替換情況。

9.Read和Write
Read和Write是供CLS調用的,不能在T-SQL中直接使用。所以這裏就不具體介紹兩個函數的使用方法了。

hierarchyid有深度優先索引和廣度優先索引
當遞歸查詢父子節點時,會利用到深度優先索引;當平行查詢兄弟節點時,會利用到廣度優先索引。
深度優先索引圖:
 
廣度優先索引圖:
 


1.建立深度優先索引:
深度優先索引是hierarchyid默認的索引,只要在hierarchyid列上建立主鍵,那麼就會自動建立hierarchyid索引。

2.建立廣度優先索引
廣度優先索引必須是個唯一索引且包括NodeLevel和NodeId兩列:
CREATE UNIQUE INDEX IX_EmployeeBreadth ON Employee(NodeLevel, NodeId)

需要注意的是採用深度優先、廣度優先還是結合使用這兩種索引,以及將哪一種設爲聚集鍵(如果有),取決於上述兩種查詢類型的相對重要性以及 SELECT 與 DML 操作的相對重要性,本文不代表一定要如此建立hierarchyid索引。

最後我們討論一下hierarchyid的GetReparentedValue幾種使用方法。
下面我們先看一個有問題的節點更新:把技術組長1從技術經理更新到產品經理。
SQL:
declare @OldNode hierarchyid=0x58;
declare @NewNode hierarchyid=0x68;
update EmployeeTreeTable set NodeId = NodeId.GetReparentedValue(@OldNode,@NewNode)
    where EmployeeId = 5
go
select NodeId.ToString(),* from EmployeeTreeTable
結果集爲:
路徑               NodeId      NodeLevel    EmployeeId    EmployeeName
/                    0x             0                    1                    項目經理
/1/                 0x58         1                    2                    技術經理
/1/1/1/           0x5AD6    3                    8                    技術員工1
/1/1/2/           0x5ADA    3                    9                    技術員工2
/1/1/3/           0x5ADE    3                    10                  技術員工3
/1/2/              0x5B40     2                    6                    技術組長2
/2/                 0x68         1                    3                    產品經理
/2/1/              0x6AC0     2                    5                    技術組長1
/3/                 0x78         1                    4                    測試經理
/3/1/              0x7AC0     2                    7                    測試員工1
從結果裏面可以看到技術組長已經變成了/2/1,成功更新到產品經理節點下。但是技術組長1下面的子節點技術員工1,2,3卻沒有相應的更新過來,還是原來的/1/1/1,2,3,但是原先的技術組長1的/1/1節點已經沒有了,所以出現了所謂的“斷層”現象。
下面提出幾種常用更新需求,並且給出相應的SQL實現語句。

1.職位變更。例如技術經理與產品經理職位互換。
針對這種情況,有兩種方法。一是把技術經理下面的所有節點Id都更新成產品經理節點下。這種情況變動比較大,不推薦使用。第二種方法是把技術經理的NodeId和產品經理的NodeId互換。下面使用第二種方法:
declare @TechNode hierarchyid=0x58;
declare @ProductNode hierarchyid=0x68;
declare @TempNode hierarchyid=0x59;
update EmployeeTreeTable set NodeId = @TempNode where NodeId = @TechNode;
update EmployeeTreeTable set NodeId = @TechNode where NodeId = @ProductNode;
update EmployeeTreeTable set NodeId = @ProductNode where NodeId = @TempNode;

2.職位升降級。例如技術組長2降級成爲技術員工,被掛在技術組長1節點下:
declare @TechTeamLeadNode1 hierarchyid=0x5AC0;
declare @TechEmployeeNode3 hierarchyid=0x5ADE;
update EmployeeTreeTable set NodeId = @TechTeamLeadNode1.GetDescendant(@TechEmployeeNode3,null)
    where EmployeeId = 6
部分結果集爲:
Path        NodeId    NodeLevel    EmployeeId    EmployeeName
/1/1/4/    0x5AE1    3                  6                     技術組長2
可見,技術組長2從/1/2變成了/1/1/4

總結:
SQL Server 2008提供的hierarchyid類型使我們能夠靈活、方便的操作樹型結構。關於hierarchyid還有很多深入的知識,很多靈活的用法,本文不可能一一涉及,這裏僅是介紹一些基本用法,拋磚引玉,如果大家在以後的使用中發現什麼問題或是更好的解決方案,請聯繫我。

 

本文來自CSDN博客:http://blog.csdn.net/tjvictor/archive/2009/07/30/4395681.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章