樹形結構的數據庫表設計

[toc]node

1 基礎數據

咱們以如下數據爲例進行說明sql

graph TD; A --> AA; A --> AB; A --> AC; AB --> ABA; AB --> ABB; AB --> ABC; AC --> ACA; ACA --> ACAA; ACA --> ACAB;

2 繼承關係驅動的架構設計

2.1 表結構

id parent_id name
1 A
2 1 AA
3 1 AB
4 3 ABA
5 3 ABB
6 3 ABC
7 1 AC
8 7 ACA
9 8 ACAA
10 8 ACAB

2.2 方案的優勢及缺點

  1. 優勢: 設計和實現簡單, 直觀
  2. 缺點: CURD操做是低效的, 主要歸根於頻繁的「遞歸」操做致使的IO開銷
  3. 解決方案: 在數據規模較小的狀況下能夠經過緩存機制來優化

3 基於左右值編碼的架構設計

  關於此方案的設計能夠查看另外一篇博客, 本人也是經過查看此篇博客學習的, 一些說明也是直接粘過來的, 因此部分細節我這裏再也不說明, 本篇博客與其的區別主要在於第四節   在基於數據庫的通常應用中,查詢的需求總要大於刪除和修改。爲了不對於樹形結構查詢時的「遞歸」過程,基於Tree的前序遍歷設計一種全新的無遞歸查詢、無限分組的左右值編碼方案,來保存該樹的數據。數據庫

3.1 表結構

id | left | right | name -- | -- | -- | :-- 1 | 1 | 20 | A 2 | 2 | 3 | AA 3 | 4 | 11 | AB 4 | 5 | 6 | ABA 5 | 7 | 8 | ABB 6 | 9 | 10 | ABC 7 | 12 | 19 | AC 8 | 13 | 18 | ACA 9 | 14 | 15 | ACAA 10 | 16 | 17 | ACAB   第一次看見這種表結構,相信大部分人都不清楚左值(left)和右值(right)是如何計算出來的,並且這種表設計彷佛並無保存父子節點的繼承關係。但當你用手指指着表中的數字從1數到20,你應該會發現點什麼吧。對,你手指移動的順序就是對這棵樹進行前序遍歷的順序,以下圖所示。當咱們從根節點A左側開始,標記爲1,並沿前序遍歷的方向,依次在遍歷的路徑上標註數字,最後咱們回到了根節點A,並在右邊寫上了20。緩存

graph TD; A["(1) A (20)"] --> AA["(2) AA (3)"]; A --> AB["(4) AB (11)"]; AB --> ABA["(5) ABA (6)"]; AB --> ABB["(7) ABB (8)"]; AB --> ABC["(9) ABC (10)"]; A --> AC["(12) AC (19)"]; AC --> ACA["(13) AC (18)"]; ACA --> ACAA["(14) AC (15)"]; ACA --> ACAB["(16) AC (17)"];

3.2 方案優缺點

  1. 優勢:
    • 能夠方便的查詢出某個節點的全部子孫節點
    • 能夠方便的獲取某個節點的族譜路徑(即全部的上級節點)
    • 可已經過自身的left, right值計算出共有多少個子孫節點
  2. 缺點:
    • 增刪及移動節點操做比較複雜
    • 沒法簡單的獲取某個節點的子節點

4 基於繼承關係及左右值編碼的架構設計

  其實就是在第三節的基礎上又加了一列parent_id, 目的是在保留上述優勢的同時能夠簡單的獲取某個節點的直屬子節點架構

4.1 表結構

id parent_id left right name
1 1 20 A
2 1 2 3 AA
3 1 4 11 AB
4 3 5 6 ABA
5 3 7 8 ABB
6 3 9 10 ABC
7 1 12 19 AC
8 7 13 18 ACA
9 8 14 15 ACAA
10 8 16 17 ACAB

4.2 CURD操做

4.2.1 create node

