層次數據結構的數據表設計

存儲樹形結構數據是一個常見的問題,同時也有多種解決方案。node

這裏介紹三種樹形結構的表設計方案:數據庫

  • 鄰接表模型bash

  • 基於路徑和層級的表設計函數

  • 基於左右值編碼的表設計(MPT)優化

這裏以一個在線食品店做爲例子,食品經過類別、顏色和品種組織食品。ui

示例以下:編碼

img

1、鄰接表模型

最簡單的方法就是使用鄰接表模型或者叫作遞歸模型。經過顯示地描述某一節點的父節點,從而可以創建二維的關係表,你只須要一個簡單的函數去迭代查詢便可獲取你的數據。spa

示例以下:.net

img

優勢:設計

  • 設計簡單

  • 實現容易

  • 直觀

缺點:

  • 因爲是遞歸模型CRUD操做低效

2、基於路徑和層級的表設計

在一的基礎上加上一個 level 字段來表示當前節點到根節點的距離和一個 key 字段來表示搜索路徑。

  1. Node_id 主鍵

  2. Name 名字

  3. Parent_id 父節點的id

  4. key 搜索路徑

  5. level 表示當前節點到根節點的距離或者層級

示例以下:

Node_id Name Parent_id key level
1 Food 0 "" 1
2 Fruit 1 "1-" 2
3 Red 2 "1-2-" 3
4 Cherry 3 "1-2-3-" 4
5 Yellow 2 "1-2-" 3
6 Banana 5 "1-2-5-" 4
7 Meat 1 "1-" 2
8 Beef 7 "1-7-" 3
9 Pork 7 "1-7-" 3

兩種需求查詢解決方案以下:

  1. 查找d的全部子孫節點:select * from table_name where key like "${d.key}-${d.id}-%"

  2. 查找某個節點的全部子節點:select * from table_name where key like "${d.key}-${d.id}-%" and level=${d.level}+1

此設計結構簡單,利用key和level兩個輔助字段能夠完成查詢操做比一更加高效,並且維護這兩個字段成本很低。

3、基於左右值編碼的表設計(MPT)

爲了不對於樹形結構查詢時的「遞歸」過程,基於Tree的前序遍歷設計一種全新的無遞歸查詢、無限分組的左右值編碼方案,來保存該樹的數據。

img

以下圖所示, 從根節點Food左側開始,標記爲1,並沿前序遍歷的方向,依次在遍歷的路徑上標註數字,最後咱們回到了根節點Food,並在右邊寫上了18。

注:lft 和 rgt 分別對應 DFS 的發現時間 d 和完成時間相同 f

img

若是咱們須要查詢Fruit的後續節點,只需找出全部左值大於2,而且右值小於11的節點便可。

1. 獲取某節點的子孫節點

返回某節點子孫節點的前序遍歷列表,以Fruit爲例:

SQL: SELECT* FROM Tree WHERE Lft BETWEEN 2 AND 11 ORDER BY Lft ASC

查詢結果以下:

img

那麼某個節點到底有多少的子孫節點呢?

經過該節點的左、右值咱們能夠將其子孫節點圈進來,則子孫總數 = (右值 – 左值– 1) / 2,以Fruit爲例,其子孫總數爲:(11 –2 – 1) / 2 = 4。

同時,爲了更爲直觀地展示樹形結構,咱們須要知道節點在樹中所處的層次,經過左、右值的SQL查詢便可實現。以Fruit爲例:SELECT COUNT(*) FROM Tree WHERE Lft <= 2 AND Rgt >=11。爲了方便描述,咱們能夠爲Tree創建一個視圖,添加一個層次數列,該列數值能夠寫一個自定義函數來計算,函數定義以下:

CREATE FUNCTION dbo.CountLayer
(
    @node_id int
)
RETURNS int
AS
begin
  declare @result int
  set @result = 0
  declare @lft int
  declare @rgt int
  if exists(select Node_id from Tree where Node_id = @node_id)
  begin
    select @lft = Lft, @rgt = Rgt from Tree where node_id = @node_id
    select @result = count(*) from Tree where Lft <= @lft and Rgt >= @rgt
  end
  return @result
end
GO
複製代碼

基於層次計算函數,咱們建立一個視圖,添加了新的記錄節點層次的數列:

CREATE VIEW dbo.TreeView
AS
SELECT Node_id, Name, Lft, Rgt, dbo.CountLayer(Node_id) AS Layer FROM dbo.Tree ORDER BY Lft
GO
複製代碼

建立存儲過程,用於計算給定節點的全部子孫節點及相應的層次:

CREATE PROCEDURE [dbo].[GetChildrenNodeList]
(
  @node_id int
)
AS
declare @lft int
declare @rgt int
if exists(select Node_id from Tree where node_id = @node_id)
  begin
    select @lft = Lft, @rgt = Rgt from Tree where Node_id = @node_id
    select * from TreeView where Lft between @lft and @rgt order by Lft ASC
  end
GO
複製代碼

如今,咱們使用上面的存儲過程來計算節點Fruit全部子孫節點及對應層次,查詢結果以下:

img

在進行樹的查詢遍歷時,只須要進行2次數據庫查詢,消除了遞歸,再加上查詢條件都是數字的比較,查詢的效率是極高的,隨着樹規模的不斷擴大,基於左右值編碼的設計方案將比傳統的遞歸方案查詢效率提升更多。

2. 獲取某節點的族譜路徑

假定咱們要得到某節點的族譜路徑,則根據左、右值分析只須要一條SQL語句便可完成。

以Fruit爲例:SELECT* FROM Tree WHERE Lft < 2 AND Rgt > 11 ORDER BY Lft ASC

相對完整的存儲過程以下:

CREATE PROCEDURE [dbo].[GetParentNodePath]
(
  @node_id int
)
AS
declare @lft int
declare @rgt int
if exists(select Node_id from Tree where Node_id = @node_id)
  begin
    select @lft = Lft, @rgt = Rgt from Tree where Node_id = @node_id
    select * from TreeView where Lft < @lft and Rgt > @rgt order by Lft ASC
  end
GO
複製代碼

3. 爲某節點添加子孫節點

假定咱們要在節點「Red」下添加一個新的子節點「Apple」,該樹將變成以下圖所示,其中紅色節點爲新增節點。

img

相對完整的插入子節點的存儲過程:

CREATE PROCEDURE [dbo].[AddSubNode]
(
  @node_id int,
  @node_name varchar(50)
)
AS
declare @rgt int
if exists(select Node_id from Tree where Node_id = @node_id)
  begin
    SET XACT_ABORT ON
    BEGIN TRANSCTION
    select @rgt = Rgt from Tree where Node_id = @node_id
    update Tree set Rgt = Rgt + 2 where Rgt >= @rgt
    update Tree set Lft = Lft + 2 where Lft >= @rgt
    insert into Tree(Name, Lft, Rgt) values(@node_name, @rgt, @rgt + 1)
    COMMIT TRANSACTION
    SET XACT_ABORT OFF
  end
GO
複製代碼

4. 刪除某節點

若是咱們想要刪除某個節點,會同時刪除該節點的全部子孫節點,而這些被刪除的節點的個數爲:(被刪除節點的右值 – 被刪除節點的左值+ 1) / 2,而剩下的節點左、右值在大於被刪除節點左、右值的狀況下會進行調整。來看看樹會發生什麼變化,以Beef爲例,刪除效果以下圖所示。

img

則咱們能夠構造出相應的存儲過程:

CREATE PROCEDURE [dbo].[DelNode]
(
  @node_id int
)
AS
declare @lft int
declare @rgt int
if exists(select Node_id from Tree where Node_id = @node_id)
  begin
    SET XACT_ABORT ON
    BEGIN TRANSCTION
      select @lft = Lft, @rgt = Rgt from Tree where Node_id = @node_id
      delete from Tree where Lft >= @lft and Rgt <= @rgt
      update Tree set Lft = Lft – (@rgt - @lft + 1) where Lft > @lft
      update Tree set Rgt = Rgt – (@rgt - @lft + 1) where Rgt > @rgt
      COMMIT TRANSACTION
    SET XACT_ABORT OFF
  end
GO
複製代碼

小結

優勢:

  • 消除了遞歸查詢,實現了無限嵌套

  • 查詢是基於整數的比較,效率很高

缺點:

  • 節點的添加、刪除及修改代價較大

在基於數據庫的通常應用中,查詢的需求總要大於刪除和修改,同時咱們能夠擴展MPT來實現更多的優化,例如:若是對層級需求較高,能夠結合MPT和二中的方法來實現。

參考文章:

相關文章
相關標籤/搜索