先看如下一個數據表(t_tree):
上圖顯示了一個表中的數據,這個表有三個字段:id、node_name、parent_id。實際上,這個表中保存了一個樹型結構,分三層:省、市、區。其中id表示當前省、市或區的id號、node_name表示名稱、parent_id表示節點的父節點的id。
現在有一個需求,要查詢出某個省下面的所有市和區(查詢結果包含省)。如果只使用SQL語句來實現,需要使用到游標、臨時表等技術。但在SQL Server2005中還可以使用CTE來實現。
從這個需求來看屬於遞歸調用,也就是說先查出滿足調價的省的記錄,在本例子中的要查“遼寧省”的記錄,如下:
id node_name parent_id
1 遼寧省 0
然後再查所有parent_id字段值為1的記錄,如下:
id node_name parent_id
2 沈陽市 1
3 大連市 1
最後再查parent_id字段值為2或3的記錄,如下:
id node_name parent_id
4 大東區 2
5 沈河區 2
6 鐵西區 2
將上面三個結果集合並起來就是最終結果集。
上述的查詢過程也可以按遞歸的過程進行理解,即先查指定的省的記錄(遼寧省),得到這條記錄後,就有了相應的id值,然後就進入了的遞歸過程,如下圖所示。
從上面可以看出,遞歸的過程就是使用union all合並查詢結果集的過程,也就是相當於下面的遞歸公式:
resultset(n) = resultset(n-1)union allcurrent_resultset
其中resultset(n)表示最終的結果集,resultset(n - 1)表示倒數第二個結果集,current_resultset表示當前查出來的結果集,而最開始查詢出“遼寧省”的記錄集相當於遞歸的初始條件。而遞歸的結束條件是current_resultset為空。下面是這個遞歸過程的偽代碼:
publicresultsetgetResultSet(resultset)
{
if(resultsetisnull)
{
current_resultset=第一個結果集(包含省的記錄集)
將結果集的id保存在集合中
getResultSet(current_resultset)
}
current_resultset=根據id集合中的id值查出當前結果集
if(current_resultisnull)returnresultset
將當前結果集的id保存在集合中
return getResultSet(resultsetunionallcurrent_resultset)
}
//獲得最終結果集
resultset=getResultSet(null)
從上面的過程可以看出,這一遞歸過程實現起來比較復雜,然而CTE為我們提供了簡單的語法來簡化這一過程。
實現遞歸的CTE語法如下:
[WITH<common_table_expression>[,n]]
<common_table_expression>::=
expression_name[(column_name[,n])]
AS(
CTE_query_definition1 -- 定位點成員(也就是初始值或第一個結果集)
unionall
CTE_query_definition2 -- 遞歸成員
)
下面是使用遞歸CTE來獲得“遼寧省”及下面所有市、區的信息的SQL語句:
with
districtas
(
-- 獲得第一個結果集,並更新最終結果集
select*fromt_treewherenode_name=N'遼寧省'
unionall
-- 下面的select語句首先會根據從上一個查詢結果集中獲得的id值來查詢parent_id
-- 字段的值,然後district就會變當前的查詢結果集,並繼續執行下面的select語句
-- 如果結果集不為null,則與最終的查詢結果合並,同時用合並的結果更新最終的查
-- 詢結果;否則停止執行。最後district的結果集就是最終結果集。
selecta.*fromt_treea,districtb
wherea.parent_id=b.id
)
select*fromdistrict
查詢後的結果如下圖所示。
下面的CTE查詢了非葉子節點:
with
districtas
(
select*fromt_treewherenode_name=N'遼寧省'
unionall
selecta.*fromt_treea,districtb
wherea.parent_id=b.id
),
district1as
(
selecta.*fromdistrictawherea.idin(selectparent_idfromdistrict)
)
select*fromdistrict1
查詢結果如下圖所示。
注:只有“遼寧省”和“沈陽市”有下子節點。
在定義和使用遞歸CTE時應注意如下幾點:
1.遞歸 CTE 定義至少必須包含兩個 CTE 查詢定義,一個定位點成員和一個遞歸成員。可以定義多個定位點成員和遞歸成員;但必須將所有定位點成員查詢定義置於第一個遞歸成員定義之前。所有 CTE 查詢定義都是定位點成員,但它們引用 CTE 本身時除外。
2.定位點成員必須與以下集合運算符之一結合使用:UNION ALL、UNION、INTERSECT 或 EXCEPT。在最後一個定位點成員和第一個遞歸成員之間,以及組合多個遞歸成員時,只能使用 UNION ALL 集合運算符。
3.定位點成員和遞歸成員中的列數必須一致。
4.遞歸成員中列的數據類型必須與定位點成員中相應列的數據類型一致。
5.遞歸成員的 FROM 子句只能引用一次 CTE expression_name。
6.在遞歸成員的 CTE_query_definition 中不允許出現下列項:
(1)SELECT DISTINCT
(2)GROUP BY
(3)HAVING
(4)標量聚合
(5)TOP
(6)LEFT、RIGHT、OUTER JOIN(允許出現 INNER JOIN)
(7)子查詢
(8)應用於對 CTE_query_definition 中的 CTE 的遞歸引用的提示。
7.無論參與的 SELECT 語句返回的列的為空性如何,遞歸 CTE 返回的全部列都可以為空。
8.如果遞歸 CTE 組合不正確,可能會導致無限循環。例如,如果遞歸成員查詢定義對父列和子列返回相同的值,則會造成無限循環。可以使用 MAXRECURSION 提示以及在 INSERT、UPDATE、DELETE 或 SELECT 語句的 OPTION 子句中的一個 0 到 32,767 之間的值,來限制特定語句所允許的遞歸級數,以防止出現無限循環。這樣就能夠在解決產生循環的代碼問題之前控制語句的執行。服務器范圍內的默認值是 100。如果指定 0,則沒有限制。每一個語句只能指定一個 MAXRECURSION 值。
9.不能使用包含遞歸公用表表達式的視圖來更新數據。
10.可以使用 CTE 在查詢上定義游標。遞歸 CTE 只允許使用快速只進游標和靜態(快照)游標。如果在遞歸 CTE 中指定了其他游標類型,則該類型將轉換為靜態游標類型。
11.可以在 CTE 中引用遠程服務器中的表。如果在 CTE 的遞歸成員中引用了遠程服務器,那麼將為每個遠程表創建一個假脫機,這樣就可以在本地反復訪問這些表。