樹形數據結構是我們常見的一種數據結構,比如文件目錄、公司組織結構等。但是關系型數據庫卻沒有對應的原生數據結構去存儲查詢這種數據結構,本文介紹了幾種實現關系型數據庫樹形數據存儲的方式供大家參考。
前言
樹形結構是生活中常見的數據結構之一,如公司的組織結構、計算機文件的目錄結構和家庭族譜等。本文將以區域作為示例,介紹幾種常見的數據庫實現樹形查詢的方式:
樹形結構的關鍵屬性:深度
方案一、毗鄰目錄模式(adjacency list model)
方案原理
毗鄰目錄模式在樹形結構數據的每條記錄中,記錄了指向父數據的記錄,如下圖所示:
數據庫中的表結構如下所示:
id | name | parent |
---|---|---|
1 | 上海 | 中國 |
2 | 浦東 | 上海 |
查詢情況1:當我們需要查詢上海的直接父區域時,通過以下Sql查詢:
select parent from 區域表 where name = '上海'
查詢情況2:當我們需要查詢上海的直接子區域時,通過以下Sql查詢:
select name from 區域表 where parent = '上海'
查詢情況3:當我們需要查詢上海的二級子區域時:
select name from 區域表 where parent in (select name from 區域表 where parent = '上海')
查詢情況4:當我們需要查詢上海的所有子區域,並且不知道區域樹的總層數(偽代碼):
result_set = select name from 區域表 where parent = '上海';
current_parent = select name from 區域表 where parent = '上海';
while current_parent is not null:
current_parent = select name from 區域表 where parent in current_parent
result_set.add_all(current_parent)
可以看到:此種查詢情況下,隨着樹的高度增加,IO次數也會增加。
方案優缺點
查詢所有子樹難度:高
查詢所有父節點難度:高
查詢下一層子節點難度:低
查詢上一層父節點難度:低
插入新記錄的難度:低
刪除原有記錄的難度:低
占用額外空間少,只需要占用額外一列O(n)的空間;
適用場景:
- 只包含直接父子查詢的場景
- 包含多層查詢,但是可以加載所有數據到內存中的場景
方案二、預排序遍歷樹算法(modified preorder tree traversal algorithm)
方案原理
預排序的意思就是我們在查詢前對存到數據庫中的數據進行一次特殊的排序,給每條數據添加兩個字段:左索引和右索引,添加的方式如下圖所示
數據庫中的表結構如下所示:
id | name | lindex | rindex |
---|---|---|---|
1 | 上海 | 16 | 25 |
2 | 浦東 | 19 | 24 |
查詢情況1:查找上海有多少子區域(不包含自身):
select rindex-lindex as region_num from 區域表 where name = '上海';
解釋:編號時,可以發現從上海的左側開始編號遞增,回到右側時候給所有的子節點左右都編號了一次,所以上海節點的右索引減去左索引除以2(每個子節點有左右兩個編號),就是子節點的總數目。
查詢情況2:查詢上海的所有子區域:
查詢情況2:查詢上海的所有子區域:
select name from 區域表 where lindex > 16 and rindex < 25;
解釋:由編號過程可以發現,上海子區域的編號值肯定在(19,24)范圍內,而非上海子區域的編號范圍肯定不在(19,24)范圍內,所以此處where中的lindex和rindex可以互換,例如以下語句也可以查詢出上海的子區域
select name from 區域表 where lindex > 16 and lindex < 25;
select name from 區域表 where rindex > 16 and rindex < 25;
select name from 區域表 where rindex > 16 and lindex < 25;
查詢情況3:查詢浦東區域的父區域(上海的父區域只有一個,不直觀):
select name from 區域表 where rindex < 19 and lindex > 24;
解釋:由上面的編號過程可知,只要一個節點的左右編號范圍在另外一個節點的左右編號范圍內(查詢2的逆推),同理,這個查詢語句中的左右19和24一樣可以互換,例如:
select name from 區域表 where rindex < 19 and lindex > 19;
select name from 區域表 where rindex < 24 and lindex > 24;
select name from 區域表 where rindex < 24 and lindex > 19;
修改情況1:刪除浦東區域
當樹形結構變更時,需要重新觸發預排序,如刪除浦東之后,左右索引值在19到24的值需要減1,左右索引大於24的需要減2.
update 區域表 set rindex = rindex-1 where rindex < 24 and rindex > 19;
update 區域表 set lindex = lindex-1 where lindex < 24 and lindex > 19;
update 區域表 set rindex = rindex-2 where rindex > 24;
update 區域表 set lindex = lindex-2 where lindex > 24;
修改情況2:把刪除的浦東區域添加回來:
update 區域表 set rindex = rindex+1 where rindex < 24 and rindex >= 19;
update 區域表 set lindex = lindex+1 where lindex < 24 and lindex >= 19;
update 區域表 set rindex = rindex+2 where rindex >= 24;
update 區域表 set lindex = lindex+2 where lindex >= 24;
方案優缺點
查詢所有子樹難度:低
查詢所有父節點難度:低
查詢下一層子節點難度:高
查詢上一層父節點難度:高
插入新記錄的難度:高
刪除原有記錄的難度:高
占用額外空間少,只需要占用額外一列O(n)的空間;
適用場景:
- 數據幾乎不更新的場景。
方案三、路徑枚舉法(Path Enumerations)
方案原理
該方法在每一條數據記錄后邊添加了一列,用於存儲根節點到該點的完整路徑。
id | name | path |
---|---|---|
1 | 上海 | 中國/ |
2 | 浦東 | 中國/上海 |
查詢情況1:查找上海有多少子區域(不包含自身):
select name from 區域表 where path like '中國/上海/%';
查詢情況2:查詢上海區域的所有父區域:
select name from 區域表 where path like '%/上海';
刪除/變更/增加情況:刪除/變更/增加上海區域:
需要更新所有子節點的路徑字符串。
方案優缺點
查詢所有子樹難度:低
查詢所有父節點難度:低
查詢下一層子節點難度:低
查詢上一層父節點難度:低
插入新記錄的難度:高
刪除原有記錄的難度:高
占用額外空間中等,只需要占用額外一列O(n)*O(m)的空間(n為節點總數目。m為樹的深度);
方案四、ClosureTable
方案原理
之前的方案中,都是對原有的記錄添加列,然后對新增的列進行查詢獲取父子節點信息關系。而ClosureTable則是新增一張表,用於記錄節點直接的關系(父節點,子節點,深度),如下圖中的孫橋和浦東,會生成以下關系記錄;
id | child | parent | deepth |
---|---|---|---|
1 | 孫橋 | 浦東 | 1 |
2 | 孫橋 | 上海 | 2 |
3 | 孫橋 | 中國 | 3 |
4 | 浦東 | 上海 | 1 |
5 | 浦東 | 中國 | 2 |
6 | 上海 | 中國 | 1 |
查詢情況1:查詢上海的下一層子區域:
select child from 區域表 where parent = '上海' and deepth = 1;
查詢情況2:查詢上海的所有子區域:
select child from 區域表 where parent = '上海';
查詢情況3:查詢上海的上一層父區域:
select parent from 區域表 where child = '上海' and deepth = 1;
查詢情況4:查詢上海的所有父區域:
select parent from 區域表 where child = '上海';
刪除情況:刪除上海區域:
更新上海的子節點的深度:
parents = select parent from 區域表 where child = '上海';
children = select child from 區域表 where parent = '上海';
update 區域表 set depth = depth -1 where parent in parents and child in children.
delete parent from 區域表 where child = '上海' or parent = '上海';
方案優缺點
查詢所有子樹難度:低
查詢所有父節點難度:低
查詢下一層子節點難度:低
查詢上一層父節點難度:低
插入新記錄的難度:高
刪除原有記錄的難度:高
占用額外空間高,需要額外一張表存O(n)*O(m)條記錄(n為節點總數目。m為樹的深度);
我是御狐神,歡迎大家關注我的微信公眾號
本文最先發布至微信公眾號,版權所有,禁止轉載!