老外的一種避免遞迴查詢所有子部門的樹資料表設計與實現!
最近我閱讀了一篇老外的文章,裡面介紹了一種通過巧妙的設計,實現了高效的部門樹查詢設計。避免遞迴等低效查詢。今天分享推薦給大家!
通常樹形結構的儲存,是在子節點上儲存父節點的編號來確定各節點的父子關係,例如這樣的組織結構:
與之對應的表資料(department):
id | name | parent_id | level |
---|---|---|---|
1 | 董事長 | 0 | 1 |
2 | 總經理 | 1 | 2 |
3 | 產品部 | 2 | 3 |
4 | 研發部 | 3 | 4 |
5 | 設計部 | 3 | 4 |
6 | 行政總監 | 2 | 3 |
7 | 財核部 | 6 | 4 |
8 | 會計 | 7 | 5 |
9 | 出納 | 7 | 5 |
10 | 行政部 | 6 | 4 |
部門表結構(department)
id 部門編號
name 部門名稱
level 所在樹層級
parent_id 上級部門編號
問題來了
這樣的方式很不錯,可以很直觀的體現各個節點之間的關係,通常可以滿足大多數需求。但是當業務需求變得多了,資料量龐大了,這樣的方式就不再適合用於生產。
例如:PM 加了以下需求:
- 查出指定部門下所有子孫部門。
- 查詢子孫部門總數。
- 判斷節點是否葉子節點。
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能。
專案地址:https://github.com/YunaiV/ruoyi-vue-pro
查出所有子孫部門
使用指定部門編號,一層一層使用遞迴
往下查,可能是多數人會想到的方法。儘管在 MySQL8.0 支援了 cte(公共表表達式),遞迴效率比傳統遞迴方式有明顯提升,但是查詢效率仍會隨著部門樹層級深度的提高而變差。
另外一種方法,一次性查出所有資料,放入記憶體中處理(資料量少時,可以選用。資料量多,不怕捱打的人也可以選這種)~
基於微服務的思想,構建在 B2C 電商場景下的專案實戰。核心技術棧,是 Spring Boot + Dubbo 。未來,會重構成 Spring Cloud Alibaba 。
專案地址:https://github.com/YunaiV/onemall
查詢子孫部門總數
遞迴查詢每一層的數量,最後相加。
判斷是否葉子節點
- 方法1:可以加欄位
isLeaf
的方式,來表示這個節點是否是葉子節點。 - 方法2:直接通過查詢
parent_id
=當前id
的 count 是否大於 0,大於 0 表示不是葉子節點,等於 0 表示為葉子節點。
在日常中,可能會經常使用上述類似方法去解決類似的問題,但我覺得這樣的方法在效率上不是最優解。於是乎開始查詢更好的方案去解決這些問題。
要不試試這個方法?
直到後面查到國外一部落格中,見到了所謂的《改進後的先序樹遍歷》(https://www.sitepoint.com/hierarchical-data-database-2
)文章(天哪,竟然是一篇 2003 年發表的文章)~
他具體是怎麼做的呢?
還是回到剛剛的組織架構。
我們從根節點開始,給董事長
左值設為1
,下級部門總經理
左值設為2
,以此類推地沿著邊緣
開始遍歷,給每個節點加上左值
,遇到葉子節點處給節點加上右值
,再繼續向上沿著邊緣繼續遍歷,遍歷結束回到根節點右側
,你將得到類似這樣的結構。
遍歷完後每一個節點都有與之對應的左右值
。這個時候可以去除parent_id
欄位,新增lft
,rgt
,來儲存左右值。
id | name | lft | rgt | level |
---|---|---|---|---|
1 | 董事長 | 1 | 20 | 1 |
2 | 總經理 | 2 | 19 | 2 |
3 | 產品部 | 3 | 8 | 3 |
4 | 設計部 | 4 | 5 | 4 |
4 | 研發部 | 6 | 7 | 4 |
6 | 行政總監 | 9 | 18 | 3 |
7 | 財核部 | 10 | 15 | 4 |
8 | 出納 | 11 | 12 | 5 |
9 | 會計 | 13 | 14 | 5 |
10 | 行政部 | 16 | 17 | 4 |
資料和結構準備完畢,我們來試試操作解決上面的需求~
查出所有子孫部門
根據當前表結構的規律,可以發現,要想查出所有子孫部門,只要查左值在 被查尋部門
的左\右數之間
的節點,查出來都是他的子節點。例如:查詢行政總監
的所有子部門,行政總監
的左右數是9
和18
,因此只需要用9
和18
做lft
欄位的between
查詢,查詢出的結果就是【被查部門本身資料和所有子孫部門】;
SET @lft := 9;
SET @rgt := 18;
SELECT * FROM department WHERE lft BETWEEN @lft AND @rgt ORDER BY lft ASC;
/*例子中用BETWEEN將被查部門本身也查了出來。實際中可以用大於小於*/
完美~
查詢子孫部門總數
到這裡可能會說,需求 1 都解決了,查總數自然也就解決了,直接上select count
就可以了,確實沒有錯,但是沒有那個必要,因為有個簡單公式可以直接計算
公式:總數 = (右值 - 左值 - 1) / 2
例如:
行政總監的子孫部門數 = (18 - 9 - 1) / 2 = 4
董事長的子孫部門數 = (20 - 1 - 1) / 2 = 9
會計的子部門數 = (14 - 13 - 1) / 2 = 0
可以數數看,確實沒錯哦~
判斷是否葉子節點
通過有了上述計算公式算總數的經驗後,現在判斷是否葉子節點,有的小夥伴已經知道了怎麼做,那就是:
右值 - 1 == 左值
那他就是葉子節點,或者左值 + 1 == 右值
那他就是葉子節點,反之則不是葉子節點。
例如:
設計部
,5 - 1 == 4
,因此他是葉子節點。
董事長
,20 - 1 != 1
,因此他不是葉子節點。
至此已經完美的解決了上述需求問題,接下來再嘗試一下業務的基本操作。
其他基本操作
新增部門
當新增一個部門時,需要對新增節點位置的後續邊緣
進行加2
操作,因為每一個節點有左右兩個數值。這個操作通常需要放到事務
中進行處理。
新增或刪除節點時右節點判斷條件應該是 lft >= 當前節點的 lft。
例如:在研發部門下新增一個新部門
:
對應 sql:
SET @lft := 7;/*新部門的左值*/
SET @rgt := 8;/*新部門的左值*/
SET @level := 5;/*新部門的層級*/
begin;
/*將插入的後續邊緣的節點左右數+2*/
UPDATE department SET lft=lft+2 WHERE lft > @lft;
UPDATE department SET rgt=rgt+2 WHERE rgt >= @lft;
/*插入資料*/
INSERT INTO department(name,lft,rgt,level) VALUES('新部門',@lft,@rgt,level);
/*新增影響行數為0時,必須回滾*/
commit;
/*rollback;*/
刪除部門
刪除部門與新增部門類似,不同的是需要對刪除節點的後續邊緣
節點減2
操作。例如:刪除剛剛新增的新部門:
對應 sql:
SET @lft := 7;/*要刪除的節點左值*/
SET @rgt := 8;/*要刪除的節點右值*/
begin;
UPDATE department SET lft=lft-2 WHERE lft > @lft;
UPDATE department SET rgt=rgt-2 WHERE rgt > @lft;
/*刪除節點*/
DELETE FROM department WHERE lft=@lft AND rgt=@rgt;
/*刪除影響行數為0時,必須回滾*/
commit;
/*rollback*/
查詢直接子部門
查詢某部門的直接子部門(即不包含孫子部門),例如:查詢總經理
下的直接子部門。正常需要返回產品部
和行政總監
。
對應的 sql:
SET @level := 2;/*總經理的level*/
SET @lft := 2;/*總經理的左值*/
SET @rgt := 19;/*總經理的右值*/
SELECT * FROM department WHERE lft > @lft AND rgt < @rgt AND level = @level+1;
查詢祖鏈路徑
查詢某部門的祖鏈路徑。例如:查詢產品部
的祖鏈路徑,正常需要返回董事長
,總經理
SET @lft := 3;/*產品部左值*/
SET @rgt := 8;/*產品部右值*/
SELECT * FROM department WHERE lft < @lft AND rgt > @rgt ORDER BY lft ASC;
完結
目前就這些,歡迎指正、交流、評論。。。我感覺這個部門設計很經典,非常值得收藏,以便用時只需!