本文深入研究如何使用CTE完成圖的深度遍歷,並查找最短路徑。本文假設你在爲一家航空公司做一個導遊的軟件,其中的一個基本需求是查詢從一個城市到另一個城市的飛行路線。並假設除了軟件所已知的航班之外,並不存在其他的直達航班。首先你讓用戶看到這樣的圖片:
在後臺你需要創建的表結構可以簡化如下:
CREATE TABLE dbo.Flights
(
city1 NVARCHAR(256) NOT NULL ,
city2 NVARCHAR(256) NOT NULL ,
distance INT NOT NULL,
PRIMARY KEY(city1, city2),
CHECK(city1 < city2),
CHECK(distance > 0)
);
表中存放如下內容:
BeiJing | TaiYuan | 10 |
Beijing | ZhenZhou | 15 |
TaiYuan | XiAn | 12 |
TaiYuan | YingChuan | 14 |
TaiYuan | ZhenZhou | 8 |
WuHan | XiAn | 13 |
WuHan | ZhenZhou | 10 |
XiAn | YingChuan | 13 |
XiAn | ZhenZhou | 8 |
那麼你現在希望找到各個城市間所有的飛行途徑以及相應的路程。爲此我們需要增加1個新的列:
- Path nvarchar(1024) 存放所有經過的城市;
我們不再需要增加Distance列,因爲新增加的行自動包含原有的Distance列。
好吧,讓我來看SQL吧。
with AllFlights as
(
select city1,city2,distance from flights
union
select city2,city1,distance from flights
),
CompleteFlights as
(
select city1,city2,distance,cast(city1+'.' as nvarchar(1024)) as [path] from AllFlights
union all
select
a.city1,
b.city2,
a.distance+b.distance,
cast([path]+a.city2+'.' as nvarchar(1024))
from CompleteFlights a join AllFlights b
on a.city2 = b.city1 and charindex(b.city1,[path])=0 and charindex(b.city2,[path])=0
)
select * from CompleteFlights
直接打印出所有結果是不明智,它不能幫助我們理解發生的一切。讓我們還是從SQL本身入手來理解它。
首先,我們使用了兩個CTE。第一個CTE是源表的擴張,既然你可以從西安到銀川,自然也能從銀川到西安。可是源表爲了減少冗餘,並沒有銀川到西安的路徑。
接下來是正宗的CTE了。請回憶我的上一篇文章講的CTE的結構: 錨點,連接詞,遞歸生成語句。
現在 錨點 就是第一步產生的CTE,它包含所有可以一步到達的航班信息。
連接詞是UnionAll,因爲我們要吧所有的路徑組合起來。
遞歸語句是我們這個解法的核心。
首先,仍然是 i 輪結果產生 i+1 輪的結果。i 輪的結果,自然是 源 和 目的 城市間轉機次數爲 i 的記錄。 特殊的,錨點的結果是轉機次數爲0的路徑記錄。
請注意到當轉機次數增加1時,只是從原始路徑AllFlights中選擇,並且有兩個額外的限制條件: charindex(b.city2,[path])=0
對新加入的路徑如果終點出現在路徑中,加入該邊後必然構成一個環,那不是一個正常的結果。 我們可以推斷,B.City1必然不在路徑中。
當新的路徑形成後,我們相應的修改Distance和Path變量。
好吧,讓我們看看結果吧:爲方便,我們把之前的SQL作爲一個View, FlightView
請找出北京到太原的所有路徑:select * from FlightView where city1='BeiJing' and city2='TaiYuan'
下面是結果:
BeiJing | TaiYuan | 10 | BeiJing. |
Beijing | TaiYuan | 23 | Beijing.ZhenZhou. |
Beijing | TaiYuan | 35 | Beijing.ZhenZhou.XiAn. |
Beijing | TaiYuan | 50 | Beijing.ZhenZhou.XiAn.YingChuan. |
Beijing | TaiYuan | 50 | Beijing.ZhenZhou.WuHan.XiAn. |
Beijing | TaiYuan | 65 | Beijing.ZhenZhou.WuHan.XiAn.YingChuan. |
請找出北京到西安的最短路徑:
select city1,city2,distance,[path]
from CompleteFlights
where city1='BeiJing' and city2='xian' and
distance = (select min(distance) from CompleteFlights where city1='BeiJing' and city2='xian')
結果如下:
BeiJing | XiAn | 22 | BeiJing.TaiYuan. |
從圖上看,這顯然是正確的結果。