Recursively find all ancestors given the child - sql-server

Given a child id, I need to return a query containing all parents of that child as well as their parents till I get to the root parent.
For example, given this data:
ID / Parent ID
1 / 0
2 / 1
3 / 2
4 / 0
5 / 3
So if I passed in ID 5 I would like to get a query with the results:
ID / Parent ID
1 / 0
2 / 1
3 / 2
This table does not work with a hierarchyid type so I suspect that this will need to be done with a CTE, but have no clue how. If it can be done in an SQL query / proc, any help would be appreciated.
Thanks

This is more or less what you want:
-- CTE to prepare hierarchical result set
;WITH #results AS
(
SELECT id,
parentid
FROM [table]
WHERE id = #childId
UNION ALL
SELECT t.id,
t.parentid
FROM [table] t
INNER JOIN #results r ON r.parentid = t.id
)
SELECT *
FROM #results;
Reference:
CTE: Common Table Expression
Working example:
-- create table with self lookup (parent id)
CREATE TABLE #tmp (id INT, parentid INT);
-- insert some test data
INSERT INTO #tmp (id, parentid)
SELECT 1,0 UNION ALL SELECT 2,1 UNION ALL SELECT 3,2
UNION ALL SELECT 4,0 UNION ALL SELECT 5,3;
-- prepare the child item to look up
DECLARE #childId INT;
SET #childId = 5;
-- build the CTE
WITH #results AS
(
SELECT id,
parentid
FROM #tmp
WHERE id = #childId
UNION ALL
SELECT t.id,
t.parentid
FROM #tmp t
INNER JOIN #results r ON r.parentid = t.id
)
-- output the results
SELECT *
FROM #results
WHERE id != #childId
ORDER BY id;
-- cleanup
DROP TABLE #tmp;
Output:
1 | 0
2 | 1
3 | 2

Related

Update sequence based on pattern SQL Server

