How to present hierarchical data in SQL Server 2014 - sql-server

I have two tables Company and CompanyRelationShip.
DECLARE #Company TABLE (
CompanyId INT
,RootCompanyId INT
,CompanyName VARCHAR(100)
)
INSERT INTO #Company
VALUES (2,2,'ROOT')
,(106,2,'ABC')
,(105,2,'CDF')
,(3,3,'ROOT2')
,(150,3,'YXZ')
,(151,3,'XZX')
DECLARE #CompanyRelationShip TABLE (
PrimaryCompanyId INT
,CompanyId INT
)
INSERT INTO #CompanyRelationShip
VALUES (2,2)
,(2,106)
,(2,105)
,(106,105)
,(3,3)
,(3,151)
,(3,150)
,(151,150)
I want the result in the below format
CompanyId PrimayCompanyId PrimaryCompanyName RootCompanyId RootCompanyName
2 2 ROOT 2 ROOT
106 2 ROOT 2 ROOT
105 106 ABC 2 ROOT
3 3 ROOT2 3 ROOT2
151 3 ROOT2 3 ROOT2
150 151 XZX 3 ROOT2
I have tried the below query to get the result
WITH PrimayCompany
AS (
SELECT CR.PrimaryCompanyId
,C.CompanyName
FROM #CompanyRelationShip CR
JOIN #Company C ON CR.CompanyId = CR.PrimaryCompanyId
)
,RootCompany
AS (
SELECT RootCompanyId
,CompanyName
FROM #Company
WHERE CompanyId = RootCompanyId
)
SELECT C.CompanyId
,C.RootCompanyId
,RC.CompanyName
,CR.PrimaryCompanyId
,PC.CompanyName
FROM #Company C
LEFT JOIN #CompanyRelationShip CR ON C.CompanyId = CR.PrimaryCompanyId
LEFT JOIN PrimayCompany PC ON PC.PrimaryCompanyId = CR.PrimaryCompanyId
LEFT JOIN RootCompany RC ON RC.RootCompanyId = CR.PrimaryCompanyId
I would really appreciate a bit of help.

In my comment I asked you, why you would need the table #CompanyRelationShip at all... This is just adding a hell of a lot of complexity and potentials for errors.
My suggestion relies on the first table alone. Look, how I've changed the parent IDs of 105 and 151 to place them below in the hierarchy. Just to show the principles I've added a second child below 150:
DECLARE #Company TABLE (
CompanyId INT
,RootCompanyId INT
,CompanyName VARCHAR(100)
);
INSERT INTO #Company
VALUES (2,2,'ROOT')
,(106,2,'ABC')
,(105,106,'CDF')
,(3,3,'ROOT2')
,(150,3,'YXZ')
,(151,150,'XZX')
,(152,150,'Second below 150');
--the query
WITH recCTE AS
(
SELECT CompanyId AS [RootId],CompanyName AS [RootName],*,1 AS HierarchyLevel FROM #Company WHERE CompanyId=RootCompanyId
UNION ALL
SELECT rc.RootId,rc.RootName,c.*,rc.HierarchyLevel+1
FROM #Company c
INNER JOIN recCTE rc ON c.RootCompanyId=rc.CompanyId AND c.CompanyId<>rc.CompanyId
)
SELECT RootId
,RootName
,RootCompanyId AS [PrevId]
,CompanyId
,CompanyName
,HierarchyLevel
FROM recCTE rc
ORDER BY RootId,HierarchyLevel;
The result
RootId RootName PrevId CompanyId CompanyName HierarchyLevel
2 ROOT 2 2 ROOT 1
2 ROOT 2 106 ABC 2
2 ROOT 106 105 CDF 3
3 ROOT2 3 3 ROOT2 1
3 ROOT2 3 150 YXZ 2
3 ROOT2 150 151 XZX 3
3 ROOT2 150 152 Second below 150 3
The idea in short:
We use a a recursive CTE (which is a rather iterative concept actually).
The first SELECT (the anchor) starts with the companies, where the two IDs match.
The second SELECT after UNION ALL picks the next level by joining to the intermediate result line
The two columns RootId and RootName are just passed through to show up in your final set.
The HierarchyLevel is the position within the line, thus placing 105 within ROOT, but below 106.
Hope this helps...
A solution for the given structure
As told, the given structure is not the best choice and should be altered. But if you have to stick to this, you might try something along this:
WITH recCTE AS
(
SELECT CompanyId AS [RootId],CompanyName AS [RootName],*,1 AS HierarchyLevel FROM #Company WHERE CompanyId=RootCompanyId
UNION ALL
SELECT rc.RootId,rc.RootName,c.*,rc.HierarchyLevel+1
FROM #Company c
INNER JOIN recCTE rc ON c.RootCompanyId=rc.CompanyId AND c.CompanyId<>rc.CompanyId
)
SELECT rc.CompanyId
,rc.CompanyName
,COALESCE(crs.PrimaryCompanyId,rc.RootCompanyId) AS ComputedPrevId
,COALESCE(c1.CompanyName,rc.RootName) AS ComputedPrevName
,rc.RootId
,rc.RootName
FROM recCTE rc
LEFT JOIN #CompanyRelationShip crs ON rc.CompanyId=crs.CompanyId AND rc.RootCompanyId<>crs.PrimaryCompanyId
LEFT JOIN #Company c1 ON crs.PrimaryCompanyId=c1.CompanyId
ORDER BY rc.RootId,rc.HierarchyLevel;
This will first use a recursive CTE to find the children below their root companies and the will try to find the corresponding line in your relationship table.
If you use just SELECT * instead of the column list you can see the full set.
Using LEFT JOIN will return NULL, when the ON claus is not met.
COALESCE will return the first non-NULL value, so - hopefully - the one you are after.

