最近在工作中業務需要,了解了左右值編碼的樹形結構存儲方案,仔細研究了一下,整理了一個筆記分享給大家,如有錯誤之處望指出。
一、左右值編碼
在基於數據庫的一般應用中,查詢的需求總要大於刪除和修改。為了避免對於樹形結構查詢時的“遞歸”過程,基於Tree的前序遍歷設計一種全新的無遞歸查詢、無限分組的左右值編碼方案,來保存該樹的數據。
第一次看見這種表結構,相信大部分人都不清楚左值(Lft)和右值(Rgt)是如何計算出來的,而且這種表設計似乎並沒有保存父子節點的繼承關系。但當你用手指指着表中的數字從1數到18,你應該會發現點什么吧。對,你手指移動的順序就是對這棵樹進行前序遍歷的順序,如下圖所示。當我們從根節點Food左側開始,標記為1,並沿前序遍歷的方向,依次在遍歷的路徑上標注數字,最后我們回到了根節點Food,並在右邊寫上了18。
依據此設計,我們可以推斷出所有左值大於2,並且右值小於11的節點都是Fruit的后續節點,整棵樹的結構通過左值和右值存儲了下來。然而,這還不夠,我們的目的是能夠對樹進行CRUD操作,即需要構造出與之配套的相關算法。按照深度優先,由左到右的原則遍歷整個樹,從1開始給每個節點標注上left值和right值,並將這兩個值存入對應的name之中。
二、如何查詢?
1. 查詢所有子節點
獲取某個節點下的所有子孫節點,以Red為例:
SELECT * FROM Tree WHERE Lft > 3 AND Lft < 6 ORDER BY Lft ASC
2. 子孫節點總數
子孫總數 = (右值–左值–1)/2,以Fruit為例,其子孫總數為:(11–2–1)/2 = 4
3. 當前節點層數
獲取節點在樹中所處的層數,以Fruit為例:
SELECT COUNT(*) FROM Tree WHERE Lft <= 2 AND Rgt >=11
4. 當前節點所在路徑
獲取當前節點所在路徑,以Beaf為例:
SELECT * FROM Tree WHERE Lft <= 13 AND Rgt >=14 ORDER BY Lft ASC
在日常的處理中我們經常還會遇到的需要獲取某一個節點的直屬上級、同級、直屬下級。為了更好的描述層級關系,我們可以為Tree建立一個視圖,添加一個層次列,該列數值可以編寫一個自定義函數來計算:
CREATE FUNCTION `CountLayer`(`_node_id` int) RETURNS int(11) BEGIN DECLARE _result INT; DECLARE _lft INT; DECLARE _rgt INT; IF EXISTS(SELECT Node_id FROM Tree WHERE Node_id = _node_id) THEN SELECT Lft,Rgt FROM Tree WHERE Node_id = _node_id INTO _lft,_rgt; SET _result = (SELECT COUNT(1) FROM Tree WHERE Lft <= _lft AND Rgt >= _rgt); RETURN _result; ELSE RETURN 0; END IF; END;
在添加完函數以后,我們創建一個a視圖,添加新的層次列:
CREATE VIEW `NewView`AS SELECT Node_id, Name, Lft, Rgt, CountLayer(Node_id) AS Layer FROM Tree ORDER BY Lft ;
5. 查詢父節點
獲取當前節點父節點,以Fruit為例:
SELECT * FROM treeview WHERE Lft <= 2 AND Rgt >=11 AND Layer=1
6. 查詢直屬子節點
獲取所有直屬子節點,以Fruit為例:
SELECT * FROM treeview WHERE Lft BETWEEN 2 AND 11 AND Layer=3
7. 查詢兄弟節點
獲取所有兄弟節點,以Fruit為例:
SELECT * FROM treeview
WHERE Rgt > 11
AND Rgt < (SELECT Rgt FROM treeview WHERE Lft <= 2 AND Rgt >=11 AND Layer=1)
AND Layer=2
8. 查詢所有葉子節點
葉子節點特點Rgt=Lft+1
,以Fruit為例:
SELECT * FROM Tree WHERE Rgt = Lft + 1 AND Lft BETWEEN 2 AND 11
三、如何創建樹?如何新增數據?
1. 建表
上面已經介紹了如何檢索結果,那么如何才能增加新的節點呢?Nested set 最重要是一定要有一個根節點作為所有節點的起點,而且通常這個節點是不被使用的。為了便於控制查詢級別,在建表的時候建議添加parent_id配合之聯結列表方式一起使用。
CREATE TABLE IF NOT EXISTS `Tree` (
`node_id` int(11) NOT NULL AUTO_INCREMENT,
`parent_id` int(10) UNSIGNED NOT NULL DEFAULT "0",
`name` varchar(255) NOT NULL,
`lft` int(11) NOT NULL DEFAULT "0",
`rgt` int(11) NOT NULL DEFAULT "0",
PRIMARY KEY (`node_id`),
KEY `idx_left_right` (`lft`,`rgt`)
) DEFAULT CHARSET=utf8;
INSERT INTO `Tree` (parent_id,name,lft,rgt) VALUES ( 0,"Food",1,2)
2. 添加葉子節點
1. 前插
=>
添加子節點(子節點起始處),以在Food下添加子節點Fruit為例:
LOCK TABLE Tree WRITE;
SELECT @parent_id := node_id, @myLeft := lft FROM Tree WHERE name = "Food";
UPDATE Tree SET rgt = rgt + 2 WHERE rgt > @myLeft;
UPDATE Tree SET lft = lft + 2 WHERE lft > @myLeft;
INSERT INTO Tree(parent_id, name, lft, rgt) VALUES(@parent_id, "Fruit", @myLeft + 1, @myLeft + 2);
UNLOCK TABLES;
2. 后插
=>
如需在末尾追加就需要以下方式進行(以在Fruit下添加Yellow為例):
LOCK TABLE Tree WRITE;
SELECT @parent_id := node_id , @myRight := rgt FROM Tree WHERE name = "Fruit";
UPDATE Tree SET rgt = rgt + 2 WHERE rgt >= @myRight;
UPDATE Tree SET lft = lft + 2 WHERE lft > @myRight;
INSERT INTO Tree(parent_id, name, lft, rgt) VALUES(@parent_id, "Yellow", @myRight, @myRight + 1);
UNLOCK TABLES;
3. 后插兄弟節點
在節點A后面添加同級節點(以在Yellow后面添加Green為例)
LOCK TABLE Tree WRITE;
SELECT @parent_id := parent_id , @myRight := rgt FROM Tree WHERE name = "Yellow";
UPDATE Tree SET rgt = rgt + 2 WHERE rgt > @myRight;
UPDATE Tree SET lft = lft + 2 WHERE lft > @myRight;
INSERT INTO Tree(parent_id, name, lft, rgt) VALUES(@parent_id, "Green", @myRight+1, @myRight+2);
UNLOCK TABLES;
3. 添加非葉子節點
→
以上討論的添加節點指的都是添加末端節點,即插入的這個節點不是當前已存在節點的父節點。如果需要插入非末端節點要怎么辦呢?
這個過程可以將流程分為2步,首先新增節點,接下里再將需要的節點移到新增的節點下級。節點移動方法(以將Apple移到Yellow中為例):
LOCK TABLE Tree WRITE;
SELECT @nodeId := node_id , @myLeft := lft , @myRight := rgt FROM Tree WHERE name = "Apple";
UPDATE Tree SET lft = lft - (@myRight - @myLeft) - 1 WHERE lft > @myRight;
UPDATE Tree SET rgt = rgt - (@myRight - @myLeft) - 1 WHERE rgt > @myRight;
SELECT @parent_id := node_id , @Left := lft , @Right := rgt FROM Tree WHERE name = "Yellow";
UPDATE Tree SET lft = lft + (@myRight - @myLeft) + 1 WHERE lft > @Left;
UPDATE Tree SET rgt = rgt + (@myRight - @myLeft) + 1 WHERE lft > @Left;
UPDATE Tree SET parent_id = @parent_id WHERE name = node_id = @nodeId;
UPDATE Tree SET lft = @Left + lft - @myLeft + 1, rgt = @Left + lft - @myLeft + 1 + (@myRight - @myLeft) WHERE lft >= @myLeft AND rgt <= @myRight;
UNLOCK TABLES;
四、移動節點
如上圖移動節點時,節點左右值更新有一定范圍,我們可以划為兩個部分,第一個部分是被移動節點和其子節點,第二部分則是其他節點的值;這里定義第一部分所包含的左右節點數量為gap
,第二部分中的節點值為step
,我們會發現一個規律,第一部分左右節點改變的值正好為step
,第二部分改變的值為gap
;
這個過程可以將流程分為2步,首先移動節點,接下來是修改移動路徑中的節點。節點移動方法(以將Apple移到Yellow中為例):
1. 前移
前移時的規律是:
=>
start =新父節點rgt;median=被移動節點lft;end=被移動節點rgt+1;
gap=被移動節點rgt-被移動節點lft+1;step=被移動節點lft-新父節點rgt;
LOCK TABLE Tree WRITE;
SELECT @nodeId := node_id , @myLeft := lft , @myRight := rgt FROM Tree WHERE name = "Apple";
SELECT @nParent_id := node_id , @nLeft := lft , @nRight := rgt FROM Tree WHERE name = "Yellow";
update Tree set
lft = CASE
WHEN lft >= @start: and lft < @median:
THEN (lft + @gap:)
WHEN lft >= @median:
and lft <![CDATA[ < ]]> @end:
THEN (lft - @step:)
ELSE
lft
END ,
rgt = CASE
WHEN rgt >= @start:
and rgt <![CDATA[ < ]]> @median:
THEN rgt + @gap:
WHEN rgt >= @median:
and rgt <![CDATA[ < ]]> @end:
THEN rgt - @step:
ELSE
rgt
END
UNLOCK TABLES;
2. 后移
后移的規律是:
=>
start =被移動節點lft;median=被移動節點rgt+1;end=新父節點rgt;
gap=被移動節點rgt-被移動節點lft+1;step=新父節點rgt-被移動節點rgt-1;
LOCK TABLE Tree WRITE;
SELECT @nodeId := node_id , @myLeft := lft , @myRight := rgt FROM Tree WHERE name = "Apple";
SELECT @nParent_id := node_id , @nLeft := lft , @nRight := rgt FROM Tree WHERE name = "Yellow";
update Tree set
lft = CASE
WHEN lft >= @start: and lft < @median:
THEN (lft + @step:)
WHEN lft >= @median:
and lft <![CDATA[ < ]]> @end:
THEN (lft - @gap:)
ELSE
lft
END ,
rgt = CASE
WHEN rgt >= @start:
and rgt <![CDATA[ < ]]> @median:
THEN rgt + @step:
WHEN rgt >= @median:
and rgt <![CDATA[ < ]]> @end:
THEN rgt - @gap:
ELSE
rgt
END
UNLOCK TABLES;
五、刪除節點
1. 刪除葉子節點
LOCK TABLE Tree WRITE;
SELECT @myLeft := lft , @myRight := rgt FROM Tree WHERE name = "Apple";
DELETE Tree WHERE lft >= @myLeft AND rgt <= @myRight;
UPDATE Tree SET lft = lft - (@myRight - @myLeft) - 1 WHERE lft > @myRight;
UPDATE Tree SET rgt = rgt - (@myRight - @myLeft) - 1 WHERE rgt > @myRight;
UNLOCK TABLES;
2. 刪除非葉子節點(包含子節點)
=>
如果需要只刪除該節點,子節點自動上移一級如何處理?
LOCK TABLE Tree WRITE;
SELECT @parent_id := parent_id , @node_id :=node_id , @myLeft := lft , @myRight := rgt FROM Tree WHERE name = "Yellow";
UPDATE Tree SET parent_id = @parent_id WHERE parent_id = @node_id
DELETE Tree WHERE lft = @myLeft;
UPDATE Tree SET lft = lft - 1,rgt = rgt-1 Where lft > @myLeft AND @rgt < @myRight
UPDATE Tree SET lft = lft - 2,rgt = rgt-2 Where lft > @rgt > @myRight
UNLOCK TABLES;
以上為Nested Set的CURD操作,具體在使用時建議結合事務和存儲過程一起使用。
本方案的
優點:時查詢非常的方便,
缺點:就是每次插入刪除數據涉及到的更新內容太多,如果樹非常大,插入一條數據可能花很長的時間。
原文:https://my.oschina.net/u/4112606/blog/4303806
原文:https://www.yht7.com/news/93554