accessing parent nodes in same level in T-SQL - sql

Let's say I have a database schema like this:
RowId ParentId Name
------ ---------- ------
1 NULL Level1
2 NULL Level2
3 1 Leaf1
4 1 Leaf2
5 2 Leaf1
6 3 LeafX
Basically, the tree would look as such:
Level1
Leaf1
LeafX
Leaf2
Level2
Leaf1
I need to extract all ancestor LEVEL of LeafX in the most efficient and dynamic way.
So it will output: Leaf1, Leaf2, and Leaf1 (of Level2)
How do I do this in T-SQL? Thanks

This will give you the result you want.
;with C as
(
select T.rowid,
T.parentid,
T.name,
1 as Lvl
from YourTable as T
where T.parentid is null
union all
select T.rowid,
T.parentid,
T.name,
C.Lvl + 1
from YourTable as T
inner join C
on T.parentid = C.rowid
)
select *
from C
where C.Lvl = (
select C.lvl-1
from C
where C.name = 'LeafX'
)
Update
And this might be faster for you. You have to test on your data.
declare #Level int;
with C as
(
select T.rowid,
T.parentid
from #t as T
where T.name = 'LeafX'
union all
select T.rowid,
T.parentid
from #t as T
inner join C
on T.rowid = C.parentid
)
select #Level = count(*) - 1
from C;
with C as
(
select T.rowid,
T.parentid,
T.name,
1 as Lvl
from #t as T
where T.parentid is null
union all
select T.rowid,
T.parentid,
T.name,
C.Lvl + 1
from #t as T
inner join C
on T.parentid = C.rowid
where C.Lvl < #Level
)
select *
from C
where C.Lvl = #Level;

There's a few methods to do that. My favourite is to create special table Trees_Parents, where you will store every parent for evere node.
So if have structure like that
RowId ParentId Name
------ ---------- ------
1 NULL Level1
2 NULL Level2
3 1 Leaf1
4 1 Leaf2
5 2 Leaf1
6 3 LeafX
your Trees_Parents table will looks like
RowId ParentId
------ ----------
1 1
2 2
3 3
3 1
4 4
4 1
5 5
5 2
6 6
6 1
6 3
then when you need to retrieve all children you just write
select RowID from Trees_Parents where ParentId = 1
I'm storing row self in this table to avoid unions, if you don't need it you can write
select RowID from Trees_Parents where ParentId = 1 and ParentId <> RowId
And for all parents you'll write
select ParentId from Trees_Parents where RowId = 6 and ParentId <> RowId
You can also store Table_Name in table Trees_Parents so you can use it for different tables
Another way is to write recursive WITH clause, but if your tree is big and it's not changing frequently I think it's better to store parents data in additional table

Well you can use recursive solution. You need to get all nodes with Depth = Depth of your node - 1
declare #Temp table (RowId int, ParentId int, Name nvarchar(128))
insert into #Temp
select 1, null, 'Level1' union all
select 2, null, 'Level2' union all
select 3, 1, 'Leaf1' union all
select 4, 1, 'Leaf2' union all
select 5, 2, 'Leaf3' union all
select 6, 3, 'LeafX';
with Parents
as
(
select T.RowId, 0 as Depth from #Temp as T where T.ParentId is null
union all
select T.RowId, P.Depth + 1
from Parents as P
inner join #Temp as T on T.ParentId = P.RowId
)
select T.Name
from Parents as P
outer apply (select TT.Depth from Parents as TT where TT.RowId = 6) as CALC
left outer join #Temp as T on T.RowId = P.RowId
where P.Depth = CALC.Depth - 1

declare #t table(rowid int, parentid int, name varchar(10))
insert #t values(1,NULL,'Level1')
insert #t values(2,NULL,'Level2')
insert #t values(3,1,'Leaf1')
insert #t values(4,1,'Leaf2')
insert #t values(5,2,'Leaf1')
insert #t values(6,3,'LeafX')
;with a as
(
select rowid, parentid, 0 level from #t where name = 'leafx'
union all
select t.rowid, t.parentid, level + 1 from #t t
join a on a.parentid = t.rowid
), b as
(
select rowid, parentid,name, 0 level from #t where parentid is null
union all
select t.rowid, t.parentid,t.name, level + 1
from b join #t t on b.rowid = t.parentid
)
select rowid, parentid, name from b
where level = (select max(level)-1 from a)
rowid parentid name
5 2 Leaf1
3 1 Leaf1
4 1 Leaf2

Related

Sql Server 2014 - Deep Recursive Parent-Child Self Join

I am trying to build a deep recursive self-join query. Having the table like:
Id | ParentId
1 | NULL
2 | 1
3 | 1
4 | 2
5 | 3
6 | 8
7 | 9
For Id 1 my query should be fetching 1,2,3,4,5 since they are either the children of 1 or children of the children of 1. In the given example 6 and 7 should not be included in the query result.
I tried using CTE but I am getting tons of duplicates:
WITH CTE AS (
SELECT Id, ParentId
FROM dbo.Table
WHERE ParentId IS NULL
UNION ALL
SELECT t.Id, t.ParentId
FROM dbo.Table t
INNER JOIN CTE c ON t.ParentId = c.Id
)
SELECT * FROM CTE
Ideas?
You can try to use DISTINCT to filter duplicate rows.
;WITH CTE AS (
SELECT Id, ParentId
FROM T
WHERE ParentId IS NULL
UNION ALL
SELECT t.Id, t.ParentId
FROM T
INNER JOIN CTE c ON t.ParentId = c.Id
)
SELECT DISTINCT Id, ParentId
FROM CTE
Try the following query with CTE where you can set parentId by #parentID:
DECLARE #parentID INT = 1
;WITH cte AS
(
SELECT
t.ID
, t.ParentId
FROM #table t
),
cteParent AS
(
SELECT
t.ID
, t.ParentId
FROM #table t
WHERE t.ParentId IN (SELECT t1.ID FROM #table t1 WHERE T1.ParentId = #parentID)
)
SELECT
DISTINCT c1.ID
, c1.ParentId
FROM cte c1
INNER JOIN cte c2 ON c2.ParentId = c1.ID
UNION ALL
SELECT *
FROM cteParent
And the sample data:
DECLARE #table TABLE
(
ID INT
, ParentId INT
)
INSERT INTO #table
(
ID,
ParentId
)
VALUES
(1, NULL )
, (2, 1 )
, (3, 1 )
, (4, 2 )
, (5, 3 )
, (6, 8 )
, (7, 9 )
OUTPUT:
ID ParentId
1 NULL
2 1
3 1
4 2
5 3
I don't see duplicates.
Your code returns the following on the data you provided:
Id ParentId
1
2 1
3 1
5 3
4 2
which is what you want.
Here is a db<>fiddle.
Here is the code:
WITH t as (
SELECT *
FROM (VALUES (1, NULL), (2, 1), (3, 1), (4, 2), (5, 3), (6, 8), (7, 9)
) v(id, parentId)
),
CTE AS (
SELECT Id, ParentId
FROM t
WHERE ParentId IS NULL
UNION ALL
SELECT t.Id, t.ParentId
FROM t
INNER JOIN CTE c ON t.ParentId = c.Id
)
SELECT *
FROM CTE;
If you are getting duplicates in your actual result set, then you presumably have duplicates in your original table. I would recommend removing them before doing the recursive logic:
with t as (
select distinct id, parentid
from <your query>
),
. . .
Then run the recursive logic.
Try this sql script which result Parent Child Hierarchy
;WITH CTE(Id , ParentId)
AS
(
SELECT 1 , NULL UNION ALL
SELECT 2 , 1 UNION ALL
SELECT 3 , 1 UNION ALL
SELECT 4 , 2 UNION ALL
SELECT 5 , 3 UNION ALL
SELECT 6 , 8 UNION ALL
SELECT 7 , 9
)
,Cte2
AS
(
SELECT Id ,
ParentId ,
CAST('\'+ CAST(Id AS VARCHAR(MAX))AS VARCHAR(MAX)) AS [Hierarchy]
FROM CTE
WHERE ParentId IS NULL
UNION ALL
SELECT c1.Id ,
c1.ParentId ,
[Hierarchy]+'\'+ CAST(c1.Id AS VARCHAR(MAX)) AS [Hierarchy]
FROM Cte2 c2
INNER JOIN CTE c1
ON c1.ParentId = c2.Id
)
SELECT Id,
RIGHT([Hierarchy],LEN([Hierarchy])-1) AS ParentChildHierarchy
FROM Cte2
GO
Result
Id ParentChildHierarchy
-------------------------
1 1
2 1\2
3 1\3
5 1\3\5
4 1\2\4
This query will help you
CREATE TABLE #table( ID INT, ParentId INT )
INSERT INTO #table(ID,ParentId)
VALUES (1, NULL ), (2, 1 ), (3, 1 ), (4, 2 ), (5, 3 ), (6, 8 ), (7, 9 )
;WITH CTE AS (
SELECT ID FROM #table WHERE PARENTID IS NULL
UNION ALL
SELECT T.ID FROM #table T
INNER JOIN #table T1 ON T.PARENTID =T1.ID
) SELECT * FROM CTE

How to select [ParentName] from the same table based on [ParentId]

I have a table structure like so (simplified):
Id ParentId Name Desc
------------------------------
1 NULL A
2 NULL B
3 1 A1
4 1 A2
5 2 B1
It's a big table and can be very painful to look through. So I want to create a view that will display data in slightly better way:
Id ParentId ParentName Name Desc
--------------------------------------------
1 NULL A
3 1 A A1
4 1 A A2
2 NULL B
5 2 B B1
My problem is getting that ParentName into SELECT query. I obviously (tried) can't do:
SELECT Id, ParentId, (SELECT Name FROM myTable WHERE Id = ParentId) AS ParentName, Name, Desc FROM myTable INNER JOIN .. WHERE ... GROUP BY etc.
Do I have to resort to Common Table Expression (CTE) to get this done ?
Instead of using CTE, You can consider to do that by simple self-join concept:
Select T1.Id, T1.ParentId, ParentTable.Name as ParentName, T1.Name, T1.[Desc]
From tableName T1
Left join tableName ParentTable
on T1.ParentId = ParentTable.Id
order by Name
More reference on Self-Join from Microsoft's TechNet (ref pointer credited to destination-data)
Your example has one parent level only. If there might be deeper nestings you could use a recursive CTE. In this example I added two more elements as children below children:
DECLARE #tbl TABLE(Id INT,ParentId INT, Name VARCHAR(MAX));
INSERT INTO #tbl VALUES
(1,NULL,'A')
,(2,NULL,'B')
,(3,1,'A1')
,(4,1,'A2')
,(5,2,'B1')
,(6,4,'A2a')
,(7,6,'A2a1')
;
WITH RecursiveCTE AS
(
SELECT Id,ParentId,Name,Name AS FullPath
FROM #tbl AS tbl
WHERE ParentId IS NULL
UNION ALL
SELECT a.*
,derived.FullPath + '/' + a.Name
FROM RecursiveCTE AS derived
CROSS APPLY (SELECT Id,ParentId,Name FROM #tbl AS innerTbl WHERE innerTbl.ParentId=derived.Id) AS a
)
SELECT * FROM RecursiveCTE
The result
Id ParentId Name FullPath
1 NULL A A
2 NULL B B
5 2 B1 B/B1
3 1 A1 A/A1
4 1 A2 A/A2
6 4 A2a A/A2/A2a
7 6 A2a1 A/A2/A2a/A2a1

Get rows in all levels with parentId SQL

I have a table Categories, and each row in this tables have a parentId. The parent is just an other row in the same table.
I want to create query to get all children in different levels by just giving the Id of the first Parent.
With recursive cte:
DECLARE #t TABLE ( id INT, pid INT )
INSERT INTO #t
VALUES ( 1, NULL ),
( 2, NULL ),
( 3, 1 ),
( 4, 1 ),
( 5, 3 ),
( 6, 5 ),
( 7, 6 ),
( 8, 6 )
DECLARE #p INT = 1;
WITH cte
AS ( SELECT *
FROM #t
WHERE pid = #p
UNION ALL
SELECT t.*
FROM #t t
JOIN cte c ON c.id = t.pid
)
SELECT *
FROM cte c
Output:
id pid
3 1
4 1
5 3
6 5
7 6
8 6
EDIT:
To use in another select statement:
WITH cte
AS ( SELECT *
FROM #t
WHERE pid = #p
UNION ALL
SELECT t.*
FROM #t t
JOIN cte c ON c.id = t.pid
)
SELECT *
FROM cte c
JOIN AnotherTable t on c.id = t.id

Get all parents in parent child hierarchy

I have the following data in my table
ID NAME PARENTID STATUS
----------- ------------------------------ ----------- ------
1 Folder A 0 0
2 Folder B 1 0
3 Folder C 2 1
4 Folder D 1 0
5 Folder E 4 0
6 Folder G 5 0
7 Folder H 6 1
Above records are from table [#Temp].
[Name] - Folder Name
[ID] - Unique ID of folder in database (identity)
[ParentID] - Represents the parent of current folder.
Query to populate above data in the table:
DECLARE #Temp TABLE
(
[ID] INT IDENTITY(1, 1) ,
[NAME] VARCHAR(30) ,
[PARENTID] INT,
[STATUS] BIT
)
INSERT INTO #Temp
SELECT 'Folder A' ,
0, 0
UNION
SELECT 'Folder B' ,
1, 0
UNION
SELECT 'Folder C' ,
2, 1
UNION
SELECT 'Folder D' ,
1, 0
UNION
SELECT 'Folder E' ,
4, 0
UNION
SELECT 'Folder G' ,
5, 0
UNION
SELECT 'Folder H' ,
6, 1
I am having the following query to get records where status=1
SELECT *
FROM #Temp WHERE [STATUS]=1
which gives the following output
ID NAME PARENTID STATUS
----------- ------------------------------ ----------- ------
3 Folder C 2 1
7 Folder H 6 1
My goal is to fetch those records too which are the parents (till parentid=0) of the records came with the above query. i.e I want to get this output which contains the parents of Folder C and Folder H:
ID NAME PARENTID STATUS
----------- ------------------------------ ----------- ------
1 Folder A 0 0
2 Folder B 1 0
3 Folder C 2 1
4 Folder D 1 0
5 Folder E 4 0
6 Folder G 5 0
7 Folder H 6 1
DECLARE #Temp TABLE
(
[ID] INT IDENTITY(1, 1) ,
[NAME] VARCHAR(30) ,
[PARENTID] INT ,
[STATUS] BIT
)
INSERT INTO #Temp
SELECT 'Folder A' ,
0 ,
0
UNION
SELECT 'Folder B' ,
1 ,
0
UNION
SELECT 'Folder C' ,
2 ,
1
UNION
SELECT 'Folder D' ,
1 ,
0
UNION
SELECT 'Folder E' ,
4 ,
0
UNION
SELECT 'Folder G' ,
5 ,
1
UNION
SELECT 'Folder H' ,
6 ,
0
CREATE TABLE #TempTable
(
[RowIndex] INT IDENTITY(1, 1) ,
[ID] INT ,
[ParentID] INT
)
DECLARE #TempIds TABLE ( [ID] INT )
INSERT INTO #TempTable
( [ID] ,
[ParentID]
)
SELECT [ID] ,
[ParentID]
FROM #Temp
WHERE [STATUS] = 1
DECLARE #intFlag INT
DECLARE #Count INT
SET #intFlag = 1
SELECT #Count = COUNT(1)
FROM #Temp
WHILE ( #intFlag <= #Count )
BEGIN
PRINT #intFlag
DECLARE #ID INT
SELECT #ID = [ParentID]
FROM #TempTable
WHERE RowIndex = #intFlag
SET #intFlag = #intFlag + 1;
WITH CTE
AS ( SELECT ID ,
ParentID
FROM #Temp
WHERE ID = #ID
UNION ALL
SELECT MainTbl.ID ,
MainTbl.ParentID
FROM #Temp MainTbl
INNER JOIN cte ON MainTbl.ID = cte.ParentID
)
INSERT INTO #TempIds
SELECT ID
FROM CTE
END
;WITH cte
AS ( SELECT DISTINCT
ID
FROM #TempIds
)
SELECT T.*
FROM cte
INNER JOIN #Temp T ON [T].ID = cte.ID
UNION
SELECT T1.*
FROM #TempTable TempTable
INNER JOIN #Temp T1 ON [TempTable].ID = T1.ID
DROP TABLE #TempTable
Output:
ID NAME PARENTID STATUS
----------- ------------------------------ ----------- ------
1 Folder A 0 0
2 Folder B 1 0
3 Folder C 2 1
4 Folder D 1 0
5 Folder E 4 0
6 Folder G 5 1
I made a recursive CTE - I start with the bottom, any folder that does not have a child. From there, I pass up whether or not a '1' Status was encountered in the 'GOOD' column, and the distinct clears out the duplicates created by starting at the bottom.
;WITH CTE AS
(
SELECT ID, NAME, PARENTID, STATUS GOOD, STATUS
FROM #Temp T1
WHERE NOT EXISTS (SELECT * FROM #Temp T2 WHERE T2.PARENTID = T1.ID)
UNION ALL
SELECT T.ID, T.NAME, T.PARENTID, T.STATUS | C.GOOD GOOD, T.STATUS
FROM #Temp T
JOIN CTE C
ON C.PARENTID = T.ID
)
SELECT DISTINCT ID, NAME, PARENTID, STATUS FROM CTE
WHERE GOOD = 1
ORDER BY ID ASC

CTE parent-child showing siblings

I have a CTE-query that displays a tree using recursion. This works great when displaying the whole tree. But I want to pass in ID as a variable and include the siblings for each current node.
Testcode:
DECLARE #TT TABLE
(
ID int,
Name varchar(25),
ParentID int,
SortIndex int
)
INSERT #TT
SELECT 1, 'A', NULL, 1 UNION ALL
SELECT 2, 'B_1', 3, 1 UNION ALL
SELECT 3, 'B', 1, 2 UNION ALL
SELECT 4, 'B_2', 3, 2 UNION ALL
SELECT 5, 'C', 1, 3 UNION ALL
SELECT 6, 'C_2', 5, 2 UNION ALL
SELECT 7, 'A_1', 1, 1 UNION ALL
SELECT 8, 'A_2', 1, 2 UNION ALL
SELECT 9, 'C_1', 5, 1
;WITH CTETree
AS
(
SELECT *, CAST(NULL AS VARCHAR(25)) AS ParentName, 1 AS Lev,
CAST(ROW_NUMBER() OVER(ORDER BY SortIndex) AS VARBINARY(MAX)) AS SortPath
FROM #TT
WHERE ParentID IS NULL
UNION ALL
SELECT F.*, CTETree.Name AS ParentName, Lev + 1,
SortPath + CAST(ROW_NUMBER() OVER(ORDER BY F.SortIndex) AS BINARY(32))
FROM #TT AS F
INNER JOIN CTETree
ON F.ParentID = CTETree.ID
)
SELECT * FROM CTETree
ORDER BY SortPath
/*
DESIRED RESULT:
WHEN ID = 3 PASSED IN:
1 A NULL 1 NULL 1
3 B 1 2 A 2
2 B_1 3 1 B 3
4 B_2 3 2 B 3
5 C 1 3 A 2
WHEN ID = 1 PASSED IN:
1 A NULL 1 NULL 1
3 B 1 2 A 2
5 C 1 3 A 2
WHEN ID = 9 PASSED IN:
1 A NULL 1 NULL 1
3 B 1 2 A 2
5 C 1 3 A 2
9 C_1 5 1 C 3
6 C_2 5 2 C 3
*/
SQL-Fiddle: http://sqlfiddle.com/#!3/d41d8/5526
Only in once I confront a question where disappear A_1 and A_2. Therefore add in exception(in case clause).
If you need this records delete this expression AND f.ID != 7 AND f.ID != 8.
DECLARE #ID int = 3 -- variable wich you want pass
DECLARE #Matched int = (SELECT CASE WHEN ParentID = 1 THEN ID ELSE ParentID END FROM #TT WHERE ID = #ID)
;WITH CTETree
AS
(
SELECT *, CAST(NULL AS VARCHAR(25)) AS ParentName, 1 AS Lev,
CAST(ROW_NUMBER() OVER(ORDER BY SortIndex) AS VARBINARY(MAX)) AS SortPath,
0 AS Matched
FROM #TT
WHERE ParentID IS NULL
UNION ALL
SELECT F.*, cte.Name AS ParentName, cte.Lev + 1,
SortPath + CAST(ROW_NUMBER() OVER(ORDER BY F.SortIndex) AS BINARY(32)),
CASE WHEN (cte.ID = 1 AND f.ID != 7 AND f.ID != 8)
OR (cte.ID = #Matched AND cte.Lev + 1 > 2)
THEN 1 END AS Matched
FROM #TT AS F INNER JOIN CTETree cte
ON F.ParentID = cte.ID
)
SELECT ID, Name, ParentID, SortIndex, ParentName, Lev FROM CTETree
WHERE Matched IS NOT NULL
ORDER BY SortPath
Demo on SQLFiddle