Search code examples
t-sqlsql-server-2012hierarchical-datarecursive-cte

Order child items in a certain order in a multi-level hierarchy query in SQL Server 2012


I have a table in SQL Server 2012 called Items which contains different items that are related to each other in a parent-child relationship through ParentId column. This table contains a topmost level item with ItemId of 429965 and its children, grandchildren, great grandchildren and so on i.e. it contains a multi-level hierarchy of a topmost level item.

A demo of my situation with sample data and query that I have tried is at SQL Fiddle of this problem. The calculated column of Position tells the order in which hierarchy items are shown.

I am trying to get the complete hierarchy of a topmost level item so that following requirements are met:

  1. Parent is immediately followed by its child items in this multi-level hierarchy
  2. Child items need to be shown in this hierarchy in an ascending order of CreateDate.

The query that I have tried achieves requirement #1, but not requirement #2.

Question

How can I achieve requirement #2 in addition to requirement #1 using existing recursive query? I cannot simply order by PathStr, CreateDate because no two paths are going to be the same in my multi-level hierarchy.

Schema and sample data creation queries

CREATE TABLE Items (
  ItemId int PRIMARY KEY,
  ParentId int,
  CreateDate datetime
);


INSERT Items
  VALUES (44129, 429965, CONVERT(datetime, '2016-01-01 17:30:55.760', 121)),
  (61291, 203905, CONVERT(datetime, '2016-01-02 20:18:35.770', 121)),
  (157898, 335625, CONVERT(datetime, '2016-01-01 00:00:06.420', 121)),
  (191951, 778472, CONVERT(datetime, '2016-01-01 00:00:01.400', 121)),
  (203905, 960767, CONVERT(datetime, '2016-01-01 00:00:01.310', 121)),
  (265468, 429965, CONVERT(datetime, '2016-05-01 06:07:26.690', 121)),
  (268246, 265468, CONVERT(datetime, '2016-10-06 13:41:55.990', 121)),
  (283015, 394157, CONVERT(datetime, '2017-12-03 01:58:08.710', 121)),
  (299356, 443367, CONVERT(datetime, '2016-01-01 00:00:01.400', 121)),
  (335625, 894441, CONVERT(datetime, '2016-11-06 21:27:00.270', 121)),
  (338413, 968392, CONVERT(datetime, '2016-11-21 07:15:48.010', 121)),
  (394157, 785375, CONVERT(datetime, '2016-05-19 09:19:28.500', 121)),
  (397189, 894441, CONVERT(datetime, '2016-01-01 13:34:03.980', 121)),
  (404536, 894441, CONVERT(datetime, '2016-01-01 00:00:16.850', 121)),
  (429965, 0, CONVERT(datetime, '2016-01-01 00:00:06.090', 121)),
  (439536, 968392, CONVERT(datetime, '2017-03-25 23:51:48.570', 121)),
  (443367, 191951, CONVERT(datetime, '2016-01-01 00:00:01.090', 121)),
  (778472, 394157, CONVERT(datetime, '2016-01-02 20:43:59.760', 121)),
  (785375, 910250, CONVERT(datetime, '2017-10-19 03:59:14.950', 121)),
  (894441, 265468, CONVERT(datetime, '2016-01-01 00:00:08.600', 121)),
  (910250, 268246, CONVERT(datetime, '2016-07-21 00:43:47.420', 121)),
  (927248, 785375, CONVERT(datetime, '2017-02-13 04:19:46.340', 121)),
  (960767, 335625, CONVERT(datetime, '2016-01-01 00:00:01.960', 121)),
  (968392, 785375, CONVERT(datetime, '2017-09-10 02:15:25.780', 121))

Query that I tried

WITH x (ItemId, ParentId, PathStr, CreateDate)
AS (SELECT
  ItemId,
  0 AS ParentId,
  CAST(ItemId AS varchar(max)) AS Pathstr,
  CreateDate
FROM Items
WHERE ItemId = 429965
UNION ALL
--get children for each parent ( c is for child table and x is for parent table)
SELECT
  i.ItemId,
  i.ParentId,
  x.PathStr + '-' + CAST(i.ItemId AS varchar(max)),
  i.CreateDate
FROM Items i
INNER JOIN x
  ON x.ItemId = i.ParentId)
SELECT
  *,
  ROW_NUMBER() OVER (ORDER BY PathStr) AS Position
FROM x;

In above query, items with Position of 2 and 24 appear in descending order of CreateDate and both are child items of same parent. If ordering was correct then item with position 24 should actually have had a position of 2, and item with position 2 should have had a position of 24.


Solution

  • you were close...

    I added a Step column which you can use to see the Level in your hierarchy. Furthermore I added a DatePosition to check, whether all dates to a given Stel are in ascending order. Seems to be okay:

    WITH x (ItemId, ParentId, PathStr, CreateDate,Step)
    AS (SELECT
        ItemId,
        0 AS ParentId,
        CAST(ItemId AS varchar(max)) AS Pathstr,
        CreateDate,
        1 AS Step
    FROM Items
    WHERE ItemId = 429965
    UNION ALL
    --get children for each parent ( c is for child table and x is for parent table)
    SELECT
        i.ItemId,
        i.ParentId,
        x.PathStr + '-' + CAST(i.ItemId AS varchar(max)),
        i.CreateDate,
        x.Step+1
    FROM Items i
    INNER JOIN x
        ON x.ItemId = i.ParentId)
    SELECT
        *,
        ROW_NUMBER() OVER (ORDER BY PathStr) AS Position,
        ROW_NUMBER() OVER (ORDER BY CreateDate) AS DatePosition
    
    FROM x
    ORDER BY Step,DatePosition;
    

    UPDATE: Threaded discussion...

    Try this

    WITH x (ItemId, ParentId, PathStr,CreateDate,Step)
    AS (SELECT  ItemId,
                0 AS ParentId,
                CAST(REPLACE(STR(ItemId,9),' ','0') AS VARCHAR(MAX)) AS PathStr,
                CreateDate,
                1 AS Step
        FROM Items
        WHERE ItemId = 429965
    
        UNION ALL
        --get children for each parent ( i is for child table and x is for parent table)
        SELECT  i.ItemId,
                i.ParentId,
                CAST(x.PathStr + '-' + REPLACE(STR(i.SortNmbr,3),' ','0') AS VARCHAR(MAX)) + '-' + CAST(REPLACE(STR(i.ItemId,9),' ','0') AS VARCHAR(MAX)),
                i.CreateDate,
                x.Step+1
        FROM x
        CROSS APPLY(SELECT *, ROW_NUMBER() OVER(ORDER BY CreateDate) AS SortNmbr FROM Items WHERE Items.ParentId=x.ItemId) AS i
       )
    SELECT *
    FROM x
    ORDER BY PathStr;
    

    If you want to order a recursive cte's result, you must include everything needed into your PathStr. Furthermore this sorting is done alphanumerically, so you have to add zeros to all numbers up to an equal width.

    I used CROSS APPLY (instead of an INNER JOIN) to get the related rows sorted and include the derived SortNmbr (padded too!) into the sort string. Alternatively one might include the date itself, but this leads to rather huge strings with deeper nesting.

    My padding allows 9 digits for the ItemId and 3 digits for the count of answers per parent, which is to much probably. Reduce this accordingly to reduce the size of the sort string. And you can remove the hyphens, they are only for better reading.