1. 程式人生 > 其它 >老外的一種避免遞迴查詢所有子部門的樹資料表設計與實現!

老外的一種避免遞迴查詢所有子部門的樹資料表設計與實現!

 


最近我閱讀了一篇老外的文章,裡面介紹了一種通過巧妙的設計,實現了高效的部門樹查詢設計。避免遞迴等低效查詢。今天分享推薦給大家!

通常樹形結構的儲存,是在子節點上儲存父節點的編號來確定各節點的父子關係,例如這樣的組織結構:

遞迴遍歷部門樹

與之對應的表資料(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 加了以下需求:

  1. 查出指定部門下所有子孫部門。
  2. 查詢子孫部門總數。
  3. 判斷節點是否葉子節點。

基於 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欄位,新增lftrgt,來儲存左右值。

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

資料和結構準備完畢,我們來試試操作解決上面的需求~

查出所有子孫部門

根據當前表結構的規律,可以發現,要想查出所有子孫部門,只要查左值在 被查尋部門左\右數之間的節點,查出來都是他的子節點。例如:查詢行政總監的所有子部門,行政總監的左右數是918,因此只需要用918lft欄位的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;

完結

目前就這些,歡迎指正、交流、評論。。。我感覺這個部門設計很經典,非常值得收藏,以便用時只需!