最近在開發jSqlBox過程中,研究樹形結構的操作,突然發現一種簡單的樹結構數據庫存儲方案,在網上找了一下,沒有找到雷同的(也可能是花的時間不夠),現介紹如下:
目前常見的樹形結構數據庫存儲方案有以下四種,但是都存在一定問題:
1)Adjacency List::記錄父節點。優點是簡單,缺點是訪問子樹需要遍歷,發出許多條SQL,對數據庫壓力大。
2)Path Enumerations:用一個字符串記錄整個路徑。優點是查詢方便,缺點是插入新記錄時要手工更改此節點以下所有路徑,很容易出錯。
3)Closure Table:專門一張表維護Path,缺點是占用空間大,操作不直觀。
4)Nested Sets:記錄左值和右值,缺點是復雜難操作。
以上方法都存在一個共同缺點:操作不直觀,不能直接看到樹結構,不利於開發和調試。
本文介紹的第一種方法我暫稱它為“簡單粗暴多列存儲法”或稱為"朱氏深度樹V1.0法"(如已有人發明過,刪掉頭兩個字就好了),因為下面還有改進版。它與Path-Enumerations模式有點類似,但區別是用很多的數據庫列來存儲一個占位符(1或空值),如下圖(https://github.com/drinkJava2/Multiple-Columns-Tree/blob/master/treemapping.jpg) 左邊的樹結構,映射在數據庫裏的結構見右圖表格:
各種SQL操作如下:
```
1.獲取(或刪除)指定節點下所有子節點,已知節點的行號為"X",列名"cY":
select *(or delete) from tb where
line>=X and line<(select min(line) from tb where line>X and (cY=1 or c(Y-1)=1 or c(Y-2)=1 ... or c1=1))
例如獲取D節點及其所有子節點:
select * from tb where line>=7 and line< (select min(line) from tb where line>7 and (c2=1 or c1=1))
刪除D節點及其所有子節點:
delete from tb where line>=7 and line< (select min(line) from tb where line>7 and (c2=1 or c1=1))
僅獲取D節點的次級所有子節點:
select * from tb where line>=7 and c3=1 and line< (select min(line) from tb where line>7 and (c2=1 or c1=1))
2.查詢指定節點的根節點, 已知節點的行號為"X",列名"cY":
select * from tb where line=(select max(line) from tb where line<=X and c1=1)
例如查I節點的根節點:
select * from tb where line=(select max(line) from tb where line<=12 and c1=1)
3.查詢指定節點的上一級父節點, 已知節點的行號為"X",列名"cY":
select * from tb where line=(select max(line) from tb where line<X and c(Y-1)=1)
例如查L節點的上一級父節點:
select * from tb where line=(select max(line) from tb where line<11 and c3=1)
3.查詢指定節點的所有父節點, 已知節點的行號為"X",列名"cY":
select * from tb where line=(select max(line) from tb where line<X and c(Y-1)=1)
union select * from tb where line=(select max(line) from tb where line<X and c(Y-2)=1)
...
union select * from tb where line=(select max(line) from tb where line<X and c1=1)
例如查I節點的所有父節點:
select * from tb where line=(select max(line) from tb where line<12 and c2=1)
union select * from tb where line=(select max(line) from tb where line<12 and c1=1)
4.插入新節點:
視需求而定,例如在J和K之間插入一個新節點T:
update tb set line=line+1 where line>=10;
insert into tb (line,id,c4) values (10,'T',1)
這是與Path Enumerations模式最大的區別,插入非常方便,只需要利用SQL將後面的所有行號加1即可,無須花很大精力維護path字串,
不容易出錯。
另外如果表非常大,為了避免update tb set line=line+1 造成全表更新,影響性能,可以考慮增加
一個GroupID字段,同一個根節點下的所有節點共用一個GroupID,所有操作均在groupID組內進行,例如插入新節點改為:
update tb set line=line+1 where groupid=2 and line>=8;
insert into tb (groupid,line,c4) values (2, 8,'T')
因為一個groupid下的操作不會影響到其它groupid,對於復雜的增刪改操作甚至可以在內存中完成操作後,一次性刪除整個group的內容
並重新插入一個新group即可。
```
總結:
以上介紹的這種方法優點有:
1)直觀易懂,方便調試,是所有樹結構數據庫方案中唯一所見即所得,能夠直接看到樹的形狀的方案,空值的采用使得樹形結構一目了然。
2)SQL查詢、刪除、插入非常方便,沒有用到Like語法。
3)只需要一張表
4) 兼容所有數據庫
5)占位符即為實際要顯示的內容應出現的地方,方便編寫Grid之類的表格顯示控件
缺點有
1)不是無限深度樹,數據庫最大允許列數有限制,通常最多為1000,這導致了樹的深度不能超過1000,而且考慮到列數過多對性能也有影響, 使用時建議定一個比較小的深度限制例如100。
2)SQL語句比較長,很多時候會出現c9=1 or c8=1 or c7=1 ... or c1=1這種n階乘式的查詢條件
3)樹的節點整體移動操作比較麻煩,需要將整個子樹平移或上下稱動,當節點須要經常移動時,不建議采用這種方案。對於一些只增減,不常移動節點的應用如論壇貼子和評論倒比較合適。
4)列非常多時,空間占用有點大。
##以下為改進版,是建立在前述基礎上,一種更簡單的無限深度樹方案
上面第一種方法比較笨拙,如果不用多列而是只用一個列來存儲一個深度等級數值,則可以不受數據庫列數限制,從而進化為無限深度樹,雖然不再具有所見即所得的效果,但是在性能和簡單性上要遠遠超過上述“簡單粗暴多列存儲法”,暫時給它取名"朱氏深度樹V2.0法"(呵呵如果沒人發明過的話),介紹如下:
如下圖 (https://github.com/drinkjava2/Multiple-Columns-Tree/blob/master/treemappingv2.png) 左邊的樹結構,映射在數據庫裏的結構見右圖表格,註意每個表格的最後一行必須有一個END標記,level設為0:
```
1.獲取指定節點下所有子節點,已知節點的行號為X,level為Y, groupID為Z
select * from tb2 where groupID=Z and
line>=X and line<(select min(line) from tb where line>X and level<=Y and groupID=Z)
例如獲取D節點及其所有子節點:
select * from tb2 where groupID=1 and
line>=7 and line< (select min(line) from tb2 where groupid=1 and line>7 and level<=2)
刪除和獲取相似,只要將sql中select * 換成delete即可。
僅獲取D節點的次級所有子節點:(查詢條件加一個level=Y+1即可):
select * from tb2 where groupID=1 and
line>=7 and level=3 and line< (select min(line) from tb2 where groupid=1 and line>7 and level<=2)
2.查詢任意節點的根節點, 已知節點的groupid為Z
select * from tb2 where groupID=Z and line=1 (或level=1)
3.查詢指定節點的上一級父節點, 已知節點的行號為X,level為Y, groupID為Z
select * from tb2 where groupID=Z and
line=(select max(line) from tb2 where groupID=Z and line<X and level=(Y-1))
例如查L節點的上一級父節點:
select * from tb2 where groupID=1
and line=(select max(line) from tb2 where groupID=1 and line<11 and level=3)
4.查詢指定節點的所有父節點, 已知節點的line=X,level=Y:
select * from tb2 where groupID=Z and
line=(select max(line) from tb2 where groupID=Z and line<X and level=(Y-1))
union select * from tb2 where groupID=Z and
line=(select max(line) from tb2 where groupID=Z and line<X and level=(Y-2))
...
union select * from tb2 where groupID=Z and
line=(select max(line) from tb2 where groupID=Z and line<X and level=1)
例如查I節點的所有父節點:
select * from tb2 where groupID=1 and
line=(select max(line) from tb2 where groupID=1 and line<12 and level=2)
union select * from tb2 where groupID=1 and
line=(select max(line) from tb2 where groupID=1 and line<12 and level=1)
5.插入新節點:例如在J和K之間插入一個新節點T:
update tb2 set line=line+1 where groupID=1 and line>=10;
insert into tb (groupid,line,id,level) values (1,10,'T',4);
```
總結:
此方法優點有:
1) 是無限深度樹
2) 雖然不象第一種方案那樣具有所見即所得的效果,但是依然具有直觀易懂,方便調試的特點。
3) 能充分利用SQL,查詢、刪除、插入非常方便,SQL比第一種方案簡單多了,也沒有用到like模糊查詢語法。
4) 只需要一張表。
5) 兼容所有數據庫。
6) 占用空間小
缺點有:
1)樹的節點整體移動操作有點麻煩, 適用於一些只增減,不常移動節點的場合如論壇貼子和評論等。當確實需要進行復雜的移動節點操作時,一種方案是在內存中進行整個樹的操作並完成排序,操作完成後刪除整個舊group再整體將新group一次性批量插入數據庫。
2017年1月22日補充:
節點的移動操作有點麻煩,只是相對於查詢/刪除/插入來說,並不是說難上天了。例如在mysql下移動整個B節點樹到H節點下,並位於J和K之間的操作如下:
update tb2 set tempno=line*1000000 where groupid=1;
set @nextNodeLine=(select min(line) from tb2 where groupid=1 and line>2 and level<=2);
update tb2 set tempno=9*1000000+line, level=level+2 where groupID=1 and line>=2 and line< @nextNodeLine;
set @mycnt=0;
update tb2 set line=(@mycnt := @mycnt + 1) where groupid=1 order by tempno;
上例需要在表中新增一個名為tempno的整數類型列, 這是個懶人算法,雖然簡單明了,但是對整棵樹進行了重新排序,所以效率並不高。 在需要頻繁移動節點的場合下,用Adjacency List方案可能更合適一些。
Tags:
文章來源: