Recursive CTEs: hierarchies and bills of materials
A CTE that refers to itself: the anchor and recursive members, generating numbers and dates, walking down and up a management hierarchy, exploding a bill of materials, and MAXRECURSION.
UNION ALL: an anchor query that produces the starting rows, and a recursive query that produces new rows from the rows of the previous step. It repeats until a step produces no rows.
WITH Numbers AS ( SELECT 1 AS n -- anchor: the first row UNION ALL SELECT n + 1 -- recursive: one new row from the last one FROM Numbers WHERE n < 5 -- stops when this is false)SELECT n FROM Numbers;n - 1 2 3 4 5
| Step | Input rows | Output rows |
|---|---|---|
Filling in missing months
WITH Months AS ( SELECT CAST('2024-01-01' AS date) AS MonthStart UNION ALL SELECT DATEADD(month, 1, MonthStart) FROM Months WHERE MonthStart < '2024-06-01')SELECT m.MonthStart, COUNT(o.OrderId) AS OrdersFROM Months AS mLEFT JOIN dbo.Orders AS o ON o.OrderDate >= m.MonthStart AND o.OrderDate < DATEADD(month, 1, m.MonthStart)GROUP BY m.MonthStartORDER BY m.MonthStart;Warning: Null value is eliminated by an aggregate or other SET operation. MonthStart Orders ---------- ------ 2024-01-01 0 2024-02-01 2 2024-03-01 2 2024-04-01 2 2024-05-01 2 2024-06-01 2
Walking down a hierarchy
CREATE TABLE dbo.Staff ( StaffId int NOT NULL PRIMARY KEY, Name nvarchar(40) NOT NULL, JobTitle nvarchar(40) NOT NULL, ManagerId int NULL REFERENCES dbo.Staff (StaffId));INSERT INTO dbo.Staff VALUES (1, N'Maya', N'Managing director', NULL), (2, N'Omar', N'Store manager', 1), (3, N'Lena', N'Finance manager', 1), (4, N'Tom', N'Yard supervisor', 2), (5, N'Priya', N'Sales lead', 2), (6, N'Sam', N'Forklift driver', 4), (7, N'Kofi', N'Sales assistant', 5), (8, N'Ruth', N'Accounts clerk', 3);WITH Org AS ( SELECT StaffId, Name, JobTitle, 0 AS Lvl, CAST(Name AS nvarchar(200)) AS Path FROM dbo.Staff WHERE ManagerId IS NULL UNION ALL SELECT s.StaffId, s.Name, s.JobTitle, o.Lvl + 1, CAST(o.Path + N' > ' + s.Name AS nvarchar(200)) FROM dbo.Staff AS s JOIN Org AS o ON s.ManagerId = o.StaffId)SELECT REPLICATE(N' ', Lvl) + Name AS Person, JobTitle, Lvl, PathFROM OrgORDER BY Path;Person JobTitle Lvl Path
---------------- ----------------- --- --------------------------
Maya Managing director 0 Maya
Lena Finance manager 1 Maya > Lena
Ruth Accounts clerk 2 Maya > Lena > Ruth
Omar Store manager 1 Maya > Omar
Priya Sales lead 2 Maya > Omar > Priya
Kofi Sales assistant 3 Maya > Omar > Priya > Kofi
Tom Yard supervisor 2 Maya > Omar > Tom
Sam Forklift driver 3 Maya > Omar > Tom > SamLvl counts the steps from the top: the anchor is 0, and each recursive step adds 1. Path builds the chain of names. Sorting by it puts everyone under their manager, and REPLICATE indents them by level. The CAST to nvarchar(200) matters: the anchor and recursive parts must have exactly the same type, and Name alone is only nvarchar(40).
Walking up
WITH Chain AS ( SELECT StaffId, Name, ManagerId, 0 AS StepsUp FROM dbo.Staff WHERE Name = N'Kofi' UNION ALL SELECT m.StaffId, m.Name, m.ManagerId, c.StepsUp + 1 FROM dbo.Staff AS m JOIN Chain AS c ON m.StaffId = c.ManagerId)SELECT StepsUp, Name FROM Chain;StepsUp Name
------- -----
0 Kofi
1 Priya
2 Omar
3 MayaExploding a bill of materials
CREATE TABLE dbo.Parts ( PartId int NOT NULL PRIMARY KEY, Name nvarchar(40) NOT NULL, ProductId int NULL REFERENCES dbo.Products (ProductId) -- set for shelf items);CREATE TABLE dbo.Bom ( ParentId int NOT NULL REFERENCES dbo.Parts (PartId), ChildId int NOT NULL REFERENCES dbo.Parts (PartId), Qty int NOT NULL, PRIMARY KEY (ParentId, ChildId));INSERT INTO dbo.Parts VALUES (1, N'Garden shed kit', NULL), (2, N'Wall panel', NULL), (3, N'Roof panel', NULL), (4, N'Floor', NULL), (5, N'Timber 2x4 2.4 m', 6), (6, N'Plywood sheet 18 mm', 7), (7, N'Wood screws (200)', 8);INSERT INTO dbo.Bom VALUES (1, 2, 4), (1, 3, 2), (1, 4, 1), (1, 7, 3), (2, 5, 5), (2, 6, 1), (3, 5, 3), (3, 6, 1), (4, 5, 6), (4, 6, 2);WITH Explode AS ( SELECT b.ParentId, b.ChildId, b.Qty AS TotalQty, 1 AS Lvl FROM dbo.Bom AS b WHERE b.ParentId = 1 UNION ALL SELECT b.ParentId, b.ChildId, e.TotalQty * b.Qty, e.Lvl + 1 FROM dbo.Bom AS b JOIN Explode AS e ON b.ParentId = e.ChildId)SELECT e.Lvl, par.Name AS PartOf, pt.Name, e.TotalQtyFROM Explode AS eJOIN dbo.Parts AS pt ON pt.PartId = e.ChildIdJOIN dbo.Parts AS par ON par.PartId = e.ParentIdORDER BY e.Lvl, par.Name, pt.Name;Lvl PartOf Name TotalQty --- --------------- ------------------- -------- 1 Garden shed kit Floor 1 1 Garden shed kit Roof panel 2 1 Garden shed kit Wall panel 4 1 Garden shed kit Wood screws (200) 3 2 Floor Plywood sheet 18 mm 2 2 Floor Timber 2x4 2.4 m 6 2 Roof panel Plywood sheet 18 mm 2 2 Roof panel Timber 2x4 2.4 m 6 2 Wall panel Plywood sheet 18 mm 4 2 Wall panel Timber 2x4 2.4 m 20
WITH Explode AS ( SELECT b.ChildId, b.Qty AS TotalQty FROM dbo.Bom AS b WHERE b.ParentId = 1 UNION ALL SELECT b.ChildId, e.TotalQty * b.Qty FROM dbo.Bom AS b JOIN Explode AS e ON b.ParentId = e.ChildId)SELECT pr.Name, SUM(e.TotalQty) AS Qty, pr.Price, SUM(e.TotalQty) * pr.Price AS CostFROM Explode AS eJOIN dbo.Parts AS pt ON pt.PartId = e.ChildIdJOIN dbo.Products AS pr ON pr.ProductId = pt.ProductIdGROUP BY pr.Name, pr.PriceORDER BY Cost DESC;Name Qty Price Cost ------------------- --- ----- ------ Plywood sheet 18 mm 8 42.00 336.00 Timber 2x4 2.4 m 32 6.80 217.60 Wood screws (200) 3 11.90 35.70
MAXRECURSION: the safety net
WITH Numbers AS ( SELECT 1 AS n UNION ALL SELECT n + 1 FROM Numbers WHERE n < 200)SELECT MAX(n) AS Highest FROM Numbers;Msg 530, Level 16, State 1, Line 1 The statement terminated. The maximum recursion 100 has been exhausted before statement completion.
OPTION (MAXRECURSION n), up to 32767, or 0 for no limit:
WITH Numbers AS ( SELECT 1 AS n UNION ALL SELECT n + 1 FROM Numbers WHERE n < 200)SELECT MAX(n) AS Highest FROM NumbersOPTION (MAXRECURSION 200);Highest
-------
200Show a hint
Show the solution
WITH UnderOmar AS ( SELECT StaffId, Name, 1 AS LevelsBelow FROM dbo.Staff WHERE ManagerId = 2 UNION ALL SELECT s.StaffId, s.Name, u.LevelsBelow + 1 FROM dbo.Staff AS s JOIN UnderOmar AS u ON s.ManagerId = u.StaffId)SELECT Name, LevelsBelowFROM UnderOmarORDER BY LevelsBelow, Name;Name LevelsBelow ----- ----------- Priya 1 Tom 1 Kofi 2 Sam 2
Remember
A recursive CTE is an anchor query UNION ALL a recursive query that refers to the CTE. Each step works on the rows produced by the previous step; it stops when a step produces none. Generate numbers and dates, then LEFT JOIN data to fill in gaps. Walk down a hierarchy by joining children to the previous level; walk up by joining managers. A bill of materials multiplies quantities on the way down. Anchor and recursive columns must have exactly the same types. MAXRECURSION (default 100) stops runaway recursion; raise it with OPTION (MAXRECURSION n).