對於組織架構中的員工層次關系我們應該怎么建模呢?
如下圖所示:

此類結構通常有兩個主要特點:
1、一個孩子有且只有一個父親
2、樹的深度不確定
為了解決這種結構,我們一般會建一張下面的表:
方案一(Adjacency List)
CREATE TABLE Employees(
employee_id int,
employee_name varchar2(100),
parent_id int
);
每個員工在Employees表中會有一條記錄,並通過parent_id來記錄其直屬領導的employee_id,這樣做很簡單明了,但是卻存在一些弊端。
考慮如下問題:
1、如何得到某個員工的直屬領導?
2、如何得到某個領導的直屬下屬?
3、如何得到某個領導全部下屬(下屬的下屬)?
問題1、2都很簡單,一次自連接就解決了:
1、
- select par.employee_id,par.employee_name
- from employees par,employees self
- where self.parent_id=par.employee_id
- and self.employee_id=3201
2、
- select child.employee_id,child.employee_name
- from employees child,employees self
- where child.parent_id=self.employee_id
- and self.employee_id=1010
但問題3呢?
兩種人會有兩種做法,一種覺得可以在程序里做,把問題2的SQL循環執行最終把結果拼起來就OK了;
一種是覺得我可以使用多次自連接,比如我知道這下領導最多有兩級下屬,我就可以這樣做:
select child.employee_id,child.employee_name,child1.employee_id,child1.employee_name
from employees self inner join employees child on child.parent_id=self.employee_id
left join employees child1 on child1.parent_id=child.employee_id and
where self.employee_id=1010
上面兩種方法看似都可以解決問題,但是別忘了此類樹結構的一個很重要的特點,那就是深度的不確定性(就算確定,如果層次很深,20級),
性能及可擴展性將是一個很大的問題。
那怎么辦呢?一時間好像看起來別無他法啊。
好消息是使用Oracle 10g及以上或者SQL Server 2005及以上的朋友可以直接使用數據庫特有的SQL特性來解決這個問題了。
例如在Oracle中可以使用層次查詢
- select EMPLOYEE_ID, employee_name
- from employees
- start with employee_id = 1
- connect by prior employee_id = parent_id
那使用MySQL或者其不支持層次查詢的數據庫怎么辦呢?難道只能用前面兩種笨方法?
答案是否定的,你需要重新設計你的表模型。
How to design?
方案二(Path Enumeration)
CREATE TABLE Employees_Path(
employee_id int,
employee_name varchar2(100),
path varchar2(1000)
);
此種方案借助了unix文件目錄的思想,如下圖所示:

我們需要做的就是正確的維護這個PATH值,現在如果我們要查詢任意領導(比如Michele)的所有下屬就只需要這樣即可:
- select * from Employees_Path where path like '/1/_%'
同樣的,如果我們需要查詢任意員工(比如Chris)的所有領導也只需要這樣即可:
- select * from Employees_Path where '/1/2/5/' like path||'%' and path<>'/1/2/5/'
缺點:
1、PATH值由程序來維護,無法在數據庫一級確保數據的有效性
2、當樹的層級太深有可能會超過PATH字段的長度,所以其能支持的最大深度並非無限的。
方案三(Nested Sets)
CREATE TABLE EMPLOYEES_NESTEDSETS(
EMPLOYEE_ID INT,
EMPLOYEE_NAME VARCHAR2(100),
NSLEFT INT,
NSRIGHT INT
);
該方案采用深度優先遍歷給樹中的每個節點分配兩個值,分別存在NSLEFT和NSRIGHT中。如下圖所示

每個節點左邊的的值存放在NSLEFT中,右邊的值存放在NSRIGHT中;節點左邊的值比該節點的所有子孫節點值都要小,節點右邊的值比該節點的所有子孫節點值都要大。
例如Hell Mayes左邊的值為2,其比Hell Mayes的所有子孫節點的值都要小(3,4,5,10,6,7,8,9)
Hell Mayes右邊的值為11,其比Hell Mayes的所有子孫節點的值都要大(3,4,5,10,6,7,8,9)
有了這個規則之后,如果想要查找某個節點的子孫或都祖先就非常容易了。
回到我們前面的題目中來,假設我要查找Helen Mayes的所有下屬員工,我們可以這樣:
- select *
- from EMPLOYEES_NESTEDSETS par,
- EMPLOYEES_NESTEDSETS child
- where child.nsleft > par.nsleft
- and child.nsleft < par.nsright
- and par.EMPLOYEE_NAME = 'Helen Mayes'
那如果我們要查找Helen Mayes的所有領導呢?
- select *
- from EMPLOYEES_NESTEDSETS par,
- EMPLOYEES_NESTEDSETS child
- where child.nsleft > par.nsleft
- and child.nsleft < par.nsright
- and child.EMPLOYEE_NAME = 'Angela Richards'
Nested Sets這種方案還有一個優點就是,當你刪除了一個非葉子節點的時候,該節點的所有子孫節點會自動成為該節點父節點的子孫,並同樣滿足前面所說的條件。
缺點:
在Adjacency List方案中很好回答的問題,在Nested Sets中卻變得困難起來
比如我想要查找任意領導(比如Helen Mayes)的直屬下屬,在Nested Sets中你需要這樣做
- select *
- from EMPLOYEES_NESTEDSETS par
- inner join EMPLOYEES_NESTEDSETS child
- on child.nsleft > par.nsleft
- and child.nsleft < par.nsright
- left join EMPLOYEES_NESTEDSETS tmp
- on child.nsleft > tmp.nsleft
- and child.nsleft < tmp.nsright
- and tmp.nsleft > par.nsleft
- and tmp.nsleft < par.nsright
- where par.EMPLOYEE_NAME = 'Helen Mayes'
- and tmp.employee_id is null
怎么樣,夠復雜吧? 其邏輯就是 首先找到Helen Mayes的所有下屬,然后在去查找這些下屬沒有屬於Helen Mayes下屬的上級.........WTF...........
另外,移動和新增加節點也比較復雜
比如我們要在Helen Mayes和Chris Jones之間插入一名員工Scott,如下圖所示

從上圖基本上就可以看出來我們要更改的地方了......需要重新生成比新插入節點的nsleft值大的所有節點的nsleft和nsright值
非常復雜......
方案四(Closure Table)
CREATE TABLE Employees(
employee_id int,
employee_name varchar2(100)
);
CREATE TABLE TreePaths (
ancestor_key int,
member_key int,
Distance int,
is_leaf int
);
Closure Table將樹中每個節點與其子孫節點的關系都存儲了下來,如下圖所示:

注意:每個節點都有一條到其本身的記錄,如上表的第一條、第四條、第六條記錄
Distance是祖先節點到其本身之間的深度,ancestor_key是祖先節點的key,member_key是該節點的成員節點的key,IS_LEAF用於標識該節點是否為葉子節點
有了這張關系表之后,下面讓我們用具體例子來感受其帶來的好處
查找某個領導(Helen Mayes)的所有下屬員工信息
select * from Employees a, TreePaths b where a.employee_id = b.ancestor_key and a.employee_name = 'Helen Mayes' and b.distance<>0
查找某個領導(Helen Mayes)的所有直屬員工信息
select * from Employees a, TreePaths b where a.employee_id = b.ancestor_key and a.employee_name = 'Helen Mayes' and b.distance=1
查找某個員工的所有領導
select * from Employees a, TreePaths b where a.employee_id = b.member_key and a.employee_name = 'Helen Mayes' and b.distance<>0
結語
