如何使用mysql存儲樹形關系


最近遇到業務的一個類似文件系統的存儲需求,對於如何在mysql中存儲一顆樹進行了一些討論,分享一下,看看有沒有更優的解決方案。

 

一、現有情況

首先,先假設有這么一顆樹,一共9個節點,1是root節點,一共深3層。(當然實際業務不會這么簡單)

原有的表結構如下:

id parents_id name full_path
1 0 a /a
2 1 b /a/b
3 1 c /a/c
4 1 d /a/d
5 4 e /a/d/e
6 4 f /a/d/f
7 5 g /a/d/e/g
8 5 h /a/d/e/h
9 5 i /a/d/e/i

 

 

 

 

 

 

 

 

 

 

 

需要滿足的幾個基本需求為:

1、從上到下逐層展開目錄層級

2、知道某一個目錄反查其全路徑

3、rename某一個路徑的名字

4、將某一個目錄挪到其他目錄下

 

現有的表結構可以滿足以上的需求:

1select id from table where parents_id=$id;(可以查出所有直接子節點)

2select full_path from table where id=$id;(通過全路徑字段獲取)

3update table set name=$newname where id=$id;(將需要修改的id的name字段修改)

4update table set parents_id=$new_parents_id,full_path=$new_full_path where id=$id;(修改父子關系到新的關系上)

 

但是現有的表結構會遇到的問題就是,第3和第4個需求,其並不是只更新一行即可,由於有full_path的存在,所有被修改的節點,其下面的所有節點的full_path都需要修改。這就無形之間增加了很多寫操作,如果這顆樹比較深,比如有100層,那么修改第3層的數據,那么就意味着其下面97層的所有數據都需要被修改,這個產生的寫操作就有些恐怖了。

以列子所示,如果4的name被修改,都會影響4,5,6,7,8,9一共6行數據的更新,這個寫邏輯放大的有點厲害。

update table set name=x,full_path='/a/x' where id=4;
update table set full_path='/a/x/e' where id=5;
update table set full_path='/a/x/f' where id=6;
update table set full_path='/a/x/e/g' where id=7;
update table set full_path='/a/x/e/h' where id=8;
update table set full_path='/a/x/e/i' where id=9;

那么如何解決這個問題呢?

 

二、優化方案

1、去除full_path字段

上面所述問題最嚴重的就是寫邏輯放大的問題,采用去除full_path字段后,6條update就變成1條update的了。

這個優化看起來完美解決寫邏輯放大問題,但是引入了另一個問題,那就是需求2的滿足費勁了。

原有SQL是:

select full_path from table where id=$id;

但是去除full_path字段之后,變為:

select parents_id from table where id =$id;
select parents_id from table where id = $parents_id;

以示例來說,如果要得到9的全路徑,那么就需要如下SQL

select parents_id,name from table where id = 9;
select parents_id,name from table where id = 5;
select parents_id,name from table where id = 4;
select parents_id,name from table where id = 1;

當最后判斷到parents_id=0的時候結束,然后將所有name聚合在一起。

如果所有操作都需要前端實現,那么就需要前端和DB交互4次,這期間消耗的鏈接成本會極大的延長總體的響應時間,基本是不可接收的。

如果要采用這種方案,目前看來只有使用存儲過程,將所有計算都在本地完成之后在返回給端,保證一次請求,一次返回,這樣才最效率,但是mysql的存儲過程個人不太建議使用,風險較大。

 

2、產品規范

我們的問題會發生在樹的層級特別多的情況下,那么可以通過產品規范來進行限制,比如最深只能有4層,這樣就將問題遏制在發生之前了。(當然,有些時候這種最有效的優化方案是最不可能實現的,產品不會那么容易妥協)

 

3、增加cache

問題既然是寫邏輯放大,那么如果我們將優化思路從降低寫入次數,改為提高寫入性能呢?

我們引入redis這種nosql存儲,將id和full_path存放在redis中,每次更新數據庫之后在更新redis,redis的寫入性能遠高於mysql,這樣問題也可以得到解決。

只不過由於更新不是同步的,采用異步更新之后,數據會最終一致,但是在某一個特殊時間點可能會存在不一致。

並且由於存儲架構變化,需要代碼方面做出一定的妥協,無論是讀操作還是寫操作。

 

4、整體改變存儲結構

以上方案都是在不大改現有表結構的基礎上做出的,那么有沒有可能修改表結構之后情況會不一樣呢?

我們對所示例子的存儲結構引入層級的概念,去除full_path,看看是否可以解決問題。

新的表結構如下:

id_name(id和name映射關系)

id name
1 a
2 b
3 c
4 d
5 e
6 f
7 g
8 h
9 i

 

 

 

 

 

 

 

 

 

 

relation(父子關系)

id chailds depth
1 1 0
1 2 1
1 3 1
1 4 1
1 5 2
1 6 2
1 7 3
1 8 3
1 9 3
2 2 0
3 3 0
4 4 0
4 5 1
4 7 2
4 8 2
4 9 2
5 5 0
5 7 1
5 8 1
5 9 1
6 6 0
7 7 0
8 8 0
9 9 0

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

這兩張新表第一張不用解釋了,第二張id字段存放節點id,chailds字段存放其所有子節點(並不是直接chaild,而是不論層級都存放),depth字段存放其子節點和本節點的層級關系。

我們看下這么設計是否可以滿足最初的4個需求:

需求1:逐層展開目錄

select id,depth from table2 where id=$id;
select name from table1 where id=$id;

由於每個id都存放了其所有的子節點,所以如果查詢4的所有下屬目錄,直接select id,depth from table2 where id = 4;一條SQL即可獲得所有結果,只要前端在處理一下即可。

id chailds depth
4 4 0
4 5 1
4 6 1
4 7 2
4 8 2
4 9 2

 

 

 

 

 

 

 

 

需求2:根據某一個目錄獲知其全路徑

select id,depth from table2 where chailds = $id;

由於每個id都存放了所有子節點,所以反差也是一條sql的事情。比如查詢9的全路徑,那么select id,depth from table2 where chailds=9;得到的結果應該是

id chailds depth
9 9 0
5 9 1
4 9 2
1 9 3

 

 

 

 

 

通過上述結果,前端進行計算處理就可以得到9的全路徑了,並且是一條sql獲得,不需要前端和db多次交互,也不需要引入存儲過程。

 

需求3:更改目錄名稱

update table1 set name = $new_name where id = $id ;

這個最簡單了,只需要更改映射表即可。

 

需求4:更改節點的父子關系

select id from table2 where id=$id and depth > 0;
delete from table2 where id = $sql1_id;
select id from table2 where id = $new_father_id;
inset into table2 values ($sql2_id,$id,$depth+1);

這個需求目前看來最麻煩,我們以示例所示,如果將5挪到3下面需要經過哪些操作?

I:先查出來5都屬於哪些節點的子節點。

select id from table 2 where id=5 and depth > 0;

id

chailds depth
1 5 2
4 5 1

 

 

 

 

II:刪除這些記錄。

delete from table2 where id=1 and chailds=5;

delete from table2 where id=4 and chailds=5;

III:查出新父節點是哪些節點的子節點。

select id,depth from table where chailds=3 and depth > 0 ;

id chailds depth
1 3 1

 

 

IIII:

根據III的結果插入新的關系。

insert into table2 values (1,5,2);

由於新父節點只是1的子節點,故只需要在增加5和1個關系既可,同時由於3是5的新父節點,那么5和1的深度關系應該比3的關系“+1”。

而所有5下面的節點都不需要更改,因為這么設計所有節點都是自己子節點的root,所以只要修改5的關系即可。

但是這個解決方案明顯可以看出來,需要存儲的關系比原有情況多了很多倍,尤其是關系層級深的情況下。

 

三、總結

方案1:解決寫邏輯放大問題,但是引入了讀邏輯放大問題,並需要引入存儲過程解決。

方案2:產品規范解決,最徹底的解決方法,但需要和PM溝通確認,業務很難妥協。

方案3:引入cache解決寫入性能,但是需要代碼進行修改,並存在數據不一致的風險。

方案4:解決寫邏輯放大問題,也沒有引入讀邏輯放大問題,僅僅只是在更改目錄的時候稍微麻煩一些,但是引入了初始存儲容量暴增的問題。

目前看來,並沒有什么特別優秀的方案,都需要付出一定的代價。

 

ps:本文的思路參考了《SQL反模式》,如果有興趣的讀者可以去研讀一下。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM