Search code examples
sqlsql-serverrecursion

SQL Server recursion with multiple tables


I'm trying to get users that are part of groups, but groups can be members of groups as well. So the question is about who has "effective" access to certain groups. I've seen many examples of using CTE on SQL Server and this seems like the same case, but all the examples are pretty simple about a single table like an org chart. In my case, I have to join multiple tables and just can't grasp the whole recursion thing.

The tables are:

usergroup: what users have which groups

uid int
gid int

groupgroup: which groups give which groups

parentid int
childid int

group: many groups are in a single sourceid.

gid int
sourceid int
name varchar

For example:

usergroup

uid   gid
------------
1     101
1     102
2     102
2     120

groupgroup

parentid  childid
-------------------
103       101
110       103
120       110

group

gid   sourceid    name
-----------------------
101   5           group101
102   5           group102
103   5           group103
110   8           group110
120   8           group120

Where the difference between parentid and childid is that parentid GRANTS childid. So if you are a member of parentid , you have effective access to childid. As expected, parentid and childid WILL EXIST in the group table.

My goal is to simply get a list of all users that have access to which groups in a given source (sourceid). Without the nesting, it would be simple for sourceid of 5:

select *
from usergroup
inner join group on group.gid = usergroup.gid
where group.sourceid = 5

And this query would give me that user 1 has groups 101, 102, and user 2 has 102.

But the issue is that a user could have a parent gid that gives a childid via any level of nesting, and those childids can be a gid in the sourceid of 5.

I would instead hope to get that user 1 still has 101 and 102 directly, and that user 2 has 102 directly AND 101 and 103 because of nesting.

At a basic level, I just need to know users XYS have groups ABC in the source either directly OR via nesting and a flag or other marker if it's nesting vs direct. If we could get something like a level (0 = direct, 1 = one level up, etc...) that's all the better.

I can do this in Java code by making a big map/list and looping, but is there an elegant way to get it from SQL Server directly and perhaps faster than in code?

Thank you!


Solution

  • A recursive CTE consists of an anchor query followed by a UNION ALL to one or more recursive queries. (Typically, there is only one recursive part, but in some scenarios, such as binary trees, there may be more than one.)

    For your case, the anchor will be a select of all of the direct group memberships from the UserGroup table. This part is just a normal query that seeds the reciursive CTE with an initial set of rows.

    The recursive part of the query expands on the existing rows with an additional query (or queries) that joins the existing rows with whatever is needed to recursively expand the results. In your case, it will join the CTE with the GroupGroup table to walk the chain of and include any parent groups.

    For each row in the seed set, the recursive query might return zero, one or many additional rows. These results are both added to the combined result set and fed back into the recursion, possibly yielding even more results. There must be a limit though. BY default, SQL Server allows 100 levels or recursion, but this can be overridden with the MAXRECURSION option.

    with ExpandedUserGroups as (
        -- Anchor part
        select ug.uid, ug.gid
        from UserGroup ug
    
        union all
    
        -- Recursive part
        select xug.uid, gg.parentid
        from ExpandedUserGroups xug
        join GroupGroup gg
            on gg.childid = xug.gid
    )
    select xug.uid, xug.gid, g.sourceid, g.name
    from ExpandedUserGroups xug
    join [Group] g
        on g.gid = xug.gid
    order by xug.uid, xug.gid;
    

    Results

    uid gid sourceid name
    1 101 5 group101
    1 102 5 group102
    1 103 5 group103
    1 110 8 group110
    1 120 8 group120
    2 102 5 group102
    2 120 8 group120

    See this db<>fiddle for a demo.