程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 數據庫知識 >> MYSQL數據庫 >> MySQL綜合教程 >> 如何使用mysql存儲樹形關系

如何使用mysql存儲樹形關系

編輯: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、將某一個目錄挪到其他目錄下

 

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

、 id    parents_id、 full_path    id、   name$newname  id、   parents_id$new_parents_id,full_path=$new_full_path  id$id;(修改父子關系到新的關系上)

 

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

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

   namex,full_path  id   full_path  id   full_path  id   full_path  id   full_path  id   full_path  id;

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

 

1、去除full_path字段

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

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

原有SQL是:

 full_path    id$id;

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

 parents_id    id  parents_id    id  $parents_id;

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

 parents_id,name    id   parents_id,name    id   parents_id,name    id   parents_id,name    id  ;

當最後判斷到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:逐層展開目錄

 id,depth  table2  id name  table1  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:根據某一個目錄獲知其全路徑

 id,depth  table2  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:更改目錄名稱

 table1  name  $new_name  id  $id ;

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

 

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

 id  table2  id$id  depth    table2  id  id  table2  id  table2  ($sql2_id,$id,$depth);

這個需求目前看來最麻煩,我們以示例所示,如果將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的關系即可。

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

 

三、總結

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

 

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

 

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved