WITH CTE_Name (Col1, Col2, ...) AS (
SELECT ... -- 定位點成員
UNION ALL
SELECT ... -- 遞歸成員 (引用 CTE_Name)
)
SELECT * FROM CTE_Name;
CREATE TABLE Menu (
Name NVARCHAR(50) NOT NULL PRIMARY KEY,
Parent NVARCHAR(50) NULL
);
INSERT INTO Menu VALUES
(N'檔案', NULL),
(N'新增', N'檔案'),
(N'專案', N'新增'),
(N'使用目前連線查詢', N'新增');
檔案
└─ 新增
├─ 專案
└─ 使用目前連線查詢
WITH LMenu (Name, Parent, Level) AS (
-- 定位點:根節點 (Parent IS NULL)
SELECT Name, Parent, 0
FROM Menu
WHERE Parent IS NULL
UNION ALL
-- 遞歸成員:子節點連接父節點
SELECT M.Name, M.Parent, L.Level + 1
FROM Menu M
INNER JOIN LMenu L ON M.Parent = L.Name
)
SELECT * FROM LMenu
OPTION (MAXRECURSION 0);
Name Parent Level 檔案 NULL 0 新增 檔案 1 專案 新增 2 使用目前連線查詢 新增 2
SQL Server 對 **外層查詢 `SELECT * FROM LMenu` 的每一行輸入**,獨立執行遞歸展開:
等價於 **巢狀子查詢展開**:
-- 第 3 行實際展開邏輯
SELECT M.Name, M.Parent, L2.Level + 1
FROM Menu M
INNER JOIN (
SELECT M2.Name, M2.Parent, L1.Level + 1 AS Level
FROM Menu M2
INNER JOIN (
SELECT Name, Parent, 0 AS Level
FROM Menu
WHERE Parent IS NULL AND Name = N'檔案'
) L1 ON M2.Parent = L1.Name
WHERE M2.Parent = N'檔案' AND M2.Name = N'新增'
) L2 ON M.Parent = L2.Name
WHERE M.Parent = N'新增' AND M.Name = N'專案';
WITH Tree AS (
SELECT Name, Parent, CAST(Name AS NVARCHAR(MAX)) AS Path, 0 AS Level
FROM Menu WHERE Parent IS NULL
UNION ALL
SELECT M.Name, M.Parent, T.Path + N' > ' + M.Name, T.Level + 1
FROM Menu M INNER JOIN Tree T ON M.Parent = T.Name
)
SELECT * FROM Tree ORDER BY Path;
WITH SubTree AS (
SELECT Name, Parent FROM Menu WHERE Name = N'新增'
UNION ALL
SELECT M.Name, M.Parent FROM Menu M INNER JOIN SubTree S ON M.Parent = S.Name
)
SELECT * FROM SubTree;
WITH Ancestors AS (
SELECT Name, Parent FROM Menu WHERE Name = N'專案'
UNION ALL
SELECT M.Name, M.Parent FROM Menu M INNER JOIN Ancestors A ON M.Name = A.Parent
)
SELECT * FROM Ancestors;
-- 刪除節點及其所有後代
WITH ToDelete AS (
SELECT Name FROM Menu WHERE Name = N'新增'
UNION ALL
SELECT M.Name FROM Menu M INNER JOIN ToDelete T ON M.Parent = T.Name
)
DELETE FROM Menu WHERE Name IN (SELECT Name FROM ToDelete);
| 問題 | 原因 | 對策 |
|---|---|---|
| 遞歸無限循環 | 資料有循環 (A→B→A) | 加入 `Level` 欄位限制深度;`OPTION (MAXRECURSION 100)`;資料層面加 `CHECK` 約束防循環 |
| 大量資料效能差 | 逐行獨立遞歸、缺乏索引 | **Parent 欄位建立非叢集索引**;考慮 `HierarchyID` (SQL 2008+) 或 `Path Enumeration` 物化路徑 |
| 結果集排序混亂 | 遞歸輸出順序非預期 | 外層 `ORDER BY Path` 或 `Level, Name`;或用 `ROW_NUMBER()` 排序 |
| MAXRECURSION 0 風險 | 誤寫導致無限遞歸佔滿記錄檔/TempDB | 測試環境驗證後再上線;生產設定合理上限 (如 1000) |
ALTER TABLE Menu ADD Hid HIERARCHYID;
-- 根節點
UPDATE Menu SET Hid = HIERARCHYID::GetRoot() WHERE Parent IS NULL;
-- 子節點
UPDATE Menu SET Hid = ParentHid.GetDescendant(NULL, NULL)
FROM Menu M JOIN Menu P ON M.Parent = P.Name
WHERE M.Parent IS NOT NULL;
-- 查詢後代
SELECT * FROM Menu WHERE Hid.IsDescendantOf(@ParentHid) = 1;
-- 查詢祖先
SELECT * FROM Menu WHERE @NodeHid.IsDescendantOf(Hid) = 1;
-- 取得層級
SELECT Hid.GetLevel() FROM Menu;
優勢: 索引支援、位元組比較極快、內建 `GetAncestor`/`GetDescendant`/`GetLevel`/`IsDescendantOf`、`ToString`/`Parse` 序列化,**大幅優於遞歸 CTE**。
CTE 遞歸查詢是 SQL Server 2005+ 處理階層/圖資料的標準工具,語法宣告式、可讀性強。掌握 **定位點/遞歸成員結構、逐行獨立展開機制、限制條件、MAXRECURSION 控制**,即可解決 90% 樹狀查詢需求。對於高頻、大量、深層級的階層資料,**`HierarchyID` 或物化路徑 (Path Enumeration) 是效能優化的終極選擇**。