Home > Net >  Using recursive CTE to generate hierarchy results ordered by depth without the use of heiarchyid
Using recursive CTE to generate hierarchy results ordered by depth without the use of heiarchyid

Time:12-17

I would like to query hierarchy results ordered by depth first without the use of SQL's heiarchyid built in function. Essentially, I am hoping to accomplish the depth ordering without any fancy functions.

I have provided a temp table below that contains these records:

Id p_Id order1 name1
1 null 1 josh
2 null 2 mary
3 null 3 george
4 1 1 joe
5 1 2 jeff
6 2 1 marg
7 2 2 moore
8 2 3 max
9 3 1 gal
10 3 2 guy
11 4 1 tod
12 4 2 ava
13 9 1 ron
14 9 2 bill
15 9 100 pat

where p_Id is the id of the parent record, and order1 is essentially just the ordering of which the depth first output should be displayed. To show why my query does not fully work, I made the order1 of the last record 100 instead of say, 3. However this should not ultimately matter since 100 and 3 both come after the previous order1 value, 2. An example of a correct result table is shown below:

Id p_Id order1 name1 Descendants
1 null 1 josh josh
4 1 1 joe josh/joe
11 4 1 tod josh/joe/tod
12 4 2 ava josh/joe/ava
5 1 2 jeff josh/jeff
2 null 2 mary mary
6 2 1 marg mary/marg
7 2 2 moore mary/moore
8 2 3 max mary/max
3 null 3 george george
9 3 1 gal george/gal
13 9 1 ron george/gal/ron
15 9 2 bill george/gal/bill
14 9 100 pat george/gal/pat
10 3 2 guy george/guy

Where an example of my results are shown below:

Id p_Id order1 name1 Descendants levels
1 null 1 josh josh .1
4 1 1 joe josh/joe .1.1
11 4 1 tod josh/joe/tod .1.1.1
12 4 2 ava josh/joe/ava .1.1.2
5 1 2 jeff josh/jeff .1.2
2 null 2 mary mary .2
6 2 1 marg mary/marg .2.1
7 2 2 moore mary/moore .2.2
8 2 3 max mary/max .2.3
3 null 3 george george .3
9 3 1 gal george/gal .3.1
13 9 1 ron george/gal/ron .3.1.1
15 9 100 pat george/gal/pat .3.1.100
14 9 2 bill george/gal/bill .3.1.2
10 3 2 guy george/guy .3.2

where I have created a levels column that essentially concatenates the order1 values and separates them with a period. This almost returns the correct results, but due to the fact that I am ordering by this string (of numbers and periods), the levels value of .3.1.100 will come before .3.1.2 , which is not what the desired output should look like. I am sure there is a different method to return the correct depth order. See below for the code that generates a temp table, and the code that I used to generate the incorrect output that I have so far.

if object_id('tempdb..#t1') is not null drop table #t1
CREATE TABLE #t1 (Id int, p_Id int, order1 int, name1 varchar(150))


INSERT into #t1 VALUES 
   (1, null, 1, 'josh'),
   (2, null, 2, 'mary'),
   (3, null, 3, 'george'),
   (4, 1, 1, 'joe'),
   (5, 1, 2, 'jeff'),
   (6, 2, 1, 'marg'),
   (7, 2, 2, 'moore'),
   (8, 2, 3, 'max'),
   (9, 3, 1, 'gal'),
   (10, 3, 2, 'guy'),
   (11, 4, 1, 'tod'),
   (12, 4, 2, 'ava'),
   (13, 9, 1, 'ron'),
   (14, 9, 2, 'bill'),
   (100, 9, 100, 'pat');

select * from #t1

