How to create post-order traversal in SQL?

Viewed 225

Having a tree structure in database (id, parentId, order) where order means some value to sort per parent (it means order in parent list), how one can build full-SQL query to traverse this table in POST-ORDER?

What is post order is described on wiki, though only for binary trees - https://en.wikipedia.org/wiki/Tree_traversal

Not all of them is applyable to custom tree (for example IN-ORDER), but POST-ORDER is actully applyable:

      A
     / \
    B   C
       /|\
      D E F

output will be:

B D E F C A

The same in SQL data table:

|Id |ParentId | Order
|___|_________|______
|A  |null     |0
|B  |A        |0
|C  |A        |1
|D  |C        |0
|E  |C        |1
|F  |C        |2

I have been struggling with it quite a time, but looks like CTE doesn't allow inner ORDER BY clause (omg, why?!), so this task becomes impossible at my current level without stored procedures.

2 Answers

More as a proof of concept than a usable answer, here's a CTE-based version. It uses STRING_AGG to concatenate the children of each node in order, then recursively replaces each node with its children to build the output string - this means it wouldn't work in situations where node keys are substrings of one another.

DECLARE @t TABLE 
(id CHAR(1) NOT NULL PRIMARY KEY,
 parentid CHAR(1) NULL,
 roworder TINYINT NOT NULL
)

INSERT @t (id, parentid, roworder)
VALUES('A', NULL, 0),
('B','A',0),
('C','A',1),
('D','C',0),
('E','C',1),
('F','C',2),
('G','E',0),-- two extra nodes to prove that this isn't a one-off
('H','E',1)


;WITH aggCTE
AS
(
    SELECT parentid, STRING_AGG(CONVERT(VARCHAR(MAX), id), ' ') WITHIN GROUP (ORDER BY Roworder) AS children
    FROM @t
    GROUP BY parentid

)
,recCTE
AS
(
    SELECT  a.parentid, 
            a.children,
            CAST(ISNULL(a.parentid,'') AS VARCHAR(MAX)) AS processed, --to prevent loops
            0 AS seq, --to pick the right output row
            a.children AS firstnode --to disambiguate if the data contains multiple trees
    FROM aggCTE AS a
    WHERE a.parentid IS NULL

    UNION ALL

    SELECT  a.parentid, 
            REPLACE(a.children, b.parentid, CONCAT(b.children, ' ', b.parentid)) AS children, 
            CONCAT(a.processed, b.parentid) AS processed, 
            a.seq + 1 AS seq, 
            a.firstnode
    FROM recCTE AS a
    JOIN aggCTE AS b
    ON CHARINDEX(b.parentid, a.children) > 0
    AND CHARINDEX(b.parentid, a.processed) = 0
)
,rnCTE
AS
(
    SELECT children,
            ROW_NUMBER() OVER (PARTITION BY firstnode ORDER BY seq DESC) AS rn
    FROM recCTE
)
SELECT children AS post_order_traversal
FROM rnCTE
WHERE rn = 1

Found solution by simply implementing it as is:

If(OBJECT_ID('tempdb..#result') Is Not Null) Drop Table #result;
If(OBJECT_ID('tempdb..#stack') Is Not Null) Drop Table #stack;


create table #result (id int not null identity primary key, [value] bigint null);
create table #stack (id int not null identity primary key, [value] bigint null);

INSERT INTO #stack values(null) --inserting root identifiers here

WHILE EXISTS (SELECT * FROM #stack)
BEGIN
    declare @stack_id int, @stack_value bigint
    select top 1 @stack_id=id, @stack_value=value from #stack order by id desc
    delete from #stack where id=@stack_id

        INSERT INTO #stack
        -- here comes our query, which should fetch children of specified id and order it
        select tos.id 
        from inputTable as t
        where (ISNULL(@stack_value, 0) = 0 AND t.ParentId IS NULL) OR (ISNULL(@stack_value, 0) != 0 AND t.ParentId = @stack_value)
        order by t.[order] asc

    insert into #result values(@stack_value)
END

select [value] from #result order by id desc


If(OBJECT_ID('tempdb..#result') Is Not Null) Drop Table #result;
If(OBJECT_ID('tempdb..#stack') Is Not Null) Drop Table #stack;

As of now, it seems impossible with usage of CTE.

Related