# 爲id爲 id_ 的節點建立名爲 name_ 的子節點
CREATE PROCEDURE `tree_create_node`(IN `id_` INT, IN `name_` VARCHAR(50))
	LANGUAGE SQL
	NOT DETERMINISTIC
	CONTAINS SQL
	SQL SECURITY DEFINER
	COMMENT '建立節點'
BEGIN
	declare right1 int;
	# 當 id_ 爲 0 時表示建立根節點
	if id_ = 0 then
		# 此處我限制了僅容許存在一個根節點, 固然這並非必須的
		if exists(select `id` from tree_table where `left` = 1) then
			SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '根節點已存在';
		end if;
		
		insert into tree_table(`parent_id`, `name`, `left`, `right`)
		values(0, name_, 1, 2);
		commit;
	elseif exists(select `id` from tree_table where `parent_id` = id_ and `name` = name_) then
		# 禁止在同一級建立同名節點
		SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '已存在同名兄弟節點';
	elseif exists(select `id` from tree_table where `id` = id_ and `is_delete` = 0) then
		start transaction;
		set right1=(select `right` from tree_table where `id` = id_);
		
		update tree_table set `right` = `right` + 2 where `right` >= right1;
		update tree_table set `left` = `left` + 2 where `left` >= right1;

		insert into tree_table(`parent_id`, `name`, `left`, `right`) 
		values(id_, name_, right1, right1 + 1);

		commit;
		# 下面一行僅爲了展現如下新插入記錄的id, 並非必須的
		select LAST_INSERT_ID();
	else
		SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '父節點不存在(未建立或被刪除)';
	end if;
END
# 建立根節點
call tree_create_node(0, 'A')
# 爲節點1建立名爲AB的子節點
call tree_create_node(1, 'AB')

4.2.2 delete node

CREATE PROCEDURE `tree_delete_node`(IN `id_` INT)
	LANGUAGE SQL
	NOT DETERMINISTIC
	CONTAINS SQL
	SQL SECURITY DEFINER
	COMMENT ''
BEGIN
	declare left1 int;
	declare right1 int;
	if exists(select id from tree_table where id = id_) then
		start transaction;
		select `left`, `right` into left1, right1 from tree_table where id = id_;
		delete from tree_table where `left` >= left1 and `right` <= right1;
		update tree_table set `left` = `left` - (right1-left1+1) where `left` > left1;
		update tree_table set `right` = `right` - (right1-left1+1) where `right` > right1;      
		commit;
	end if;
END
# 刪除節點2, 節點2的子孫節點也會被刪除
call tree_delete_node(2)

4.2.3 move node

   move的原理是先刪除再添加, 但涉及被移動的節點的left, right值不能亂因此須要使用臨時表(因爲在存儲過程當中沒法建立臨時表, 此處我使用了一張正常的表進行緩存, 歡迎提出更合理的方案)學習

# 此存儲過程當中涉及到is_delete字段, 表示數據是否被刪除, 由於正式環境中刪除操做通常都不會真的刪除而是進行軟刪(即標記刪除), 若是不須要此字段請自行對程序進行調整
CREATE PROCEDURE `tree_move_node`(IN `self_id` INT, IN `parent_id` INT
, IN `sibling_id` INT)
	LANGUAGE SQL
	NOT DETERMINISTIC
	CONTAINS SQL
	SQL SECURITY DEFINER
	COMMENT ''
BEGIN
	declare self_left int;
	declare self_right int;
	declare parent_left int;
	declare parent_right int;
	declare sibling_left int; 
	declare sibling_right int;
	declare sibling_parent_id int;
	if exists(select id from tree_table where id = parent_id and is_delete = 0) then
		# 建立中間表
		CREATE TABLE If Not Exists tree_table_self_ids (`id` int(10) unsigned NOT NULL);
		truncate tree_table_self_ids;
		
		start transaction;  # 事務
		# 獲取移動對象的 left, right 值
		select `left`, `right` into self_left, self_right from tree_table where id = self_id;
		# 將須要移動的記錄的 id 存入臨時表, 以保證操做 left, right 值變化時這些記錄不受影響
		insert into tree_table_self_ids(id) select id from tree_table where `left` >= self_left and `right` <= self_right;
		
		# 將被移動記錄後面的記錄往前移, 填充空缺位置
		update tree_table set `left` = `left` - (self_right-self_left+1) where `left` > self_left and id not in (select id from tree_table_self_ids);
		update tree_table set `right` = `right` - (self_right-self_left+1) where `right` > self_right and id not in (select id from tree_table_self_ids);
		
		select `left`, `right` into parent_left, parent_right from tree_table where id = parent_id;
		if sibling_id = -1 then
			# 在末尾插入子節點
			update tree_table set `right` = `right` + (self_right-self_left+1) where `right` >= parent_right and id not in (select id from tree_table_self_ids);
			update tree_table set `left` = `left` + (self_right-self_left+1) where `left` >= parent_right and id not in (select id from tree_table_self_ids);
			update tree_table set `right`=`right` + (parent_right-self_left), `left`=`left` + (parent_right-self_left) where id in (select id from tree_table_self_ids);
		elseif sibling_id = 0 then
			# 在開頭插入子節點
			update tree_table set `right` = `right` + (self_right-self_left+1) where `right` > parent_left and id not in (select id from tree_table_self_ids);
			update tree_table set `left` = `left` + (self_right-self_left+1) where `left` > parent_left and id not in (select id from tree_table_self_ids);
			update tree_table set `right`=`right` - (self_left-parent_left-1), `left`=`left` - (self_left-parent_left-1) where id in (select id from tree_table_self_ids);
		else
			# 插入指定節點以後
			select `left`, `right`, `parent_id` into sibling_left, sibling_right, sibling_parent_id from tree_table where id = sibling_id;
			if parent_id != sibling_parent_id then
				SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '指定的兄弟節點不在指定的父節點中';
			end if;
			update tree_table set `right` = `right` + (self_right-self_left+1) where `right` > sibling_right and id not in (select id from ctree_table_self_ids);
			update tree_table set `left` = `left` + (self_right-self_left+1) where `left` > sibling_right and id not in (select id from tree_table_self_ids);
			update tree_table set `right`=`right` - (self_left-sibling_right-1), `left`=`left` - (self_left-sibling_right-1) where id in (select id from tree_table_self_ids);
		end if;
		update tree_table set `parent_id`=parent_id where `id` = self_id;
		commit;
	else
		SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '父節點不存在(未建立或被刪除)';
	end if;
END
# 將節點2移動到節點1下面開頭的位置
call tree_move_node(2, 1, 0)
# 將節點2移動到節點1下面末尾的位置
call tree_move_node(2, 1, -1)
# 將節點2移動到節點1下面且跟在節點3後面的位置
call tree_move_node(2, 1, 3)

4.2.4 select

# 如下sql中須要傳的值全用???表示
# 根據節點id獲取此節點全部子孫節點
select * from tree_table where 
	left > (select left from tree_table where id=???) and 
	right < (select right from tree_table where id=???)
# 根據節點id獲取此節點的全部子孫節點(包含本身)
select * from tree_table where 
	left >= (select left from tree_table where id=???) and 
	right <= (select right from tree_table where id=???)
# 根據節點id獲取此節點的全部上級節點
select * from tree_table where 
	left < (select left from tree_table where id=???) and 
	right > (select right from tree_table where id=???)
# 根據節點id獲取此節點的全部上級節點(包括本身)
select * from tree_table where 
	left <= (select left from tree_table where id=???) and 
	right >= (select right from tree_table where id=???)

5 總結

   此篇文章對左右值編碼結構的原理介紹的很少, 須要詳細瞭解的能夠查閱末尾引用的博客優化

樹形結構的數據庫表設計編碼

相關文章
相關標籤/搜索