Home > OS >  Combine values into string inside recursive CTE
Combine values into string inside recursive CTE

Time:12-22

I am using a recursive CTE to essentially navigate down a tree structure of id values. At each iteration of the recursion I would like to have a column that represents a string 'list' of all id values (nodes) that have been visited in the iteration steps so far. At first glance it seems like I would need a group by or aggregate function (like string_agg()) to accomplish this, but these are not allowed in the recursive part of a recursive CTE. My question seems to be similar to that found here recursive CTE to combine values, but I am hoping for a slightly more straightforward answer that will relate to the data I have added a sample of below. This first table is essentially the top of the tree. There could be one or multiple nodes at the top depending on how many records are in this first table. Here there is just 1 record:

name_id group_id
100 15

where name_id you could think of as the name of the tree, which is essentially irrelevant to this example since there is only one name_id, and group_id is the top of the tree which will then branch out based on the following table:

group_id group_id_member
10 15
11 15
4 10
11 4
3 11
10 3

where the group_id_member is saying that, for example, the group_id from the first table, 15, is a member of the group_id 10 and also 11. So the tree would look like 15 at the top, with a branch down for 10 and another branch for 11. Then off of 10 comes a branch of 4, and off of 11 comes a branch of 3. Then off of 4 comes a branch of 11, and off of 3 comes a branch of 10. Thus, this is an infinite loop since those branches are already in the table.

Essentially, the goal of this recursive query is to return all the branch nodes but not return any of them more than once. This recursive cte will run forever unless we do a check, like checking whether or not the group_id value is already in rpath. To check that, I am wanting to create a column which is a list of all nodes that have gotten hit from current and previous iterations (rpath column), and append to that column at each iteration, making the result table look something like:

name_id group_id group_id_member iter rpath
100 15 null 1 /15/
100 10 15 2 /15/10/11/
100 11 15 2 /15/10/11/
100 4 10 3 /15/10/11/4/3/
100 3 11 3 /15/10/11/4/3/
100 10 3 4 /15/10/11/4/3/10/11/
100 11 4 4 /15/10/11/4/3/10/11/

Where I included the 4th iteration in the table, which shows that the group_id values 10 and 11 are now in the rpath two times since this loop has been fully completed. Ultimately I would want to terminate this recursion at the 3rd iteration since at the 4th, for both group_id values, they appear in the rpath list already.

So my primary question is, what is the best way to create this rpath string inside the recursive part of the cte? See below for code to create the temp tables, and my code which attempts to solve this but is returning an error since I am trying to use string_agg() in the recursive part of the cte which is not allowed. Thanks!

if object_id('tempdb..#t1') is not null drop table #t1
CREATE TABLE #t1 (group_id int, group_id_member int)
INSERT into #t1 VALUES 
   (10,15),
   (11, 15),
   (4, 10), 
   (11, 4),
   (3, 11),
   (10, 3);
if object_id('tempdb..#t2') is not null drop table #t2
CREATE TABLE #t2 (name_id int, group_id int)
INSERT into #t2 VALUES 
   (100, 15)
; with rec as (
select  name_id,
        group_id,
        cast(null as int) as group_id_member,
        1 as iter,
        convert(varchar(128),concat('/', str_agg.agg, '/')) as rpath
    from #t2
    cross apply (select string_agg(group_id, '/') as agg from #t2) as str_agg  -- this does work since it is not in the recursive part of the cte
union all
select  rec.name_id,
        t1.group_id,
        t1.group_id_member,
        iter   1 as iter,
        convert(varchar(128), concat(rec.rpath, str_agg1.agg1, '/')) as rpath  -- trying to create the string that represents all of the nodes that have been visited
    from #t1 t1
    inner join rec
        on t1.group_id_member = rec.group_id
    cross apply (select string_agg(t1.group_id_member ,'/') as agg1 from #t1 t1 where t1.group_id_member = rec.group_id) str_agg1  -- cross apply to create the string_agg, if this was allowed in recursive cte...
    where rec.rpath not like '%/'   convert(varchar(128), t1.group_id)   '/%'
)
select * from rec order by iter asc

CodePudding user response:

At each iteration of the recursion I would like to have a column that represents a string 'list' of all id values (nodes) that have been visited in the iteration steps so far

As I understand your question, you don’t need aggregation here. The recursive query processes iteratively, so you can just accumulate the ids of the visited nodes along the way, using concat.

with rec as (
    select 
        name_id,
        group_id,
        cast(null as int) as group_id_member,
        1 as iter,
        concat('/', convert(varchar(max), group_id)) as rpath
    from #t2
    union all
    select
        r.name_id,
        t1.group_id,
        t1.group_id_member,
        iter   1 as iter,
        concat(r.rpath, '/', t1.group_id)
    from #t1 t1
    inner join rec r
        on t1.group_id_member = r.group_id
    where r.rpath not like concat('%/', t1.group_id, '/%')
)
select * from rec order by iter

Demo on DB Fiddle

  • Related