MySQL基於左右值編碼的樹形數據庫表結構設計


MySQL基於左右值編碼的樹形數據庫表結構設計
 
在關系型數據庫中設計樹形的數據結構一直是一個十分考驗開發者能力的,最常用的方案有主從表方案和繼承關系(parent_id)方案。主從表方案的最大缺點是樹形結構的深度擴展困難,一般來說都是固定的,適合深度固定的需求。繼承關系方案設計和實現自然而然,非常直觀和方便。缺點當然也是非常的突出:由於直接地記錄了節點之間的繼承關系,因此對Tree的任何 CRUD操作都將是低效的,這主要歸根於頻繁的“遞歸”操作,遞歸過程不斷地訪問數據庫,每次數據庫IO都會有時間開銷。因此這種方案適合Tree規模相對較小的情況,我們可以借助於緩存機制來做優化,將Tree的信息載入內存進行處理,避免直接對數據庫IO操作的性能開銷。
理想中樹形結構應該具備如下特征:檢索遍歷過程簡單高效;節點增刪改查CRUD操作高效;數據存儲冗余度小、直觀性強。筆者在查閱網上相關資料之后整理了一個基於左右值編碼的樹形結構的數據庫表結構設計方案,並在MySQL數據庫中實現。
首先我們記住以下這張圖
圖一 左右值屬性結構
采用深度優先遍歷給樹中的每個節點分配兩個值,一個左值和一個右值。節點左邊的值比該節點的所有子孫節點值都要小,節點右邊的值比該節點的所有子孫節點值都要大。例如:
B左邊的值為2,其比Hell Mayes的所有子孫節點的值都要小(D[3,4]、E[5,10]、I[6,7]、J[8,9]、F[11,12])
B右邊的值為13,其比Hell Mayes的所有子孫節點的值都要大(D[3,4]、E[5,10]、I[6,7]、J[8,9]、F[11,12])
有了這個規則整棵樹的結構通過左值和右值存儲了下來。
接下來我們在MySQL中建表,並實現整棵樹的CURD方法
創建表結構
CREATE TABLE `tree` (
`node_id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(45) DEFAULT NULL,
`lft` int(11) DEFAULT NULL,
`rgt` int(11) DEFAULT NULL,
PRIMARY KEY (`node_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
插入數據
INSERT INTO `tree` VALUES
(1,'A',1,20),
(2,'B',2,13),
(3,'C',14,19),
(4,'D',3,4),
(5,'E',5,10),
(6,'F',11,12),
(7,'G',15,16),
(8,'H',17,18),
(9,'I',6,7),
(10,'J',8,9);
准備工作就緒。
1)獲取某個節點的子孫節點
以B為例:
SELECT* FROM Tree WHERE Lft BETWEEN 2 AND 13 ORDER BY Lft ASC
圖二 B的子孫節點
某個節點到底有多少的子孫節點呢?通過該節點的左、右值我們可以將其子孫節點圈進來,則子孫總數 = (右值 – 左值– 1) / 2,以B為例,其子孫總數為:(13–2 – 1) / 2 = 5。同時,為了更為直觀地展現樹形結構,我們需要知道節點在樹中所處的層次,通過左、右值的SQL查詢即可實現。以B為例:
SELECT COUNT(*) FROM Tree WHERE Lft <= 2 AND Rgt >=13
結果為2,表明B處於該樹的第二層。為了方便描述,我們可以為Tree建立一個視圖,添加一個層次字段,該字段值可以寫一個自定義函數來計算,函數定義如下:
定義計算指定節點所在層的函數CountLayer
CREATE DEFINER=`root`@`localhost` FUNCTION `CountLayer`(p_node_id int) RETURNS int(11)
BEGIN
declare p_result,p_lft,p_rgt int default 0;
if exists (select 1 from tree where node_id=p_node_id) then
begin
select lft, rgt into p_lft, p_rgt from tree where node_id=p_node_id;
select count(*) into p_result from tree where lft <= p_lft and rgt >= p_rgt;
end;
return p_result;
end if;
RETURN 0;
END
函數名稱為CountLayer,需傳入指定節點的id。此時我們可以基於剛剛定義的層計算函數來創建一個包含層次字段的試圖。
定義層次試圖Tree_View
CREATE
ALGORITHM = UNDEFINED
DEFINER = `root`@`localhost`
SQL SECURITY DEFINER
VIEW `tree_view` AS
SELECT
`tree`.`node_id` AS `node_id`,
`tree`.`name` AS `name`,
`tree`.`lft` AS `lft`,
`tree`.`rgt` AS `rgt`,
COUNTLAYER(`tree`.`node_id`) AS `layer`
FROM
`tree`
ORDER BY `tree`.`lft`
圖三 各節點所處的層次
此時我們來創建一個存儲過程,用來獲取給定節點的所有子孫節點和每個節點所在的層。
獲取所有子孫節點的存儲過程GetChildrenNodeList
CREATE DEFINER=`root`@`localhost` PROCEDURE `GetChildrenNodeList`(in p_node_id int)
BEGIN
declare p_lft,p_rgt int default 0;
if exists (select node_id from tree where node_id=p_node_id) then
begin
select lft,rgt into p_lft,p_rgt from tree where node_id=p_node_id;
select * from Tree_View where lft between p_lft and p_rgt order by layer, lft;
end;
end if;
END
查詢B的所有子孫節點
call GetChildrenNodeList(2);
可以得到
圖四 B的所有子孫節點及相應的層
可以計算其子孫節點,當讓也可以計算其節點。
創建獲取所有父節點的存儲過程GetParentNodePath
CREATE DEFINER=`root`@`localhost` PROCEDURE `GetParentNodePath`(in p_node_id int)
BEGIN
declare p_lft,p_rgt int default 0;
if exists (select node_id from tree where node_id=p_node_id) then
begin
select lft,rgt into p_lft,p_rgt from tree where node_id=p_node_id;
select * from Tree_View where lft<p_lft and rgt>p_rgt order by layer,lft asc;
end;
end if;
END
以E節點為例:
call GetParentNodePath(5);
可以得到
圖五 E的所有父節點
2)在某個節點下插入一個子節點。
仔細觀察圖一,我們以在H下添加一個節點K為例。K節點的左值為H節點的右值,K節點的右值為其左值+1,其他所有右值大於等於K的左值的節點的右值須+2,所有左值大於等於K的左值的節點的左值須+2。圖示如下:
圖六 新增節點
我們將其定義為存儲過程:
添加節點的存儲過程AddSubNode
CREATE DEFINER=`root`@`localhost` PROCEDURE `AddSubNode`(in p_node_id int,in p_node_name varchar(50))
BEGIN
declare p_rgt int default 0;
if exists(select node_id from tree where node_id=p_node_id) then
begin
SET AUTOCOMMIT=0;
START TRANSACTION;
select rgt into p_rgt from tree where node_id=p_node_id;
update tree set rgt=rgt+2 where rgt>=p_rgt;
update tree set lft=lft+2 where lft>=p_rgt;
insert into tree(name,lft,rgt) values(p_node_name,p_rgt,p_rgt+1);
COMMIT;
end;
end if;
END
調用AddSubNode存儲過程
call AddSubNode(8,'K');
圖七 K節點成功插入
3)刪除節點
刪除節點是新增節點的逆向過程。
創建刪除節點存儲過程DelNode
CREATE DEFINER=`root`@`localhost` PROCEDURE `DelNode`(in p_node_id int)
BEGIN
declare p_lft,p_rgt int default 0;
if exists(select p_node_id from tree where node_id =p_node_id) then
START TRANSACTION;
select lft,rgt into p_lft,p_rgt from tree where node_id=p_node_id;
delete from tree where lft>=p_lft and rgt<=p_rgt;
update tree set lft=lft-(p_rgt - p_lft + 1) where lft > p_lft;
update tree set rgt=rgt-(p_rgt - p_lft + 1) where rgt > p_rgt;
COMMIT;
end if;
END
以刪除C節點為例
call DelNode(3);
再次查詢tree結果為:
圖八 C及其所有的子節點都被刪除
到此我們已將基於左右值編碼的樹形數據庫表結構設計的基本原理介紹完了。當然,對於節點的操作還遠不止這些,感興趣的朋友可以自己動手實現。
誠然,這個方案也有其不足之處:節點的添加、刪除及修改代價較大,將會涉及到表中多方面數據的改動。但是,在消除了遞歸操作的前提下實現了無限分組,而且查詢條件是基於整形數字的比較,效率很高。所以,該方案比較實用與查詢較多,變更不大的場景。
 


免責聲明!

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



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