Related

Sum of rows with certain values T-SQL

Thanks for all the fast and great answers. But I just found out that in my case, the example table I gave were not correct enough to describe the true situation, so I updated it again. New updated table shown below.
I am having the following queries:
temporary table #d to list the selected domains, temporary table #total to list the total value of these domains and temporary table #speed to list the speeds of these domains.
Then
SELECT domains, total, speed
FROM #d AS d
LEFT JOIN #total AS t ON d.domains = t.domains
LEFT JOIN #speed as S ON d.domains = s.domains
ORDER BY d.domains DESC
The result is as followings:
Domains Total Speed
-----------------------
XYZ AB 10 1
XYZ CD 12 2
XYZ EF 14 3
Bhzu 6 4
Cjuki 19 5
What I wish to have is the SUM of XYZ AB, XYZ CD and XYZ EF like this:
Domains Total Speed Rate (Speed/Total)
---------------------------------------------
XYZ 36 6 6/36
Bhzu 6 4 4/6
Cjuki 19 5 5/19
In the reality, there is some other rows with some other length of Names of domains.
This will be an SSRS automated generation Report, I am using T-SQL.
How could I make it work?
Thank you!
If you want to remove the part of the domain that follows the (first) dot, and group by that, you can do:
select x.domain,
sum(t.total) total,
sum(s.speed) speed,
1.0 * sum(s.speed) / sum(t.total) rate
from #d as d
cross apply (values (left(d.domains, charindex('.', d.domains + '.') - 1))) x(domain)
left join #total as t on d.domains = t.domains
left join #speed as s on d.domains = s.domains
group by x.domains
order by d.domains desc
The upside is that this works regardless of how many characters there are before the dot.
If the data is literally in the form A.a, you could replace Domains with LEFT(Domains, 1), and group by that, then replace Total and Speed with SUM(Total) and SUM(Speed).
SELECT LEFT(domains,1), SUM(total), SUM(speed)
FROM #d AS d
LEFT JOIN #total AS t ON d.domains=t.domains
LEFT JOIN #speed as S ON d.domains=s.domains
GROUP BY LEFT(domains,1)
ORDER BY LEFT(domains,1)
Something like :
WITH T AS
(
SELECT domains,total, speed
FROM #d AS d
LEFT JOIN #total AS t ON d.domains=t.domains
LEFT JOIN #speed as S ON d.domains=s.domains
)
SELECT LEFT(Domains, CHARINDEX('.', Domains) -1) AS DOMAINS, SUM(Total) AS TOTAL, SUM(Seed) AS SPEED, 1.0 * SUM(Total) AS TOTAL, SUM(Seed) AS RATE
FROM T
GROUP BY LEFT(Domains, CHARINDEX('.', Domains) -1)

Recursive hierarchical traversal in MSSQL

