-1

I need to write a query for a database structure where a parent element has 0-many child elements, but a child element can "have" many parents.

Note the problem I'm not trying to solve is that where the child element definition contains its parent element, and you can simply write a recursive CTE that "starts" at a child and joins back up itself until it hits a root element (ie, an element with NULL ParentID).

For this I need specifically to start at a parent and work my way down finding all children, grandchildren etc. So my database structure currently is as follows:

create table Element (
    Id int identity(1, 1) not null,
    Name varchar(100) not null,
    -- other stuff ...
)

create table ElementRelation (
    ParentElementId int not null, -- foreign key to Element.Id
    ChildElementId int not null, -- foreign key to Element.Id
    -- other stuff...
)

select * from Element
/* returns
       Id    | Name
       ------|---------
       1     | ElementA
       2     | ElementB
       3     | ElementC
       4     | ElementD

*/

select * from ElementRelation
/* returns
       ParentElementId | ChildElementId
       ----------------|---------------
          1            |   2
          1            |   3
          1            |   4
          2            |   3
          2            |   4
          3            |   4
*/

Which results in this tree structure (pardon the cruse Paint doodle):

visualisation of the tree

So you can see the typical solution of having a leaf-first table with a ParentId foreign key column doesn't work - element 4 has three immediate parents, element 3 has two, etc. It would be inappropriate for the child to declare its parent elements.

What I effectively need is a query that, given a starting parent element, finds all its immediate children. And then for each of those children, finds all of their immediate children, and so on until all paths have reached a leaf node. In this example data, a query against element 1 in this example would return the {1,2}, {1,3}, {1,4}, {2,3}, {2,4}, {3,4}, {3,4} (though it doesn't matter whether or not the query only returns a distinct list), and a query against element 2 would return {2,4}, {2,3}, {3,4} I could solve this with a cursor, but if there's a faster set-based means that would be preferred. If there's a better approach that would redefine the fundamental structure, that's also acceptable.

In terms of "what have you tried?" - several variants on a CTE query based on child-to-parent recursion, none of which came close to solving the problem, so I won't share them here.

Kai
  • 2,050
  • 8
  • 28
  • 46
  • What do your expected result look like? Have a look at [this](https://stackoverflow.com/questions/42119057/sql-recursive-cte-finding-objects-linked-by-property/42139978#42139978) example to see one way to handle relations and cycles in the data. Moving up or down through relations is pretty much the same. – HABO Sep 04 '20 at 13:49

2 Answers2

2

An option would be a recursive CTE

DROP TABLE IF EXISTS [#ElementRelation];

CREATE TABLE [#ElementRelation]
    (
        [ParentElementId] INT
      , [ChildElementId] INT
    );

INSERT INTO [#ElementRelation] (
                                   [ParentElementId]
                                 , [ChildElementId]
                               )
VALUES ( 1, 2 )
     , ( 1, 3 )
     , ( 1, 4 )
     , ( 2, 3 )
     , ( 2, 4 )
     , ( 3, 4 );

DECLARE @Start INT = 1;

;WITH [cte]
AS ( 
     --this query here is the Anchor, where do I start in my recursive CTE
     SELECT [p].[ParentElementId]
          , [p].[ChildElementId]
     FROM   [#ElementRelation] [p]  
     WHERE  [p].[ParentElementId] = @Start
     UNION ALL
     --This query is the recusive member, select from the table, joining my CTE and you see the ON clause [d].[ChildElementId] = [c].[ParentElementId]
     SELECT     [c].[ParentElementId]
              , [c].[ChildElementId]
     FROM       [#ElementRelation] [c]
     INNER JOIN [cte] [d]
         ON [d].[ChildElementId] = [c].[ParentElementId] )
SELECT *
FROM   [cte];

Giving you the results:

ParentElementId ChildElementId
--------------- --------------
1               2
1               3
1               4
3               4
2               3
2               4
3               4
Tim Mylott
  • 2,553
  • 1
  • 5
  • 11
1

Doing this in a single query is going to be difficult. CTE is probably the way to go but removing duplicates could be an issue. You've always got the possibility of cycles also. You could loop and add all children that aren't already in your list:

declare @ids table (id int primary key);
insert @ids values (1); -- id you are finding descendants for
while @@ROWCOUNT > 0
begin
  insert @ids (id)
  select distinct e.ChildElementId
  from @ids i
  join ElementRelation e on e.ParentElementId = i.id
  where not exists (select 1 from @ids i2 where i2.id = e.ChildElementId);
end
Jason Goemaat
  • 28,692
  • 15
  • 86
  • 113