樹狀結構的儲存與管理,是每乙個在關係型資料庫平台上工作的程式設計師早晚都要遇到的問題。說大不大,怎麼都能解決,說小不小,處理不好,有的是麻煩等著你。仁者見仁,智者見智,公說公有理,婆說婆有理(誰用機箱砸我?機箱是個好東西,亂丟會摔壞硬碟的,你看我話沒說完你又把顯示器丟了……),咳咳,好吧,閒話少說,我們從最大路的處理風格談一談吧。這裡面的大部分內容並非我的獨創,從很久很久以前,資料庫程式設計師們就這樣做啦。<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
樹狀表的結構化表達
在一切開始前,我們先就樹狀表的表示方式達成乙個共識。在關係型資料庫中,我們當然沒有辦法這樣直接表示乙個樹:
ab c
d e f g
相應的,我們會把它變形為平面表,這種變形讓我想起拓撲幾何:
r n1 n2
a b d
a b e
a c f
a c g
儲存結構
稍有經驗的朋友,大概都不會試著把樹狀結構一層一列的存進去了吧。這樣做的問題是顯而易見的:與表中儲存的資訊結構不同,表的結構應該是相對固定的,不能隨便改動。而對於層數不能固定的普通樹狀結構,這是不可思議的。沒有必要討論過多的錯誤,我們選擇乙個相對正確的方式――把樹狀結構抽象成適合關聯式資料庫的形式。
只考慮某乙個節點的話,和這個節點相關的資訊是:它的唯一標識、父節點、子節點、資料資訊。這其中只有子節點的數目不定。不過如果每乙個節點都確定了自己的父節點,顯然可以省略子節點。這樣一來,乙個節點需要儲存三部分資訊――它的唯一標識、父節點、資料資訊。乙個理想的treeview表只需要三個節點就可以了,用sql語句來表達就是
create table [dbo].[treeview] (
[id] [char] (10) primary key,
[pid] [char] (10) foreign key references [dbo].[treeview],
[data] [char] (10)
)id是當前節點的唯一標識號,顯然它應當是主鍵;我們建立的是乙個自閉的儲存結構,每乙個節點的父節點也應當出自表中儲存的節點,所以pid列引用id作為外來鍵;至於data,它是節點中的資訊,通常和樹的結構沒有絕對關係。我把這三列全設成char(10),是為了後面做演示更方便,當然也有人喜歡用自動標識列來做主鍵,在這種場合,也自有其優點。為了維護資料的完整性或儲存、檢索等方面的考慮,實用中我們可能會採用更複雜的結構,不過骨幹就這樣子了。這個結構從數學上講很簡潔,而且是自洽的。如果乙個節點沒有父節點,那麼它的pid就等於它自己的id。這並不違反我們關於主外來鍵的定義。
資訊的管理與使用
樹表的結構確定後,問題就集中在如何讀寫其中的資料。
增加節點:增加乙個葉節點很簡單,只要指定這個節點的父節點,把它「掛接」到treeview中。從遞迴的角度來看,我們可以重複這一步驟,真到插入乙個完整的子樹。相對而言,比較麻煩的是,如何把乙個子節點插入到現有的樹中間,而不是最末端。比如存在乙個節點n,它的根為r,現在要在r和n之間插入乙個新節點n』,我們可以這樣做:把n』掛在r下面,作為它的子節點,然後把n的父節點指定為n』即可。
修改節點:這裡指修改樹結構,改變某一節點的父節點,在這種結構中,修改是很方便的,只要呼叫標準的update就可以。
刪除節點:刪除節點時要注意這個節點下面還有沒有子節點,如果有,我們通常以兩種方式處理,一是把相關子節點全刪掉,如果是ms sql server 2000這樣的系統,你可以很簡單的在建表時將外來鍵約束指定為支援級聯刪除,自己寫乙個級聯刪除比較麻煩,不過也不是不可能,重點在於,為這個過程建立遞迴。簡要示例如下:
--定義儲存過程
create procedure deletenode
@nodeid char(10)
asbegin
--以當前節點的子節點作為記錄集建立游標
declare childnodes cursor
read_only
for select id from treeview where pid = @nodeid
declare @childnode varchar(40)
open childnodes
fetch next from childnodes into @childnode
while (@@fetch_status <> -1)--判斷記錄集是否成功開啟
begin
if (@@fetch_status <> -2)
begin
--遞迴呼叫
exec deletenode @childnode
endfetch next from childnodes into @childnode
endclose childnodes
deallocate childnodes
--**執行到這裡,可以確定@nodeid不再有子節點,現在,我們刪除它
delete from treeview where id = @nodeid
end;
當然,這是一種比較低效的設計,每乙個將要刪除的節點都要執行一次delete,比較有效率的方法是多深入一層,操作當前節點的子節點。有興趣的讀者可以一試。
查詢:樹狀表的查詢是最有趣的內容之一。當然僅僅查出某乙個節點的資訊沒有太大的意思,我們希望的是得到從當前節點一直向下(通常是到最底層)的完整子樹。這同樣需要乙個遞迴。讓我們從乙個歸納法遊戲開始,事先,我們先在treeview中插入以下資料: id
piddata
----------
----------
----------
root
root
root
n11root
node1-1
n12root
node1-2
n13root
node1-3
n21n11
node2-1
n22n11
node2-2
n23n12
node2-3
n24n13
node2-4
n31n21
node3-1
n32n21
node3-2
n33n21
node3-3
n34n22
node3-4
歸納法第一步,當然是構造初值,查詢子樹的根節點就是標準的sql,select id, data form treeview where id = …,在這裡有乙個細節,查詢表中所有的根節點(細心的讀者可能早就發現,我們設計的這個treeview可以儲存任意多個樹)可以通過select r.id, r.data form treeview r where r.id = r.pid來實現。簡單起見,我們就從這裡起步吧。
第二步,選擇出前兩層也不難,
select r.id, n1.id, n1.data
from treeview r
left join treeview n1
on r.id = n1.pid
and r.id <> n1.id
where r.id = r.pid
我們要注意的是加藍的部分,這是增加一層後做改動的部分。
很容易,我們得到前三層,
select r.id, n1.id,
n2.id, n2.data
from treeview r
left join treeview n1
on r.id = n1.pid
and r.id <> n1.id
left join treeview n2
on n1.id = n2.pid
and n1.id <> n2.id
where r.id = r.pid
同樣的,我們重點觀察加藍的部分。相信經過這兩步,大家會發現其中的規律。現在我們可以把這個過程自動化了…… ……
不知道大家是否還記得前幾集裡那個關於四色問題的笑話,我又一次犯了這樣的錯誤。乙個多月以來,我就陷在這裡不能自拔。顯然,我低估了問題的難度。這個問題似乎不能轉化為乙個簡單表示式,只能通過乙個遞迴的流程來實現。而流程化的程式又非sql所長。這裡面有著各種各樣的麻煩。相信試過的朋友都有體會。現在我使用的架構問題是顯然的,它需要明確知道到底從子樹的頂點向下到底有多少層。所以,計算樹的層數就首先被提了出來。
但是,但是,但是……(我簡直沒臉見人啦)我一直找不到乙個簡潔優雅的方法。乙個多月的時間就這麼過去了。我不得不一點點放棄我的原則(此乃人生墮落之始啊)。最後,我得承認,要想寫出乙個優雅的樹狀表選擇來,對我還是比較困難的。所以,在這一篇文章裡,我們先討論到這裡,樹狀表的選擇――其實應該說樹狀檢視構造,還是留到下次再討論吧。
SQLStory摘錄(五) 關係真相
長期以來,我們習慣了稱關係型中的表為二維表。因為它有行和列,很容易我們就可以把它同乙個二維平面聯絡起來,但事實上,這並非關係型資料庫的初衷,也並非符合關係模型的。其實長久以來,我對此也只有乙個很模糊的概念,對平面表的觀點雖有懷疑,卻一直無從驗證。直到有一天,翻出一本老書 關聯式資料庫 石樹剛 鄭振楣...
《SQL Story》摘錄五 關係真相
關係的真相 長期以來,我們習慣了稱關係型資料庫中的表為二維表。因為它有行和列,很容易我們就可以把它同乙個二維平面聯絡起來,但事實上,這並非關係型資料庫的初衷,也並非符合關係模型的設計。其實長久以來,我對此也只有乙個很模糊的概念,對平面表的觀點雖有懷疑,卻一直無從驗證。直到有一天,翻出一本老書 關聯式...
u boot分析(十一)
u boot 分析 十一 通過前面十篇博文,我們已經完成了對bl1階段的分析,通過這些分析相信我們對u boot已經有了乙個比較深入的認識,在bl2階段大部分是對外設的初始化,並且有的我們已經分析過,在這篇博文我打算對bl1階段沒有分析到的重要外設進行簡單分析,並結束對u boot的分析,同時對後面...