TSQL Common Table Expression (CTE) Recursion
Recursion does my head in if I haven’t used it for a while. The following SQL is a typical employee/manager scenario, but I have used meaningful names to try and make it clear what is going on. The SQL returns 2 sets of data. The first is sample data before the recursion is applied. The second is the result of the recursion. I have also included where clauses to protect against infinite loops.
--------------------------------------------------------------------
--Set up a table of data
--------------------------------------------------------------------
SELECT *
INTO #Employee
FROM
(
SELECT 'King' AS EmployeeName, NULL AS ManagerName
UNION ALL
SELECT 'Queen' AS EmployeeName, 'King' AS ManagerName
UNION ALL
SELECT 'Jack' AS EmployeeName, 'Queen' AS ManagerName
UNION ALL
SELECT 'Ten' AS EmployeeName, 'Jack' AS ManagerName
UNION ALL
SELECT 'Nine' AS EmployeeName, 'Ten' AS ManagerName
UNION ALL
SELECT 'Eight' AS EmployeeName, 'Ten' AS ManagerName
UNION ALL
SELECT 'Seven' AS EmployeeName, 'Ten' AS ManagerName
) t
--Return the sample data before we start the recursion
SELECT * FROM #Employee;
--------------------------------------------------------------------
--Do the recursion
--------------------------------------------------------------------
WITH cte
AS
(
--The Anchor (Parent, Manager etc)
SELECT e.EmployeeName
,e.ManagerName
,0 AS [Level] -- Useful as it increments for each level making the result easier to read
,CAST(ISNULL(e.ManagerName,'') AS VARCHAR(MAX)) AS TreePath --again makes the result easier to read
FROM #Employee e
--Restricts the iteration to one record. To iterate through every record in the anchor comment this where clause out
WHERE EmployeeName = 'King'
UNION ALL
--The recursive bit
SELECT e.EmployeeName
,e.ManagerName
,[Level] + 1 AS [Level]
--Tree path (opposite direction commented out...try swapping these around)
,CAST(ISNULL(e.ManagerName,'') + ' -> ' + cte.TreePath AS VARCHAR(MAX)) AS TreePath
--,CAST(cte.TreePath + ' -> ' + IsNull(e.ManagerName,'') AS VARCHAR(max)) AS TreePath
FROM cte cte
INNER JOIN #Employee e
ON cte.EmployeeName = e.ManagerName
--Some extra where clauses to get rid of troublesome data
WHERE --My Boss should not be able to report to me. However, if the data is incorrect, don't follow the recursive path, but do return me for my boss and vice versa (stop after level 0)
(
ISNULL(cte.ManagerName,'') != ISNULL(e.EmployeeName,'')
OR cte.[Level] = 0
)
--I should not be able to report to myself. However, if the data is incorrect then we need to show the record once only so that the underlying hiearchy is properly displayed without following the recursive path
AND (
ISNULL(cte.ManagerName,'') != ISNULL(e.ManagerName,'')
OR cte.[Level] = 0
)
)
--------------------------------------------------------------------
--Return the result
--------------------------------------------------------------------
SELECT EmployeeName
,[Level]
,[TreePath]
FROM CTE
--The recordset returned includes the anchor record - we may or may not want this
--WHERE EmployeeName != 'King'
--------------------------------------------------------------------
--Tidy up
--------------------------------------------------------------------
DROP TABLE #Employee