存儲樹形結構數據是一個常見的問題,同時也有多種解決方案。node
這裏介紹三種樹形結構的表設計方案:數據庫
鄰接表模型bash
基於路徑和層級的表設計函數
基於左右值編碼的表設計(MPT)優化
這裏以一個在線食品店做爲例子,食品經過類別、顏色和品種組織食品。ui
示例以下:編碼
最簡單的方法就是使用鄰接表模型或者叫作遞歸模型。經過顯示地描述某一節點的父節點,從而可以創建二維的關係表,你只須要一個簡單的函數去迭代查詢便可獲取你的數據。spa
示例以下:.net
優勢:設計
設計簡單
實現容易
直觀
缺點:
在一的基礎上加上一個 level 字段來表示當前節點到根節點的距離和一個 key 字段來表示搜索路徑。
Node_id 主鍵
Name 名字
Parent_id 父節點的id
key 搜索路徑
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 |
兩種需求查詢解決方案以下:
查找d的全部子孫節點:select * from table_name where key like "${d.key}-${d.id}-%"
查找某個節點的全部子節點:select * from table_name where key like "${d.key}-${d.id}-%" and level=${d.level}+1
此設計結構簡單,利用key和level兩個輔助字段能夠完成查詢操做比一更加高效,並且維護這兩個字段成本很低。
爲了不對於樹形結構查詢時的「遞歸」過程,基於Tree的前序遍歷設計一種全新的無遞歸查詢、無限分組的左右值編碼方案,來保存該樹的數據。
以下圖所示, 從根節點Food左側開始,標記爲1,並沿前序遍歷的方向,依次在遍歷的路徑上標註數字,最後咱們回到了根節點Food,並在右邊寫上了18。
注:lft 和 rgt 分別對應 DFS 的發現時間 d 和完成時間相同 f
若是咱們須要查詢Fruit的後續節點,只需找出全部左值大於2,而且右值小於11的節點便可。
返回某節點子孫節點的前序遍歷列表,以Fruit爲例:
SQL: SELECT* FROM Tree WHERE Lft BETWEEN 2 AND 11 ORDER BY Lft ASC
查詢結果以下:
那麼某個節點到底有多少的子孫節點呢?
經過該節點的左、右值咱們能夠將其子孫節點圈進來,則子孫總數 = (右值 – 左值– 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全部子孫節點及對應層次,查詢結果以下:
在進行樹的查詢遍歷時,只須要進行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
複製代碼
假定咱們要在節點「Red」下添加一個新的子節點「Apple」,該樹將變成以下圖所示,其中紅色節點爲新增節點。
相對完整的插入子節點的存儲過程:
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
複製代碼
若是咱們想要刪除某個節點,會同時刪除該節點的全部子孫節點,而這些被刪除的節點的個數爲:(被刪除節點的右值 – 被刪除節點的左值+ 1) / 2,而剩下的節點左、右值在大於被刪除節點左、右值的狀況下會進行調整。來看看樹會發生什麼變化,以Beef爲例,刪除效果以下圖所示。
則咱們能夠構造出相應的存儲過程:
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和二中的方法來實現。
參考文章: