0

I'm trying to recursively loop through and return all child_id's that have the root element of 9.

The structure:

->9
->->8
->->->17
->->22
->->->11

Parent Child Link Table:

+----+-----------+----------+
| id | parent_id | child_id |
+----+-----------+----------+
|  1 |         9 |        8 |
|  2 |         8 |       17 |
|  3 |         8 |       33 |
|  4 |         8 |       18 |
|  5 |         9 |       22 |
|  6 |        22 |       11 |
|  7 |        22 |        4 |
|  8 |         3 |        5 |
+----+-----------+----------+

Procedure (so far):

BEGIN

DECLARE x INT(11)

SET x = 0;
SET @elements = "";
SET @node = _root_; -- 9
SET @child_count = count_children(@node) -- function returning the child count of @node;
SET @children = get_children(@node); -- function returning the child id's of @node

-- check IF node has children
WHILE x <= @child_count DO
    SET @elements = CONCAT(@elements,x,',');
    SET x = x + 1;
END WHILE
SELECT @elements;

END

Desired Output: [8,17,33,18,22,11,4]

Question: How can I modify my procedure to be able to return all child_id's of the parent?

Evan Carroll
  • 65,432
  • 50
  • 254
  • 507
Jordan Davis
  • 101
  • 1

2 Answers2

0

try this,

declare @t table( id int, parent_id int,child_id int)
insert into @t values
(1 , 9 ,  8 ),(2 , 8 , 17 )
,(3 , 8 , 33 ),(4 , 8 , 18 )
,(5 , 9 , 22 ),(6 ,22 , 11 )
,(7 ,22 ,  4 ),(8 , 3 ,  5 )

declare @parentinput int=9

select t.* from @t t
left join @t t1
on t.child_id=t1.parent_id
where t.parent_id=@parentinput

union 
select t1.* from @t t
left join @t t1
on t.child_id=t1.parent_id
where t.parent_id=@parentinput
KumarHarsh
  • 1,623
  • 11
  • 10
0

If your solution really requires a table dedicated to parent-child connections, then the data that we're working with is necessarily a graph (not a tree/hierarchy) as that would allow a child to have more than one parent.

Hierarchical data doesn't need to contain child references. So for the initial table, I'm leaving out any references to children. Every row in the table is a 'child'. Each child may have zero or one parents as indicated in its parent_id FK column. Children with no parent are root elements. In case your problem does have purely hierarchical data, then you might benefit from a nested set approach.

What follows is a nested set approach that I've used from time to time with my hierarchical data. This gets a bit involved, but is crazy powerful. And fun!

DECLARE @tree table(
      id                    int PRIMARY KEY
    , parent_id             int
    , node_name             varchar(50)
    -- the following fields added to enable nested sets:
    , lvl                   int
    , lft                   int
    , rgt                   int
    , count_children        int
    , count_descendants     int
    , bct                   varchar(60)  -- bread crumb trail
)

DECLARE @x table ( -- scratch table for nested set calcs id int PRIMARY KEY , parent_left int , descendants int , lft int , rgt int , ord int )

DECLARE @max_level int , @row_id int , @level int , @count_rows int , @last_parent_left int , @fetchStatus int = 0 , @parent_left int , @lft int , @rgt int , @last_right int , @count_descendants int

insert into @tree (id, parent_id, node_name) VALUES (1 , 9 , 'Volkswagen' ) ,(2 , 8 , 'Slartibartfast' ) ,(3 , 8 , 'Arthur Dent' ) ,(4 , 8 , 'Trillian' ) ,(5 , 9 , 'Subaru' ) ,(11 , 5 , 'Crosstrek' ) ,(50 , 11 , 'Base' ) ,(51 , 11 , 'Premium' ) ,(52 , 11 , 'Sport' ) ,(53 , 11 , 'Limited' ) ,(54 , 11 , 'Wilderness' ) ,(12 , 5 , 'Outback' ) ,(6 ,22 , 'African Savanna' ) ,(7 ,22 , 'African Forest' ) ,(500 ,22 , 'Asian' ) ,(8 , NULL , 'Hitchhikers' ) ,(9 , NULL , 'Cars') ,(22 , NULL , 'Elephants')

-- count children, mark level 0 (zero) UPDATE p SET lvl = CASE WHEN p.parent_id IS NULL THEN 0 ELSE NULL END -- nodes with NULL parent_id are level zero (0) , count_children = ISNULL(cc.count_children, 0) , bct = p.node_name FROM @tree as p LEFT OUTER JOIN ( SELECT parent_id, count(*) as count_children FROM @tree as c GROUP BY c.parent_id ) as cc ON p.id = cc.parent_id

-- mark descendant levels
WHILE EXISTS ( SELECT TOP 1 1 FROM @tree as c WHERE c.lvl IS NULL AND EXISTS ( SELECT TOP 1 1 FROM @tree as p WHERE c.parent_id = p.id ) ) BEGIN UPDATE c SET lvl = p.lvl + 1 , bct = p.bct + ' -> ' + c.node_name FROM @tree as c INNER JOIN @tree as p ON c.parent_id = p.id WHERE c.lvl IS NULL AND p.lvl IS NOT NULL END

SELECT TOP 1 @max_level = lvl FROM @tree ORDER BY lvl DESC;

SELECT @level = @max_level;

WHILE @level >= -1 BEGIN PRINT @level;

UPDATE p
SET 
      count_descendants = ISNULL(p.count_children, 0) + ISNULL(cc.count_descendants, 0)
