Hierarchical Query Using a Recursive CTE: Levels, Order and Loops

A hierarchical query using a recursive CTE walks a table that points at itself, one level at a time. An org chart is the classic case. Each row stores the ID of its boss, and the CTE follows those IDs from the top down. This post builds the levels, sorts the tree, walks upward and stops a loop.

The ten people of a staff table as an indented tree with a level number on each row, from Maya at level 1 down to Ben at level 5, beside the three parts of the recursive CTE: the anchor, the recursive member and the stop

The Sample Table

Ten people in a small garden shop. Maya has no boss, so the BossID of that row is NULL. Everyone else points at one person above them. Run this in any test database. Every result in this post comes from SQL Server 2025.

CREATE TABLE Staff (StaffID INT PRIMARY KEY, StaffName VARCHAR(20) NOT NULL, BossID INT NULL);
GO

INSERT INTO Staff (StaffID, StaffName, BossID) VALUES
(1, 'Maya', NULL), (2, 'Arjun', 1), (3, 'Lena', 1), (4, 'Omar', 2), (5, 'Priya', 2),
(6, 'Noah', 3), (7, 'Iris', 4), (8, 'Sam', 4), (9, 'Tara', 6), (10, 'Ben', 7);
GO

The Four Parts

A CTE is a named result that lives for one statement. A recursive CTE refers to itself, so it can repeat. It needs four parts. An anchor query runs once. A recursive member runs again and again. A UNION ALL joins the two, and the recursive member joins back to the CTE.

Here the anchor is the person with no boss. The recursive member finds the people whose boss is already in the result.

Without the Join, Nothing Repeats

Start with the mistake. This version has an anchor and a second member that never mentions Org, with a fixed 2 for the level.

WITH Org AS (
  SELECT StaffID, StaffName, BossID, 1 AS Lvl FROM Staff WHERE BossID IS NULL
  UNION ALL
  SELECT StaffID, StaffName, BossID, 2 FROM Staff WHERE BossID IS NOT NULL
)
SELECT StaffID, StaffName, Lvl FROM Org;

SQL Server accepts it, but nothing is recursive. The second member runs once, and all 9 other people get level 2, even Ben, who is five levels down.

The Join and Lvl + 1

Join Staff back to Org on the boss column, and add 1 to the level of the boss already found. Each round then lands one level lower.

WITH Org AS (
  SELECT StaffID, StaffName, BossID, 1 AS Lvl FROM Staff WHERE BossID IS NULL
  UNION ALL
  SELECT s.StaffID, s.StaffName, s.BossID, o.Lvl + 1
  FROM Staff AS s INNER JOIN Org AS o ON s.BossID = o.StaffID
)
SELECT StaffID, StaffName, BossID, Lvl FROM Org ORDER BY StaffID;

Now Maya is level 1, and Ben is level 5. The rounds found 1, 2, 3, 3 and 1 people, one level at a time. One more round found nobody, and the recursion stopped.

Table of the ten people with the level from a fixed 2 beside the level from Lvl + 1. Maya, Arjun and Lena match, and the other seven show 2, so Ben shows 2 instead of 5

Sorting the Tree

Sorting by level puts all of level 3 together, not each person under their boss. To fix that, build a path of names in the CTE and sort by it. REPLICATE then indents each name by its level.

WITH Org AS (
  SELECT StaffID, StaffName, 1 AS Lvl, CAST(StaffName AS VARCHAR(200)) AS Path FROM Staff WHERE BossID IS NULL
  UNION ALL
  SELECT s.StaffID, s.StaffName, o.Lvl + 1, CAST(o.Path + '/' + s.StaffName AS VARCHAR(200))
  FROM Staff AS s INNER JOIN Org AS o ON s.BossID = o.StaffID
)
SELECT REPLICATE('    ', Lvl - 1) + StaffName AS Tree, Lvl
FROM Org ORDER BY Path;

Ben appears under Iris, who appears under Omar, under Arjun, under Maya. Lena’s branch follows Arjun’s.

Walking Up Instead

Reverse the join to climb. Start at one person, find their boss, and repeat until there is none.

WITH Chain AS (
  SELECT StaffID, StaffName, BossID, 0 AS Steps FROM Staff WHERE StaffName = 'Ben'
  UNION ALL
  SELECT s.StaffID, s.StaffName, s.BossID, c.Steps + 1
  FROM Staff AS s INNER JOIN Chain AS c ON s.StaffID = c.BossID
)
SELECT Steps, StaffName FROM Chain ORDER BY Steps;

Ben is step 0, then Iris, Omar, Arjun and Maya, in steps 1 to 4. The same pattern lists every manager above one person.

A Loop in the Data

The recursion stops after 100 rounds unless you change it. A loop in the data hits that limit. Make Arjun report to Ben, who works below Arjun, and run the top-down query again.

UPDATE Staff SET BossID = 10 WHERE StaffID = 2;

WITH Org AS (
  SELECT StaffID, StaffName, BossID, 1 AS Lvl FROM Staff WHERE BossID IS NULL
  UNION ALL
  SELECT s.StaffID, s.StaffName, s.BossID, o.Lvl + 1
  FROM Staff AS s INNER JOIN Org AS o ON s.BossID = o.StaffID
)
SELECT StaffID, StaffName, BossID, Lvl FROM Org ORDER BY StaffID;

No error, but only 4 of the 10 people come back. The loop has no top, so the walk from Maya can’t reach it. A walk upward from Ben meets the loop.

WITH Chain AS (
  SELECT StaffID, StaffName, BossID, 0 AS Steps FROM Staff WHERE StaffName = 'Ben'
  UNION ALL
  SELECT s.StaffID, s.StaffName, s.BossID, c.Steps + 1
  FROM Staff AS s INNER JOIN Chain AS c ON s.StaffID = c.BossID
)
SELECT Steps, StaffName FROM Chain ORDER BY Steps;

The error is message 530: the maximum recursion 100 has been exhausted before statement completion. A lower limit stops sooner, with the same message.

WITH Chain AS (
  SELECT StaffID, StaffName, BossID, 0 AS Steps FROM Staff WHERE StaffName = 'Ben'
  UNION ALL
  SELECT s.StaffID, s.StaffName, s.BossID, c.Steps + 1
  FROM Staff AS s INNER JOIN Chain AS c ON s.StaffID = c.BossID
)
SELECT Steps, StaffName FROM Chain ORDER BY Steps
OPTION (MAXRECURSION 20);

The limit only protects you. Fix the data, and the same query returns five rows again.

UPDATE Staff SET BossID = 1 WHERE StaffID = 2;

Two panels. The first walks up from Ben through Iris, Omar and Arjun to Maya in steps 0 to 4. The second shows a loop in the data: walking up from Ben ends with message 530, while walking down from the top returns 4 of 10 people

Clean Up

Remove the sample table when you finish.

DROP TABLE Staff;

What to Remember

A recursive CTE has an anchor, a recursive member, a UNION ALL and a join back to itself. Add 1 to the level in the recursive member. Sort by a path to keep each person under their boss. A loop in the data hits the recursion limit, so fix the data.

To go further, read Recursive CTE Cycles: Detect Loops and Depth Boundaries, Recursive CTE Performance: Index the Child Lookup and Building an Org Chart Query With hierarchyid.

Quick reference card: Hierarchical query using a recursive CTE

A recursive CTE is not a loop you write, it is a question you keep asking until nobody answers.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

CTE, SQL Scripts, SQL Server
Previous Post
Securables and Principals in SQL Server: Who Gets Which Permission
Next Post
Query Hint Basics: Why a Variable Scans and OPTIMIZE FOR Seeks

Related Posts