I was not able to quote correct tile for the question.
Below is my table.
Expected Output : With ID and SequenceNo column. Was unable to upload image.
ID Act SequenceNo1
1 1 1
2 2 NULL
3 3 NULL
4 4 NULL
5 1 5
6 2 NULL
7 3 NULL
8 4 NULL
9 5 NULL
10 6 NULL
11 5 11
12 6 NULL
13 1 13
14 2 NULL
15 3 NULL
16 4 NULL
17 5 NULL
18 6 NULL
19 1 19
20 2 NULL
21 3 NULL
22 4 NULL
23 5 NULL
24 6 NULL
At the start last column SequenceNo is NULL.`
My requirement is to update value of ID column to SequenceNo column whenever new series of Act column is started.
Act column has value of 1 to 6. There can be case where any number from 1 to 6 is missing from Act.
Example1 : ID 1 to 4 - Act is correct but, in next row (ID=5) Act is restarted. Hence need to update SequenceNo column.
Example2 : ID 5 to 10 are correct. But next row (ID=11;Act=5) has new sequence hence need to update SequenceNo column.
CREATE TABLE #tmp
(
ID int
, ScheduleID varchar(50)
,DCNumber VARCHAR(50)
, BuildingID varchar(10)
, StoreNumber int
, [DayOfWeek] int
, [Tm] varchar(10)
,[Act] int
, SequenceNo int
)
INSERT INTO #tmp SELECT 1,'WAS',9003,900301,254,1,'00:00',1,NULL
INSERT INTO #tmp SELECT 2,'WAS',9003,900301,254,1,'00:00',2,NULL
INSERT INTO #tmp SELECT 3,'WAS',9003,900301,254,1,'00:00',3,NULL
INSERT INTO #tmp SELECT 4,'WAS',9003,900301,254,1,'00:00',4,NULL
INSERT INTO #tmp SELECT 5,'WAS',9003,900301,254,2,'00:00',1,NULL
INSERT INTO #tmp SELECT 6,'WAS',9003,900301,254,2,'00:00',2,NULL
INSERT INTO #tmp SELECT 7,'WAS',9003,900301,254,2,'00:00',3,NULL
INSERT INTO #tmp SELECT 8,'WAS',9003,900301,254,2,'00:00',4,NULL
INSERT INTO #tmp SELECT 9,'WAS',9003,900301,254,2,'00:00',5,NULL
INSERT INTO #tmp SELECT 10,'WAS',9003,900301,254,2,'00:00',6,NULL
INSERT INTO #tmp SELECT 11,'WAS',9003,900301,254,3,'00:00',5,NULL
INSERT INTO #tmp SELECT 12,'WAS',9003,900301,254,3,'00:00',6,NULL
INSERT INTO #tmp SELECT 13,'WAS',9003,900301,254,4,'00:00',1,NULL
INSERT INTO #tmp SELECT 14,'WAS',9003,900301,254,4,'00:00',2,NULL
INSERT INTO #tmp SELECT 15,'WAS',9003,900301,254,4,'00:00',3,NULL
INSERT INTO #tmp SELECT 16,'WAS',9003,900301,254,4,'00:00',4,NULL
INSERT INTO #tmp SELECT 17,'WAS',9003,900301,254,5,'00:00',5,NULL
INSERT INTO #tmp SELECT 18,'WAS',9003,900301,254,5,'00:00',6,NULL
INSERT INTO #tmp SELECT 19,'WAS',9003,900301,254,6,'00:00',1,NULL
INSERT INTO #tmp SELECT 20,'WAS',9003,900301,254,6,'00:00',2,NULL
INSERT INTO #tmp SELECT 21,'WAS',9003,900301,254,6,'00:00',3,NULL
INSERT INTO #tmp SELECT 22,'WAS',9003,900301,254,6,'00:00',4,NULL
INSERT INTO #tmp SELECT 23,'WAS',9003,900301,254,7,'00:00',5,NULL
INSERT INTO #tmp SELECT 24,'WAS',9003,900301,254,7,'00:00',6,NULL
I have build one logic but that is time consuming.
DECLARE #Act INT, #iStart INT, #iMax INT, #iFirst INT
SET #iFirst = 7
SET #iMax = (SELECT MAX(ID) FROM #tmp)
SET #iStart = 1
WHILE(#iStart <= #iMax)
BEGIN
SET #Act = (SELECT Act FROM #tmp WHERE ID = #iStart)
IF(#iFirst > #Act )
BEGIN
UPDATE #tmp SET SequenceNo = #iStart WHERE ID = #iStart
END
SET #iFirst = #Act
SET #iStart = #iStart + 1
END
I am looking out for any alternative optimized solution.
Is this what you wanted?
update tupd
set
SequenceNo = tupd.ID
from (
select
*
, LAG(t.Act, 1, null) over(order by t.ID) as LagAct
from #tmp t
) tt
inner join #tmp tupd on tt.ID = tupd.ID
where tt.Act < tt.LagAct
select
*
from #tmp tt
order by tt.ID
You can find breaks in the Act sequence if you compare the current row with the previous. In current SQL Server versions, ie 2012+ you can do that with the LAG() method. In previous versions, you have to perform a self join on the current ID value and its previous value. If the difference between the current and previous Act values isn't 1, there is a break in the sequence. This only works if there are no gaps in ID.
This query will return 1 in the SeqBreak column for each row where Act restarts
select
t1.ID,
t1.SequenceNo ,
case when t1.act = t2.act+1 then '0' else '1' end as SeqBreak
from #tmp t1 left join #tmp t2 on t1.id=t2.id+1
You can use this in a CTE to select only the rows where Act breaks. You can update the CTE directly, at least in SQL Server 2014.
with x as (
select
t1.ID,
t1.SequenceNo ,
case when t1.act = t2.act+1 then '0' else '1' end as SeqBreak
from #tmp t1 left join #tmp t2 on t1.id=t2.id+1 )
update x
set SequenceNo=x.ID
where seqbreak=1 and id>1
If that doesn't work with SQL Server 2008, you'll have to join between the cte and the table:
with x as (
select
t1.ID,
t1.SequenceNo ,
case when t1.act = t2.act+1 then '0' else '1' end as SeqBreak
from #tmp t1 left join #tmp t2 on t1.id=t2.id+1 )
update #tmp
set SequenceNo=x.ID
from #tmp inner join x on x.ID=#tmp.ID
where seqbreak=1 and #tmp.id>1
The equivalent query using LAG(), one of the windowing functions in SQL Server 2012 is simpler and twice as fast, since it avoids the self join:
with x as (
select
ID,
SequenceNo ,
case when act = 1 +LAG(act,1) OVER (ORDER BY ID) then '0' else '1' end as SeqBreak
from #tmp)
update x
set SequenceNo=x.ID
where seqbreak=1 and id>1
;WITH CTE
AS
(
SELECT ID,Act,RANK() OVER (PARTITION BY [DayOfWeek] ORDER BY Act) RN
FROM #tmp
)
UPDATE t
SET t.SequenceNo = t.id
FROM #tmp t
inner join CTE C
ON t.ID = C.ID
WHERE C.RN = 1
If I'm getting your question correctly below query should fetch the desired result.
update t3 set sequenceno = t3.id
from
#temp t1
cross apply (select id+1 as idsum
from #temp
tmp where t1.id = tmp.id)t2
inner join #temp t3
on t3.id = t2.idsum
where t1.act>t3.act

Rewriting function to conditional CTE

Considering following table:
SELECT [ItemID]
,[ParentID]
,[PolicyID]
,[PolicyRoot]
FROM [AdventureWorks2008R2].[dbo].[Example]
ItemID ParentID PolicyID PolicyRoot
----------- ----------- ---------- ----------
1 NULL default 1
2 1 b 1
3 1 c 0
4 NULL d 1
5 3 e 0
6 3 f 1
7 NULL g 0
I'm trying to select the PolicyID from each item where PolicyRoot = 1, in case PolicyRoot = 0 I need to use PolicyID from its ParentID. This is recursive...
Working with a function:
CREATE FUNCTION dbo.Policies(#ItemID INT) RETURNS VARCHAR(10)
AS
BEGIN
DECLARE #ParentID INT, #PolicyRoot BIT, #PolicyID VARCHAR(10)
SELECT #ParentID = ParentID
, #PolicyRoot = PolicyRoot
, #PolicyID = PolicyID
FROM [dbo].[Example]
WHERE ItemID = #ItemID
IF #PolicyRoot != 1
SELECT #PolicyID = dbo.Policies(#ParentID)
RETURN #PolicyID
END;
GO
SELECT ItemID
, dbo.Policies(ItemID) AS Policy
FROM [dbo].[Example];
ItemID Policy
----------- ----------
1 default
2 b
3 default
4 d
5 default
6 f
7 NULL
I'm trying to rewrite this function to a CTE, but I don't have any CTE knowledge yet. I've read into multiple CTE's but I don't have a single clue how to manage a conditional CTE. This is as far as I've gotten, I'm not familiar (enough) with the UNION ALL.
WITH Policies (ItemID, PolicyID) AS (
SELECT ItemID
, PolicyID
FROM dbo.Example
UNION ALL
...
)
SELECT ItemID
, PolicyID
FROM Policies;
Can someone explain me in plain steps how such a CTE works and push me in the right direction?
A recursive CTE works by joining to itself, using a UNION ALL to collate the results.
You start with yourtable to populate the initial dataset of the recursive query
select * from yourtable
and you add to that with the UNION ALL, further results
select c.ItemID, t2.ParentID, t2.PolicyID, t2.PolicyRoot
from yourtable t2
inner join c on c.ParentID = t2.ItemID
where c.PolicyRoot=0
and the recursion occurs in this - where the results of this query are fed through this query again and again, up to the MAXRECURSION limit, or when no more results are added.
;with c as
(
select * from yourtable
union all
select c.ItemID, t2.ParentID, t2.PolicyID, t2.PolicyRoot
from yourtable t2
inner join c on c.ParentID = t2.ItemID
where c.PolicyRoot=0
)
select t.ItemID, c.PolicyID
from yourtable t
left join c on t.ItemID = c.ItemID
and c.PolicyRoot=1

Is there a way to duplicate part of a adjacent list hierarchy given a starting id?

I have an adjacent list hierarchy model that makes up a topic structure
ID Parent_Id Topic_Name
1 Null Topic 1
2 Null Topic 2
3 2 Topic 3
4 3 Topic 4
5 2 Topic 5
6 Null Topic 6
This forms part of an application which I cant change - the topics dont have multiple parents so unfortunatly I can't move to a nested sets - although if this was an interim step in the process - this would be fine as long as it went back to adjacent list hierarchy model
I want to specify a topic id and then copy it to a new topic id and retain the levels / structure underneath
So in my example I could specify topic topic_id 2 and it would create
ID Parent_Id Topic_Name
7 Null Topic 2
8 7 Topic 3
9 8 Topic 4
10 7 Topic 5
Auto numbering is taken care of for the ID so no need to construct that, but obviously the parent id needs to be retained
How can I achieve the above? would I need to flatten the data and do 3 seperate inserts logging the id after each insert?
You can use a recursive CTE to get the rows to insert.
If you use merge to add the rows you can use output to capture a mapping between the generated ID and the old ID which can be used to update the column Parent_ID for the inserted rows.
-- ID for topic to copy
declare #ID int;
set #ID = 2;
-- Table to hold the inserted rows
declare #T table
(
New_ID int,
Old_ID int,
Old_ParentID int
);
-- Add rows from recursive CTE using merge
with C as
(
select T.ID, T.Parent_Id, T.Topic_Name
from YourTable as T
where T.ID = #ID
union all
select T.ID, T.Parent_Id, T.Topic_Name
from YourTable as T
inner join C
on C.ID = T.Parent_Id
)
merge YourTable
using C
on 0 = 1
when not matched then
insert (Topic_Name) values (C.Topic_Name)
output inserted.ID,
C.ID,
C.Parent_Id
into #T(New_ID, Old_ID, Old_ParentID);
-- Update Parent_Id for the new rows
update Y set
Parent_Id = T2.New_ID
from #T as T1
inner join #T as T2
on T1.Old_ParentID = T2.Old_ID
inner join YourTable as Y
on T1.New_ID = Y.ID;
SE-Data
Working example. Works in theory but probably not robust for high transaction volumes or fast enough for big tables.
--- sample table
create table tbl (
id int identity primary key,
parent_id int references tbl(id),
topic_name varchar(100));
insert tbl values
( Null, 'Topic 1'),
( 1, ' Topic 2'),
( 2 , ' Topic 3'),
( 3 , ' Topic 4'),
( 2 , ' Topic 5'),
( Null, ' Topic 6'),
( 4, ' Topic 4-3'),
( 7, ' Topic 5-4')
;
--- script to duplicate a hierarchy branch
declare #inserttbl table (
id int,
parent_id int,
topic_name varchar(100));
;with cte as (
select id, parent_id, topic_name
from tbl
where id=2 -- or parameter
union all
select t.id, t.parent_id, t.topic_name
from tbl t
join cte c on t.parent_id=c.id
), cte2 as (
select *,rn=row_number() over (order by id)
from cte
), cte3 as (
select rec.*, par.rn as parent_rn
from cte2 rec
left join cte2 par on par.id=rec.parent_id
)
insert #inserttbl
select cte3.rn,
case when cte3.rn=1 then cte3.parent_id
else cte3.parent_rn end,
topic_name
from cte3;
insert tbl(topic_name)
select topic_name from #inserttbl order by id;
declare #delta int=scope_identity()-##rowcount;
update t
set parent_id = i.parent_id + case when i.id=1 then 0
else #delta end
from tbl t
join #inserttbl i on t.id - #delta = i.id;
--- check results
select * from tbl;

SQL Server CTE -Find top parentID forEach childID?

I have a table which contains hierarchy data - something like:
childID | parentID
____________________
1 | 5
5 | 9
9 | 20
2 | 4
3 | 7
7 | 8
8 | 8
20 | 20
4 | 4
8 | 8
desired output:
I've created a recursive CTE which finds me the top fatherID.
Something like:
;WITH cte AS (
SELECT a.childID
,a.parentID
,1 AS lvl
FROM [Agent_Agents] a
WHERE a.childID = 214 //<==== value to begin with !! - thats part the problem
UNION ALL
SELECT tmp.childID
,tmp.parentID
,cte.lvl+1
FROM [Agent_Agents] tmp
INNER JOIN cte ON tmp.childID = cte.parentID
WHERE cte.childID<>cte.parentID
)
SELECT *
FROM cte
WHERE lvl = (
SELECT MAX(lvl)
FROM cte
)
The problem:
I executed the CTE with explicit childID value to begin with (214) !
So it gives me the value for 214 only.
the CTE do the recursive part and find topParent for childID.
but
I want ForEach row in the Table - to execute the CTE with the childID value !
I have tried to do it with CROSS APPLY:
Something like:
select * from myTable Cross Apply (
;WITH cte AS (....)
)
but IMHO (from my testing !!) - its impossible.
The other idea of putting the recursive CTE in a UDF has a performance penalty (udf's problem as we know).
How can I create this query so that it'll actually work? ( or some near solution )?
here is what I've tried
https://data.stackexchange.com/stackoverflow/query/edit/69458
Can't you do something like this?
;WITH cte AS (....)
SELECT
*
FROM
cte
CROSS APPLY
dbo.myTable tbl ON cte.XXX = tbl.XXX
Put the CROSS APPLY after the CTE definition - into the one SQL statement that refers back to the CTE. Wouldn't that work??
OR: - flip around your logic - do a "top-down" CTE, that picks the top-level nodes first, and then iterates through the hiearchy. This way, you can easily determine the "top-level father" in the first part of the recursive CTE - something like this:
;WITH ChildParent AS
(
SELECT
ID,
ParentID = ISNULL(ParentID, -1),
SomeName,
PLevel = 1, -- defines level, 1 = TOP, 2 = immediate child nodes etc.
TopLevelFather = ID -- define "top-level" parent node
FROM dbo.[Agent_Agents]
WHERE ParentID IS NULL
UNION ALL
SELECT
a.ID,
ParentID = ISNULL(a.ParentID, -1),
a.SomeName,
PLevel = cp.PLevel + 1,
cp.TopLevelFather -- keep selecting the same value for all child nodes
FROM dbo.[Agent_Agents] a
INNER JOIN ChildParent cp ON r.ParentID = cp.ID
)
SELECT
ID,
ParentID,
SomeName,
PLevel,
TopLevelFather
FROM ChildParent
This would give you nodes something like this (based on your sample data, slightly extended):
ID ParentID SomeName PLevel TopLevelFather
20 -1 Top#20 1 20
4 -1 TOP#4 1 4
8 -1 TOP#8 1 8
7 8 ChildID = 7 2 8
3 7 ChildID = 3 3 8
2 4 ChildID = 2 2 4
9 20 ChildID = 9 2 20
5 9 ChildID = 5 3 20
1 5 ChildID = 1 4 20
Now if you select a particular child node from this CTE output, you'll always get all the infos you need - including the "level" of the child, and its top-level parent node.
Not sure I understand what you are looking for but it could be this.
;WITH c
AS (SELECT childid,
parentid,
parentid AS topParentID
FROM #myTable
WHERE childid = parentid
UNION ALL
SELECT T.childid,
T.parentid,
c.topparentid
FROM #myTable AS T
INNER JOIN c
ON T.parentid = c.childid
WHERE T.childid <> T.parentid)
SELECT childid,
topparentid
FROM c
ORDER BY childid
SE-Data
It is the same as answer by marc_s with the difference that I use your table variable and the fact that you have childID = parentID for root nodes where the answer by marc_s has parent_ID = null for root nodes. In my opinion it is better to have parent_ID = null for root nodes.
I have not yet the time to look further into your question and am not sure whether or not i've understood your problem, but couldn't you use this svf to get the top father's id?
CREATE FUNCTION [dbo].[getTopParent] (
#ChildID INT
)
RETURNS int
AS
BEGIN
DECLARE #result int;
DECLARE #ParentID int;
SET #ParentID=(
SELECT ParentID FROM ChildParent
WHERE ChildID = #ChildID
)
IF(#ParentID IS NULL)
SET #result = #ChildID
ELSE
SET #result = [dbo].[getTopParent](#ParentID)
RETURN #result
END
Then you should be able to find each top parent in this way:
SELECT ChildID
, [dbo].[getTopParent](ChildID) AS TopParentID
FROM ChildParent
select distinct
a.ChildID,a.ParentID,
--isnull(nullif(c.parentID,b.parentID),a.parentID) as toppa,
B.parentID
--,c.parentID
,isnull(nullif(d.parentID,a.parentID),c.parentID) as toppa1,a.name
from myTable a
inner join myTable c
on a.parentID=c.parentID
inner join myTable b
on b.childID=a.parentID
inner join myTable d
on d.childID=b.parentID
I have using the without CTE expression and then using joins to get the step to step parent for child and then more important Common table expressions were introduced in SQL Server 2005 not in server 2000 so using joins to get values this is basic way for to get parentid for a child value
select dbo.[fn_getIMCatPath](8)
select Cat_id,Cat_name,dbo.[fn_getIMCatPath](cat_id) from im_category_master
Create FUNCTION [dbo].[fn_getIMCatPath] (#ID INT)
returns NVARCHAR(1000)
AS
BEGIN
DECLARE #Return NVARCHAR(1000),
#parentID INT,
#iCount INT
SET #iCount = 0
SELECT #Return = Cat_name,
#parentID = parent_id
FROM im_category_master
WHERE [cat_id] = #ID
WHILE #parentID IS NOT NULL
BEGIN
SELECT #Return = cat_name + '>' + #Return,
#parentID = parent_id
FROM im_category_master
WHERE [cat_id] = #parentID
SET #iCount = #iCount + 1
IF #parentID = -1
BEGIN
SET #parentID = NULL
END
IF #iCount > 10
BEGIN
SET #parentID = NULL
SET #Return = ''
END
END
RETURN #Return
END
Consider this sample data and respective SQL to access child records along with their top parent.
Sample DATA
SQL code:
;WITH c AS (
SELECT Id, Name, ParentId as CategoryId,
Id as MainCategoryId, Name AS MainCategory
FROM pmsItemCategory
WHERE ParentId is null
UNION ALL
SELECT T.Id, T.Name, T.ParentId, MainCategoryId, MainCategory
FROM pmsItemCategory AS T
INNER JOIN c ON T.ParentId = c.Id
WHERE T.ParentId is not null
)
SELECT Id, Name, CategoryId, MainCategoryId, MainCategory
FROM c
order by Id
select distinct
a.ChildID,a.ParentID,
--isnull(nullif(c.parentID,b.parentID),a.parentID) as toppa,
B.parentID
--,c.parentID
,isnull(nullif(d.parentID,a.parentID),c.parentID) as toppa1,a.name
from myTable a
inner join myTable c
on a.parentID=c.parentID
inner join myTable b
on b.childID=a.parentID
inner join myTable d
on d.childID=b.parentID
With cte as
(
Select ChileId,Name,ParentId from tblHerarchy
where ParentId is null
union ALL
Select h.ChileId,h.Name,h.ParentId from cte
inner join tblHerarchy h on h.ParentId=cte.ChileId
)
Select * from cte
With cteherarchy as
(
Select ChileId,Name,ParentId from tblHerarchy
where ParentId is null
union ALL
Select h.ChileId,h.Name,h.ParentId from cte
inner join tblHerarchy h on h.ParentId=cte.ChileId
)
Select * from cteherarchy

Order of Recursion (SQL Server CTE)

I can achieve recursion by using SQL Server's With command (CTE).
WITH MyCTE(ParentID,ID,Name,Level)
AS
(
SELECT ManagerID AS ParentID, UserID AS ID, UserName AS Name, 0 AS Level
FROM USERS U
WHERE U.ManagerID IS NULL
UNION ALL
SELECT U.ManagerID AS ParentID, U.UserID AS ID, U.UserName AS Name, H.Level+1 AS Level
FROM USERS U
INNER JOIN MyCTE H ON H.ID = U.ManagerID
)
SELECT ParentID,ID FROM MyCTE
returns
ParentID ID
NULL 1
1 2
1 3
2 4
What I want to achieve is to reverse this result set. Namely,reversing the root node and the deepest child node as,
ParentID ID
NULL 4
4 2
2 1
3 1
Couldn't figure out how to programmatically implement this (preferably by using CTE), like by using a parameter to determine the recursion order etc. Any help is greatly appreciated, thanks.
Edit :
Modified this a bit inserting my first CTE's results into a temp table, then using another recursion I reverse the order as (I know "WHERE T.ID = (SELECT MAX(ID) FROM #tmp)" wont work in a real situation, I also gotta determine the deepest node with the "Level" column, just tried to simplify this for this example),
INSERT INTO #tmp
SELECT ParentID,ID,Level FROM MyCTE
WITH MyCTE2(ParentID,ID,Level)
AS
(
SELECT NULL AS ParentID, ID AS ID, 0 AS Level FROM #tmp T
WHERE T.ID = (SELECT MAX(ID) FROM #tmp)
UNION ALL
SELECT R2.ID AS ParentID, T.ParentID AS ID, R2.Level+1 FROM #tmp T
INNER JOIN MyCTE2 R2 ON R2.ID = T.ID
WHERE T.ParentID IS NOT NULL
)
Original Results (removed the 1,3 pair)
ParentID ID Level
NULL 1 0
1 2 1
2 4 2
Reversed results,
ParentID ID Level
NULL 4 0
4 2 1
2 1 2
Edit 2:
I did something like this,
SELECT TTT.ParentID,TTT.ID,TTT.Level FROM
(
SELECT ParentID,ID,Level FROM MyCTE2
UNION ALL
SELECT TT.ID AS ParentID,TT.ParentID AS ID,(SELECT Level+1 FROM #tmp WHERE ID=TT.ID)
AS Level FROM
(
SELECT ID FROM #tmp
EXCEPT
SELECT ID FROM MyCTE2
)T INNER JOIN #tmp TT ON TT.ID = T.ID
)TTT
ORDER BY TTT.Level
gives,
ParentID ID Level
NULL 4 0
4 2 1
2 1 2
3 1 2
This may contain errors, im not sure yet, just wanted to show to make sure that pair (3,1) is whther correct with level 2 ? Been thinking on this for quite a while now, I might make some silly mistakes.
Sample data
declare #T table
(
ParentID int,
ID int
)
insert into #T values
(NULL, 1),
(1 , 2),
(1 , 3),
(2 , 4)
Recursion from root:
;with C as
(
select ParentID, ID
from #T
where ParentID is null
union all
select T.ParentID, T.ID
from #T as T
inner join C
on T.ParentID = C.ID
)
select *
from C
Result
ParentID ID
----------- -----------
NULL 1
1 2
1 3
2 4
Recursion from leafs:
;with C as
(
select null as PParentID, ID, ParentID
from #T
where ID not in (select ParentID
from #T
where ParentID is not null)
union all
select C.ID, T.ID, T.ParentID
from #T as T
inner join C
on T.ID = C.ParentID
)
select distinct
PParentID as ParentID,
ID
from C
Result:
ParentID ID
----------- -----------
NULL 3
NULL 4
4 2
2 1
3 1
If you have many branches you will have duplicate rows as merge together. Using distinct takes care of that.
To get the levels correct you need to first calculate the level from top down. Store that in a table variable (or temp table) and then use that as the source for leaf->root recursion.
-- Primary key and unique is in there to get the indexes used in the recursion
declare #T2 table
(
ParentID int,
ID int,
Level int,
primary key (ID),
unique(ParentID, ID)
)
;with C as
(
select ParentID, ID, 0 as Level
from #T
where ParentID is null
union all
select T.ParentID, T.ID, Level + 1
from #T as T
inner join C
on T.ParentID = C.ID
)
insert into #T2
select ParentID, ID, Level
from C
;with C as
(
select null as PParentID, ID, ParentID, Level
from #T2
where ID not in (select ParentID
from #T2
where ParentID is not null)
union all
select C.ID, T.ID, T.ParentID, T.Level
from #T2 as T
inner join C
on T.ID = C.ParentID
)
select distinct
PParentID as ParentID,
ID,
max(Level) over() - Level as level
from C
Result:
ParentID ID level
----------- ----------- -----------
NULL 3 1
NULL 4 0
2 1 2
3 1 2
4 2 1
It is possible but a really bad idea to replace #T2 with a multi CTE query. It will kill performance because to first CTE will be rebuilt for each recursion. At least that is my guess of what is happening but believe me it is not fast.

Resources