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

http://blog.csdn.net/tonyzhou2008/article/details/5100683函數

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

在上一節中.net

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

雖然經過主鍵和外鍵的相互搭配能夠知足咱們的查詢、存儲需求,可是這種方式並不易於管理和維護,幸運的是,在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的NodeIdget

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

相關文章
相關標籤/搜索