FROM @tree AS p
LEFT OUTER JOIN (
    SELECT 
          b.parent_id
        , SUM(count_descendants) AS count_descendants
    FROM @tree AS b  
    WHERE b.lvl = @level + 1
    GROUP BY b.parent_id
) AS cc
    ON p.id = cc.parent_id
WHERE p.lvl = @level

SELECT @count_Rows = @@ROWCOUNT;

SELECT @level -= 1

END

SELECT @level = -1

WHILE @level <= @max_level BEGIN SELECT @last_parent_left = -1, @fetchStatus = 0;

DELETE @x;

INSERT @x ( id, parent_left, descendants, ord )
SELECT 
        b.id
    , ISNULL(p.lft, 0) AS parent_left
    , b.count_descendants 
    , ROW_NUMBER() OVER (PARTITION BY p.lft ORDER BY b.node_name) as ord
FROM @tree AS b  
LEFT OUTER JOIN @tree AS p  
    ON b.parent_id = p.id
    AND p.lvl = b.lvl - 1
WHERE b.lvl = @level

DECLARE curs_x CURSOR FOR
SELECT id, parent_left, descendants 
FROM @x
ORDER BY parent_left, ord 
FOR UPDATE OF lft, rgt 

OPEN curs_x

SELECT @fetchStatus = 0;

WHILE @fetchStatus = 0
BEGIN
    FETCH NEXT FROM curs_x
    INTO @row_id, @parent_left, @count_descendants 

    SELECT @fetchStatus = @@FETCH_STATUS;

    IF @fetchStatus = 0
    BEGIN
        IF @parent_left = @last_parent_left
        BEGIN
            SELECT 
                  @lft = @last_right + 1
                , @rgt = @lft + @count_descendants * 2 + 1
                , @last_right = @rgt
        END

        ELSE
        BEGIN
            SELECT 
                  @last_parent_left = @parent_left
                , @lft = @parent_left + 1
                , @rgt = @lft + @count_descendants * 2 + 1
                , @last_right = @rgt
        END

        UPDATE @x
        SET 
              lft = @lft
            , rgt = @rgt
        WHERE id = @row_id
    END
END

CLOSE curs_x
DEALLOCATE curs_x

UPDATE h
SET 
      lft = x.lft
    , rgt = x.rgt 
    , bct = bct
FROM @tree as h
INNER JOIN @x as x
    ON h.id = x.id

SELECT @level += 1;

END

The @tree table is set up for reporting using nested sets. The maintenance of the left/right values isn't cheap. But if you're hierarchical dimension data isn't in rapid flux, it's worthwhile to use it and include a refresh of the nested set metadata as part of a maintenance procedure. Once the tooling is in, queries against it are powerful and fast.

Presenting the hierarchy in a flat rowset is a breaze.

-- Present the entire hierarchical table as if it was a fully exploded tree

SELECT t.id, t.bct, REPLICATE('| ', t.lvl) + t.node_name as node_text FROM @tree as t ORDER BY lft

/* -- result id bct node_text


9 Cars Cars 5 Cars -> Subaru | Subaru 11 Cars -> Subaru -> Crosstrek | | Crosstrek 50 Cars -> Subaru -> Crosstrek -> Base | | | Base 53 Cars -> Subaru -> Crosstrek -> Limited | | | Limited 51 Cars -> Subaru -> Crosstrek -> Premium | | | Premium 52 Cars -> Subaru -> Crosstrek -> Sport | | | Sport 54 Cars -> Subaru -> Crosstrek -> Wilderness | | | Wilderness 12 Cars -> Subaru -> Outback | | Outback 1 Cars -> Volkswagen | Volkswagen 22 Elephants Elephants 7 Elephants -> African Forest | African Forest 6 Elephants -> African Savanna | African Savanna 500 Elephants -> Asian | Asian 8 Hitchhikers Hitchhikers 3 Hitchhikers -> Arthur Dent | Arthur Dent 2 Hitchhikers -> Slartibartfast | Slartibartfast 4 Hitchhikers -> Trillian | Trillian

*/

Expand a family tree on any node in the tree


-- descendants of a guilty node
SELECT @guilty_node_id = 11 -- Crosstrek

SELECT t.id, REPLICATE('| ', t.lvl - gn.glvl) + t.node_name as node_text FROM @tree as t CROSS JOIN ( SELECT lft as glft , rgt as grgt , lvl as glvl FROM @tree as t WHERE t.id = @guilty_node_id ) as gn WHERE t.lft >= gn.glft
AND t.rgt <= gn.grgt
-- AND t.lvl > gn.glvl -- un-comment if you don't want root included ORDER BY t.lft

/* id node_text


11 Crosstrek 50 | Base 51 | Premium 52 | Sport 53 | Limited 54 | Wilderness */

-- ancestors of a guilty node SELECT @guilty_node_id = 54 -- Wilderness

SELECT t.id, REPLICATE('| ', t.lvl) + t.node_name as node_text FROM @tree as t CROSS JOIN ( SELECT lft as glft , rgt as grgt , lvl as glvl FROM @tree as t WHERE t.id = @guilty_node_id ) as gn WHERE t.lft <= gn.glft
AND t.rgt >= gn.grgt
-- AND t.lvl < gn.glvl -- un-comment if you don't want leaf included ORDER BY t.lft

/* -- result id node_text


9 Cars 5 | Subaru 11 | | Crosstrek 54 | | | Wilderness */

Of course, the OP didn't ask for all of this stuff. I thought I should add it in case return order becomes important. And it's fun. However, the final answer is in first column of that second-to-the-last query. The result-set includes the root node.

Doug Hills
  • 107
  • 6