70 Comments. Leave new

  • Hi, i have a data like this

    id, parent id, child id
    1,1,2
    2,1,3
    3,2,4
    4,2,5
    5,3,6
    6,3,7
    7,8,9
    8,8,10
    9,9,11
    10,9,12
    11,10,13
    12,10,14

    and I need via a query results like
    1|2
    1|2|4
    1|2|5
    1|3
    1|3|6
    1|3|7
    8|9
    8|9|11
    8|9|12
    8|10
    8|10|13
    8|10|14

    Reply
  • Thanks you Pinal, this was very helpful.

    Reply
  • Great article Pinal. I have a question. I want to add a new column called ParentId and update it with top level parent I’d (in the above example, all records except EmpId 11 would have ParentId as 11. Can you please help me to do it. (In my table there are millions of records. )

    Reply
  • How to achieve below with Common Table Expression (CTE).

    Problem –
    ParentCustomerNo ChildCustomerNo
    123456 343432
    343432 888888
    888888 999999
    999999 666666
    777777 111111
    111111 222222
    787878 898989
    898989 656565
    656565 454545

    Result –
    Child Top Parent
    222222 777777
    666666 123456
    999999 123456
    888888 123456
    454545 787878
    656565 787878
    898989 787878

    Please let me know.

    Reply
  • Is it better to use CTE with hierarchical id on the table ?
    Could you build this example with hierarchyid ?

    Reply
  • Thanks for the example. Should it work equally well when multiple records qualify as anchors – e.g. there is no CEO as such but, for example, half a dozen board members at level 0 where each board member oversees a tree structure of employees – senior managers, manager, base employee etc?

    Reply
  • Hi,
    I am facing the same scenario.
    Please let me know if you have found the resolution on the same.

    Reply
  • you are a very best!!!

    Reply
  • How can we show the data in a particular order

    Kelly Smith
    David Kenneson
    Lisa Kenson
    John
    Barabara

    Reply
  • Hi Pinal Thank you for this article. I am looking for the reverse..that is find all recursive children of a parent in the hierarchical data. So in your employee table in the example the recursive children of Sally should be Sally–>David–>Lisa,John,Barbara,Phil
    I want to get these in a table along with Employee Level .
    Sally 1
    David 4
    Lisa 3
    John 3
    Barbara 3
    Phil 3
    Thanks!

    Reply
  • Saurabh Desai
    June 7, 2017 11:11 am

    I’ve tried the exact approach as mentioned in the blog but Inner join is not working as expected. After applying inner join between recursive query and CTE it is only showing result for anchor query. Please help

    Reply
  • Subhrangsu Bhowmik
    August 4, 2017 3:08 pm

    Awesome work done

    Reply
  • Hi, Thanks for a great explanation. I realize that this is an old thread and this has been asked before, but I did not see an answer. How would I then carry down the level 1 manager to all subsequent levels of employees so that I can see whom everyone rolls up to? the assumption being that there are multiple level 1 managers and each has a chain of command.

    Reply
  • Flynthia Knox
    July 3, 2019 2:14 am

    I am using Microsoft SQL Server Management Studio 2014 to export data. Some of the company names in my database have commas in the name. I am trying to export data into a true .csv file that surrounds each column in quotes. The option within SQL Server Management Studio 2014 doesn’t work. Do you have any suggestions to get around that?

    I have also tried to create a .bat file, using the following line, but it is still not capturing the quotes around each column.

    sqlcmd -S servername -d databasename -E -i “SQL_Statement.sql” -o “OutputFile.csv” -s”,” -W

    Any help is greatly appreciated.

    Reply
  • Thank you Pinal. You did simplified CONNECT BY VALUE and START WITH clauses of Oracle into SQL SERVER version.
    I was thinking to use HierachyId::GetRoot() method. Thanks a lot. Pradyumna..

    Reply
  • Hi, Thanks Dave.

    My data has duplicate record in emp id. So when i used above query, i am getting below error. Can you please suggest
    “The statement terminated. The maximum recursion 100 has been exhausted before statement completion”

    Reply
  • Thank You very much for the explaniation!

    Reply
  • TerriblyVexed
    April 29, 2022 6:10 pm

    Thank you very much. As per usual Pinal comes through.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.