Traversing a Recursive Table Self Reference Using T-SQL

Randy Evans here…I am a Principal Architect on the Information Security Tools team.  During the Risk Tracker project (something well be talking more about in the coming weeks), we found that a natural hierarchy of roles was inherent in the RBA authorization design.  For example, the “Task User” role has a set of permissions and the “Task Manager” role requires all the same permissions of a “Task User” plus some additional permissions.  The “Risk Manager” role requires all the same permissions of a “Task Manager” plus some additional permissions and so on.  To model this hierarchy we added a recursive table self reference to the Role table by adding a ParentRoleID column.

 CREATE TABLE [dbo].[Role](
[RoleID] [int] IDENTITY(1,1) NOT NULL,
[RoleName] [nvarchar](50) NOT NULL,
[ParentRoleID] [int] NULL)

Traversing a recursive table self reference can be a little confusing to code using T-SQL; especially if you don’t have a defined number of recursions.  Below is the approach we used to find all descendants of any particular role using T-SQL.  The first “insert into” primes the temp table by finding all child roles with a ParentRoleID matching the top level role that we’re interested in.  The second “insert into” populates the temp table with all remaining descendants by looping until no more records are selected.  The second query looks for all children with ParentRoleIDs that match any RoleID in the temp table.  It uses a correlated subquery to prevent previously selected roles from being selected again.  The final query simply returns all roles found in the temp table.  This pattern will work regardless of the number of levels that exist in the role hierarchy.

 DELCARE @role as nvarchar(50) = 'Risk Manager'
     
CREATE TABLE [dbo].[#TopRoles] (RoleID int, ParentRoleID int)

/*Get the immediate children of the passed in role. */ 
 INSERT INTO [dbo].[#TopRoles]
SELECT child.RoleID
, child.ParentRoleID
FROM [dbo].[Role] parent
JOIN [dbo].[Role] child
ON   child.ParentRoleID = parent.RoleID
AND  parent.RoleName = @role

WHILE @@ROWCOUNT > 0
BEGIN

/*Get the children's children. */ 
     INSERT INTO [dbo].[#TopRoles]
   SELECT child.RoleID
 , child.ParentRoleID
    FROM [dbo].[#TopRoles] parent
   JOIN [dbo].[Role] child
 ON   child.ParentRoleID = parent.RoleID  --Gets the children
    AND  not exists (           --Don't return rows that already exist.
 SELECT parent2.RoleID 
  FROM [dbo].[#TopRoles] parent2
  WHERE parent2.RoleID = child.RoleID)
END

SELECT 
r.RoleName
FROM [dbo].[#TopRoles] tr
JOIN [dbo].[Role] r
ON   r.RoleID = tr.RoleID

Hope you find this useful if you are faced solving similar problems!