-- Looking to generate heiarchy results ordered by depth --
; with structure as (

    -- Non-recursive term.
    -- Select the records where p_Id is null

    select  p.Id,
            p.p_Id,
            p.order1,
            p.name1,
            cast(p.name1 as varchar(64)) as Descendants,
            cast(concat('.', p.order1) as varchar(150)) as levels 
        from #t1 p 
        where p.p_Id is null

        union all

        -- Recursive term.
        -- Treat the records from previous iteration as parents.
        -- Stop when none of the current records have any further sub records.

        select  c.Id,
                c.p_Id,
                c.order1,
                c.name1,
                cast(concat(p.Descendants, '/', c.name1) as varchar(64)) as Descendants,
                cast(concat(p.levels, '.', c.order1) as varchar(150)) as levels
            from #t1 c                               -- c being the 'child' records
                inner join structure p                   -- p being the 'parent' records
                    on c.p_Id = p.Id

)

select  *
    from structure
    order by replace(levels, '.', '') asc

CodePudding user response:

Take II. As pointed out by OP my original answer fails for more than 10 children. So what we can do (OP's suggestion) is pad the values out with zeros to a constant length. But what length? We need to take the largest number of children under a node and add this to the largest value or order, so for the example provided this is 100 3, and then take the length of that (3) and pad every order with zeros to 3 digits long. This means we will always be ordering as desired.

declare @PadLength int = 0;

select @PadLength = max(children)
from (
    select len(convert(varchar(12),max(order1) count(*))) children
    from #t1
    group by p_Id
) x;

-- Looking to generate heiarchy results ordered by depth --
with structure as (
    -- Non-recursive term
    -- Select the records where p_Id is null
    select
        p.Id [Id]
        , p.p_Id [ParentId]
        , p.order1 [OrderBy]
        , p.name1 [Name]
        , cast(p.name1 as varchar(64)) Descendants
        , concat('.', right(replicate('0',@Padlength)   convert(varchar(12),p.order1), @PadLength)) Levels
    from #t1 p 
    where p.p_Id is null

    union all

    -- Recursive term
    -- Treat the records from previous iteration as parents.
    -- Stop when none of the current records have any further sub records.
    select
        c.Id,
        c.p_Id,
        c.order1,
        c.name1,
        cast(concat(p.Descendants, '/', c.name1) as varchar(64)),
        concat(p.levels, '.', right(replicate('0',@Padlength)   convert(varchar(12),c.order1), @PadLength))
    from #t1 c -- c being the 'child' records
    inner join structure p on c.p_Id = p.Id -- p being the 'parent' records
)
select *
from structure
order by replace(levels, '.', '') asc;

CodePudding user response:

Note: This answer fails in the case when there are more than 10 children under a particular node. Leaving for interest.


So this issue you have run into is that you are ordering by a string not a number. So the string 100 comes before the string 2. But you need to order by a string to take care of the hierarchy, so one solution is to replace order1 with row_number() based on the order1 column while its still a number and use the row_number() to build your ordering string.

So you replace:

cast(concat(p.levels, '.', c.order1) as varchar(150)) as levels

with

cast(concat(p.levels, '.', row_number() over (order by c.Order1)) as varchar(150))

giving a full query of

with structure as (

    -- Non-recursive term.
    -- Select the records where p_Id is null

    select  p.Id,
            p.p_Id,
            p.order1,
            p.name1,
            cast(p.name1 as varchar(64)) as Descendants,
            cast(concat('.', p.order1) as varchar(150)) as levels 
        from #t1 p 
        where p.p_Id is null

        union all

        -- Recursive term.
        -- Treat the records from previous iteration as parents.
        -- Stop when none of the current records have any further sub records.

        select  c.Id,
                c.p_Id,
                c.order1,
                c.name1,
                cast(concat(p.Descendants, '/', c.name1) as varchar(64)) as Descendants,
                cast(concat(p.levels, '.', row_number() over (order by c.Order1)) as varchar(150))
            from #t1 c                               -- c being the 'child' records
                inner join structure p                   -- p being the 'parent' records
                    on c.p_Id = p.Id

)
select  *
    from structure
    order by replace(levels, '.', '') asc;

Which returns the desired results.

Note: good question, well written.

  • Related