Using MSSQL, I am trying to traverse through a table with parent child relationship. I need my result set so that I get all elements in a proper indented manner, till the last leaf, like shown below.
A parent item 36 has 2 children 17 and 18. Each of those children 17 and 18 have one more children to them 26, 42 respectively
36 - 17
17 - 26
36 - 18
18 - 42
But my recursion is working OK in terms of the data traversal, but order wise, it is failing. My recursive query gives me the following output
36 - 17
36 - 18
17 - 26
18 - 42
It brings all levels at once, stores them in a record, then traverses through each of the children of those levels.
Oracle's "connect by prior" seems to be working fine, but, MSSQL is not. I am pasting a sample of what I am using
WITH SRC (Level, PARITEMID, CHIITEMID) AS
(
SELECT
0 as Level,
PI.pitem_id as PARITEMID,
CI.pitem_id as CHIITEMID
FROM PI, CI JOIN <Condition> where PI.PITEM_ID =
UNION ALL
SELECT
Level + 1,
PI1.pitem_id as PARITEMID,
CI1.pitem_id AS CHIITEMID
FROM PI1, CI1 JOIN <Condition>
)
Select * from SRC
Is there something I need to do on the SRC I obtain by ordering it, or is there fundamentally something wrong with the recursion itself?
Wasn't clear on your field names so I assumed the following:
cItem_ID - Child ID
pItem_ID - Parent ID
item_Title - Item name/Description
Also, not clear on the Sequence, So I assumed Item_Title (alphabetical). However, you can use any field available. (see the the "10000+Row_Number()" lines)
I should note, cteR1, and cteR2 are not necessary. I do like the range keys, they server many purposes. If you do remove them, just set the final Order By to Order By A.Seq
Declare #MyTable table (pItem_ID int,cItem_ID int,item_Title varchar(50))
Insert into #MyTable values
(null,36,'Item 36')
,(36,17,'Item 17')
,(17,26,'Item 26')
,(36,18,'Item 18')
,(18,42,'Item 42')
Declare #Top int = null --<< Sets top of Hier Try 7
Declare #Nest varchar(25) = '|-----' --<< Optional: Added for readability
;with cteP as (
Select Seq = cast(10000+Row_Number() over (Order by item_Title) as varchar(500))
,cItem_ID
,pItem_ID
,Lvl=1
,item_Title
From #MyTable
Where IsNull(#Top,-1) = case when #Top is null then isnull(pItem_ID,-1) else cItem_ID end
Union All
Select Seq = cast(concat(p.Seq,'.',10000+Row_Number() over (Order by r.item_Title)) as varchar(500))
,r.cItem_ID
,r.pItem_ID
,p.Lvl+1
,r.item_Title
From #MyTable r
Join cteP p on r.pItem_ID = p.cItem_ID)
,cteR1 as (Select *,R1=Row_Number() over (Order By Seq) From cteP)
,cteR2 as (Select A.Seq,A.cItem_ID,R2=Max(B.R1) From cteR1 A Join cteR1 B on (B.Seq like A.Seq+'%') Group By A.Seq,A.cItem_ID )
Select A.R1
,B.R2
,A.cItem_ID
,A.pItem_ID
,A.Lvl
,item_Title = Replicate(#Nest,A.Lvl-1) + A.item_Title
From cteR1 A
Join cteR2 B on A.cItem_ID=B.cItem_ID
Order By A.R1
Returns

Displaying sorted hierarchy rows in SQL server?

Assuming I have this table : ( c is a child of parent p)
c p
------
40 0
2 3
2 40
3 1
7 2
1 0
Where (0 means root) — I want the order of select to be displayed as :
c b
------
1 0
3 1
2 3
40 0
2 40
7 2
That's becuase we have 2 roots (1,40) and 1 < 40.
So we start at 1 and then display below it - all it's descendants.
Then we get to 40. same logic again.
Question:
How can I do it ?
I've succeeded to display it recursively + finding level of hierarchy*(not sure if it helps though)*
WITH cte(c, p) AS (
SELECT 40, 0 UNION ALL
SELECT 2,3 UNION ALL
SELECT 2,40 UNION ALL
SELECT 3,1 UNION ALL
SELECT 7,2 UNION ALL
SELECT 1,0
) , cte2 AS(
SELECT c,
p,
PLevel = 1
FROM cte
WHERE p = 0
UNION ALL
SELECT cte.c,
cte.p,
PLevel = cte2.PLevel + 1
FROM cte
INNER JOIN cte2
ON cte2.c = cte.p
)
SELECT *
FROM cte2
Full SQL fiddle
You have almost done it. Just add a rank to identify each group and then sort the data on it.
Also, as you are working with more complex hierarchy we need to change the [level] value. In is now not a number, put the full path of the current element to its parent. Where \ means parent. For example the following string:
\1\5\4\1
represents the hierarchy below:
1
--> 5
--> 4
--> 1
I get the idea from hierarchyid type. You may want to consider storing hierarchies using it, as it has handy build-in functions for working with such structures.
Here is full working example with the new data:
DECLARE #DataSource TABLE
(
[c] TINYINT
,[p] TINYINT
);
INSERT INTO #DataSource ([c], [p])
VALUES (1,0)
,(3, 1)
,(2, 3)
,(5,1)
,(7, 2)
,(40, 0)
,(2, 40);
WITH DataSource ([c], [p], [level], [rank])AS
(
SELECT [c]
,[p]
,CAST('/' AS VARCHAR(24))
,ROW_NUMBER() OVER (ORDER BY [c] ASC)
FROM #DataSource
WHERE [p] = 0
UNION ALL
SELECT DS.[c]
,DS.[p]
,CAST(DS1.[level] + CAST(DS.[c] AS VARCHAR(3)) + '/' AS VARCHAR(24))
,DS1.[rank]
FROM #DataSource DS
INNER JOIN DataSource DS1
ON DS1.[c] = DS.[p]
)
SELECT [c]
,[p]
FROM DataSource
ORDER BY [Rank]
,CAST([level] AS hierarchyid);
Again, pay attention to the node (7,2) which is participating in the two groups (even in your example). I guess this is just a sample data and you have a way to defined where the node should be included.

how to do a FOR EACH loop in T-Sql using join?

I have a table PRODUCT that is basically set up so there is an PRODUCTID, PRODUCTNAME... it looks sort of like this
PRODUCTID PRODUCTNAME
100 PNAME1
101 PNAME2
102 PNAME3
Now I have to insert a record into new table PRODUCTMAPPING for each row in the PRODUCT.
so my new table PRODUCTMAPPING should look like this
PRODUCTMAPPINGID PRODUCTID
1 100
1 101
1 102
2 100
2 101
2 102
3 100
and so on ....
I tried doing while but I need it using Join.
Can we acheive this using joins ?
Thanks in advance.
One way;
select
row_number() over(partition by a.PRODUCTID order by a.PRODUCTID) PRODUCTMAPPINGID,
a.PRODUCTID
from PRODUCT a, PRODUCT b
Using LOOP
The following example specifies that the JOIN operation in the query is performed by a LOOP join.
Select sp.Name,spqh.quota
FROM Sales.SalesPersonQuotaHistory AS spqh
INNER LOOP JOIN Sales.SalesPerson AS sp
ON spqh.SalesPersonID = sp.SalesPersonID
WHERE sp.SalesYTD > 2500000.00;
GO
Refer this MSDN link
INSERT
INTO dbo.PRODUCTMAPPING
(
PRODUCTMAPPINGID
,PRODUCTID
)
SELECT pmv.PRODUCTMAPPINGID
,p.PRODUCTID
FROM dbo.Product p
CROSS JOIN
(
SELECT pm.ProductMappingID
FROM dbo.ProductMappingValues pmv -- WHERE DO THESE COME FROM?
) pmv

SQL Server: How to limit CTE recursion to rows just recursivly added?

Simpler Example
Let's try a simpler example, so people can wrap their heads around the concepts, and have a practical example that you can copy&paste into SQL Query Analizer:
Imagine a Nodes table, with a heirarchy:
A
- B
- C
We can start testing in Query Analizer:
CREATE TABLE ##Nodes
(
NodeID varchar(50) PRIMARY KEY NOT NULL,
ParentNodeID varchar(50) NULL
)
INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('A', null)
INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('B', 'A')
INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('C', 'B')
Desired output:
ParentNodeID NodeID GenerationsRemoved
============ ====== ==================
NULL A 1
NULL B 2
NULL C 3
A B 1
A C 2
B C 1
Now the suggested CTE expression, with it's incorrect output:
WITH NodeChildren AS
(
--initialization
SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
FROM ##Nodes
WHERE ParentNodeID IS NULL
UNION ALL
--recursive execution
SELECT P.ParentNodeID, N.NodeID, P.GenerationsRemoved + 1
FROM NodeChildren AS P
INNER JOIN ##Nodes AS N
ON P.NodeID = N.ParentNodeID
)
SELECT ParentNodeID, NodeID, GenerationsRemoved
FROM NodeChildren
Actual output:
ParentNodeID NodeID GenerationsRemoved
============ ====== ==================
NULL A 1
NULL B 2
NULL C 3
Note: If SQL Server 2005† CTE cannot do what i was doing before in 2000‡, that's fine, and that's the answer. And whoever gives "it's not possible" as the answer will win the bounty. But i will wait a few days to make sure everyone concur's that it's not possible before i irrecovably give 250 reputation for a non-solution to my problem.
Nitpickers Corner
†not 2008
‡without resorting to a UDF*, which is the solution already have
*unless you can see a way to improve the performance of the UDF in the original question
Original Question
i have a table of Nodes, each with a parent that points to another Node (or to null).
For illustration:
1 My Computer
2 Drive C
4 Users
5 Program Files
7 Windows
8 System32
3 Drive D
6 mp3
i want a table that returns all the parent-child relationships, and the number of generations between them
For for all direct parent relationships:
ParentNodeID ChildNodeID GenerationsRemoved
============ =========== ===================
(null) 1 1
1 2 1
2 4 1
2 5 1
2 7 1
1 3 1
3 6 1
7 8 1
But then there's the grandparent relationships:
ParentNodeID ChildNodeID GenerationsRemoved
============ =========== ===================
(null) 2 2
(null) 3 2
1 4 2
1 5 2
1 7 2
1 6 2
2 8 2
And the there's the great-grand-grandparent relationships:
ParentNodeID ChildNodeID GenerationsRemoved
============ =========== ===================
(null) 4 3
(null) 5 3
(null) 7 3
(null) 6 3
1 8 3
So i can figure out the basic CTE initialization:
WITH (NodeChildren) AS
{
--initialization
SELECT ParentNodeID, NodeID AS ChildNodeID, 1 AS GenerationsRemoved
FROM Nodes
}
The problem now is the recursive part. The obvious answer, of course, doesn't work:
WITH (NodeChildren) AS
{
--initialization
SELECT ParentNodeID, ChildNodeID, 1 AS GenerationsRemoved
FROM Nodes
UNION ALL
--recursive execution
SELECT parents.ParentNodeID, children.NodeID, parents.Generations+1
FROM NodeChildren parents
INNER JOIN NodeParents children
ON parents.NodeID = children.ParentNodeID
}
Msg 253, Level 16, State 1, Line 1
Recursive member of a common table expression 'NodeChildren' has multiple recursive references.
All the information needed to generate the entire recursive list is present in the inital CTE table. But if that's not allowed i'll try:
WITH (NodeChildren) AS
{
--initialization
SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
FROM Nodes
UNION ALL
--recursive execution
SELECT parents.ParentNodeID, Nodes.NodeID, parents.Generations+1
FROM NodeChildren parents
INNER JOIN Nodes
ON parents.NodeID = nodes.ParentNodeID
}
But that fails because it's not only joining on the recursive elements, but keeps recursivly adding the same rows over and over:
Msg 530, Level 16, State 1, Line 1
The statement terminated. The maximum recursion 100 has been exhausted before statement completion.
In SQL Server 2000 i simulated a CTE by using a User Defined Function (UDF):
CREATE FUNCTION [dbo].[fn_NodeChildren] ()
RETURNS #Result TABLE (
ParentNodeID int NULL,
ChildNodeID int NULL,
Generations int NOT NULL)
AS
/*This UDF returns all "ParentNode" - "Child Node" combinations
...even multiple levels separated
BEGIN
DECLARE #Generations int
SET #Generations = 1
--Insert into the Return table all "Self" entries
INSERT INTO #Result
SELECT ParentNodeID, NodeID, #Generations
FROM Nodes
WHILE ##rowcount > 0
BEGIN
SET #Generations = #Generations + 1
--Add to the Children table:
-- children of all nodes just added
-- (i.e. Where #Result.Generation = CurrentGeneration-1)
INSERT #Result
SELECT CurrentParents.ParentNodeID, Nodes.NodeID, #Generations
FROM Nodes
INNER JOIN #Result CurrentParents
ON Nodes.ParentNodeID = CurrentParents.ChildNodeID
WHERE CurrentParents.Generations = #Generations - 1
END
RETURN
END
And the magic to keep it from blowing up was the limiting where clause:
WHERE CurrentParents.Generations - #Generations-1
How do you prevent a recursive CTE from recursing forever?
Try this:
WITH Nodes AS
(
--initialization
SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
FROM ##Nodes
UNION ALL
----recursive execution
SELECT P.ParentNodeID, N.NodeID, P.GenerationsRemoved + 1
FROM Nodes AS P
INNER JOIN ##Nodes AS N
ON P.NodeID = N.ParentNodeID
WHERE P.GenerationsRemoved <= 10
)
SELECT ParentNodeID, NodeID, GenerationsRemoved
FROM Nodes
ORDER BY ParentNodeID, NodeID, GenerationsRemoved
Basically removing the "only show me absolute parents" from the initialization query; That way it generates the results starting from each of them and decending from there. I also added in the "WHERE P.GenerationsRemoved <= 10" as an infinite recursion catch(replace 10 with any number up to 100 to fit your needs). Then add the sort so it looks like the results you wanted.
Aside: do you have SQL Server 2008? This might be suited to the hierarchyid data type.
If I understand your intentions you can get you result by doing something like this:
DECLARE #StartID INT;
SET #StartID = 1;
WITH CTE (ChildNodeID, ParentNodeID, [Level]) AS
(
SELECT t1.ChildNodeID,
t1.ParentNodeID,
0
FROM tblNodes AS t1
WHERE ChildNodeID = #StartID
UNION ALL
SELECT t1.ChildNodeID,
t1.ParentNodeID,
t2.[Level]+1
FROM tblNodes AS t1
INNER JOIN CTE AS t2 ON t1.ParentNodeID = t2.ChildNodeID
)
SELECT t1.ChildNodeID, t2.ChildNodeID, t1.[Level]- t2.[Level] AS GenerationsDiff
FROM CTE AS t1
CROSS APPLY CTE t2
This will return the generation difference between all nodes, you can modify it for you exact needs.
Well, your answer is not quite that obvious :-)
WITH (NodeChildren) AS
{
--initialization
SELECT ParentNodeID, ChildNodeID, 1 AS GenerationsRemoved
FROM Nodes
This part is called the "anchor" part of the recursive CTE - but it should really only select one or a select few rows from your table - this selects everything!
I guess what you're missing here is simply a suitable WHERE clause:
WITH (NodeChildren) AS
{
--initialization
SELECT ParentNodeID, ChildNodeID, 1 AS GenerationsRemoved
FROM Nodes
**WHERE ParentNodeID IS NULL**
However, I am afraid your requirement to have not just the "straight" hierarchy, but also the grandparent-child rows, might not be that easy to satisfy.... normally recursive CTE will only ever show one level and its direct subordinates (and that down the hierarchy, of course) - it doesn't usually skip one, two or even more levels.
Hope this helps a bit.
Marc
The issue is with the Sql Server default recursion limit (100). If you try your example at the top with the anchor restriction removed (also added Order By):
WITH NodeChildren AS
(
--initialization
SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
FROM Nodes
UNION ALL
--recursive execution
SELECT P.ParentNodeID, N.NodeID, P.GenerationsRemoved + 1
FROM NodeChildren AS P
inner JOIN Nodes AS N
ON P.NodeID = N.ParentNodeID
)
SELECT ParentNodeID, NodeID, GenerationsRemoved
FROM NodeChildren
ORDER BY ParentNodeID ASC
This produces the desired results. The problem you aare facing is with a larger number of rows you will recirse over 100 times which is a default limit. This can be changed by adding option (max recursion x) after your query, where x is a number between 1 and 32767. x can also be set to 0 which sets no limit however could very quickly have a very detrimental impact on your server performance. Clearly as the number of rows in Nodes increases, the number of recursions will can rise very quickly and I would avoid this approach unless there was a known upper limit on the rows in the table. For completeness, the final query should look like:
WITH NodeChildren AS
(
--initialization
SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
FROM Nodes
UNION ALL
--recursive execution
SELECT P.ParentNodeID, N.NodeID, P.GenerationsRemoved + 1
FROM NodeChildren AS P
inner JOIN Nodes AS N
ON P.NodeID = N.ParentNodeID
)
SELECT *
FROM NodeChildren
ORDER BY ParentNodeID
OPTION (MAXRECURSION 32767)
Where 32767 could be adjusted downwards to fit your scenario
Have you tried constructing a path in the CTE and using it to identify ancestors?
You can then subtract the descendant node depth from the ancestor node depth to calculate the GenerationsRemoved column, like so...
DECLARE #Nodes TABLE
(
NodeId varchar(50) PRIMARY KEY NOT NULL,
ParentNodeId varchar(50) NULL
)
INSERT INTO #Nodes (NodeId, ParentNodeId) VALUES ('A', NULL)
INSERT INTO #Nodes (NodeId, ParentNodeId) VALUES ('B', 'A')
INSERT INTO #Nodes (NodeId, ParentNodeId) VALUES ('C', 'B')
DECLARE #Hierarchy TABLE
(
NodeId varchar(50) PRIMARY KEY NOT NULL,
ParentNodeId varchar(50) NULL,
Depth int NOT NULL,
[Path] varchar(2000) NOT NULL
)
WITH Hierarchy AS
(
--initialization
SELECT NodeId, ParentNodeId, 0 AS Depth, CONVERT(varchar(2000), NodeId) AS [Path]
FROM #Nodes
WHERE ParentNodeId IS NULL
UNION ALL
--recursive execution
SELECT n.NodeId, n.ParentNodeId, p.Depth + 1, CONVERT(varchar(2000), p.[Path] + '/' + n.NodeId)
FROM Hierarchy AS p
INNER JOIN #Nodes AS n
ON p.NodeId = n.ParentNodeId
)
INSERT INTO #Hierarchy
SELECT *
FROM Hierarchy
SELECT parent.NodeId AS AncestorNodeId, child.NodeId AS DescendantNodeId, child.Depth - parent.Depth AS GenerationsRemoved
FROM #Hierarchy AS parent
INNER JOIN #Hierarchy AS child
ON child.[Path] LIKE parent.[Path] + '/%'
This breaks the recursion limit imposed on Chris Shaffer's answer.
I create a table with a cycle:
CREATE TABLE ##Nodes
(
NodeID varchar(50) PRIMARY KEY NOT NULL,
ParentNodeID varchar(50) NULL
)
INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('A', 'C');
INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('B', 'A');
INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('C', 'B');
In cases where there is a potential cycle (i.e. ParentNodeId IS NOT NULL), the generation removed is started at 2. We can then identity cycles by checking (P.ParentNodeID == N.NodeID), which we simply don't add it. Afterwards, we append the omitted generation remove = 1.
WITH ParentNodes AS
(
--initialization
SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
FROM ##Nodes
WHERE ParentNodeID IS NULL
UNION ALL
SELECT P.ParentNodeID, N.NodeID, 2 AS GenerationsRemoved
FROM ##Nodes N
JOIN ##Nodes P ON N.ParentNodeID=P.NodeID
WHERE P.ParentNodeID IS NOT NULL
UNION ALL
----recursive execution
SELECT P.ParentNodeID, N.NodeID, P.GenerationsRemoved + 1
FROM ParentNodes AS P
INNER JOIN ##Nodes AS N
ON P.NodeID = N.ParentNodeID
WHERE P.ParentNodeID IS NULL OR P.ParentNodeID <> N.NodeID
),
Nodes AS (
SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
FROM ##Nodes
WHERE ParentNodeID IS NOT NULL
UNION ALL
SELECT ParentNodeID, NodeID, GenerationsRemoved FROM ParentNodes
)
SELECT ParentNodeID, NodeID, GenerationsRemoved
FROM Nodes
ORDER BY ParentNodeID, NodeID, GenerationsRemoved
with cte as
(
select a=65, L=1
union all
select a+1, L=L+1
from cte
where L<=100
)
select
IsRecursion=Case When L>1 then 'Recursion' else 'Not Recursion' end,
AsciiValue=a,
AsciiCharacter=char(a)
from cte
Create a column containing the current level.
Check if the level is >1
My example here shows a recursive CTE that stops recursion after 100 levels (the max). As a bonus, it displays a bunch of ASCII characters and the corresponding numeric value.

Resources