Writing a recursive SQL query on a self-referencing table - sql

I have a database with a table called Items, that contains these columns:
ID - primary key, uniqueidentifier
Name - nvarchar(256)
ParentID - uniqueidentifier
The name field can be used to build out a path to the item, by iterating through each ParentId until it equals '11111111-1111-1111-1111-111111111111', which is a root item.
So if you had a table that had rows like
ID Name ParentID
-------------------------------------------------------------------------------------
11111111-1111-1111-1111-111111111112 grandparent 11111111-1111-1111-1111-111111111111
22222222-2222-2222-2222-222222222222 parent 11111111-1111-1111-1111-111111111112
33333333-3333-3333-3333-333333333333 widget 22222222-2222-2222-2222-222222222222
So if I looked up an item with id '33333333-3333-3333-3333-333333333333' in the example above, i'd want the path
/grandparent/parent/widget
returned. i've attempted to write a CTE, as it looks like that's how you'd normally accomplish something like this - but as I don't do very much SQL, I can't quite figure out where i'm going wrong. I've looked at some examples, and this is as close as I seem to be able to get - which only returns the child row.
declare #id uniqueidentifier
set #id = '10071886-A354-4BE6-B55C-E5DBCF633FE6'
;with ItemPath as (
select a.[Id], a.[Name], a.ParentID
from Items a
where Id = #id
union all
select parent.[Id], parent.[Name], parent.ParentID
from Items parent
inner join ItemPath as a
on a.Id = parent.id
where parent.ParentId = a.[Id]
)
select * from ItemPath
I have no idea how i'd declare a local variable for the path and keep appending to it in the recursive query. i was going to try to at least get all the rows to the parent before going after that. if anyone could help with that as well - i'd appreciate it.

well here's working solution
SQL FIDDLE EXAMPLE
declare #id uniqueidentifier
set #id = '33333333-3333-3333-3333-333333333333'
;with ItemPath as
(
select a.[Id], a.[Name], a.ParentID
from Items a
where Id = #id
union all
select parent.[Id], parent.[Name] + '/' + a.[Name], parent.ParentID
from ItemPath as a
inner join Items as parent on parent.id = a.parentID
)
select *
from ItemPath
where ID = '11111111-1111-1111-1111-111111111112'
I don't like it much, I think better solution will be to do it other way around. Wait a minute and I try to write another query :)
UPDATE here it is
SQL FIDDLE EXAMPLE
create view vw_Names
as
with ItemPath as
(
select a.[Id], cast(a.[Name] as nvarchar(max)) as Name, a.ParentID
from Items a
where Id = '11111111-1111-1111-1111-111111111112'
union all
select a.[Id], parent.[Name] + '/' + a.[Name], a.ParentID
from Items as a
inner join ItemPath as parent on parent.id = a.parentID
)
select *
from ItemPath
and now you can use this view
declare #id uniqueidentifier
set #id = '33333333-3333-3333-3333-333333333333'
select *
from vw_Names where Id = #id

I needed a slightly different version of this answer as I wanted to produce a list of all lineages in the tree. I also wanted to know the depth of each node. I added a temporary table of top level parents that I could loop through and a temporary table to build the result set.
use Items
Select *
Into #Temp
From Items
where ParentID=0
Declare #Id int
create table #Results
(
Id int,
Name nvarchar(max),
ParentId int,
Depth int
)
While (Select Count(*) From #Temp) > 0
Begin
Select Top 1 #Id = Id From #Temp
begin
with ItemPath as
(
select a.[Id], cast(a.[Name] as nvarchar(max))as Name, a.ParentID ,1 as
Depth
from Items a
where a.ID = #id
union all
select a.[Id], parent.[Name] + '/' + a.[Name], a.ParentID, 1 + Depth
from Items as a
inner join ItemPath as parent on parent.id = a.parentID
)
insert into #Results
select *
from ItemPath
end
Delete #Temp Where Id = #Id
End
drop table #Temp
select * from #Results
drop table #Results
If we start from the following table...
Id Name ParentID
1 Fred 0
2 Mary 0
3 Baker 1
4 Candle 2
5 Stick 4
6 Maker 5
We would get this result table.
Id Name ParentID Depth
1 Fred 0 1
2 Mary 0 1
3 Fred/Baker 1 2
4 Mary/Candle 2 2
5 Mary/Candle/Stick 4 3
6 Mary/Candle/Stick/Maker 5 4

Related

Recursive CTE for URL path generation

We have a parent child relationship for pages in our content management system. The table is as follows
ContentID
Parent
Title
1
0
This Page
2
1
That Page
3
0
Another Page
4
3
Child of Another
5
4
Child of child
A parent of 0 indicates the ultimate parent.
I want to output just the parents of a given contentid in a path. As an example, the output for ContentID = 5 would be:
/another-page/child-of-another/
I've tried many recursive CTE examples that generate breadcrumbs and such like, but they always output the whole path:
/another-page/child-of-another/child-of-child/
I have a function to replace the spaces in the URL, so I'm just interested in the SQL to achieve the above.
One method, using an rCTE and STRING_AGG:
CREATE TABLE dbo.YourTable (ContentID int IDENTITY(1,1),
Parent int,
Title varchar(50));
INSERT INTO dbo.YourTable (Parent, Title)
VALUES(0,'This Page'),
(1,'That Page'),
(0,'Another Page'),
(3,'Child of Another'),
(4,'Child of child');
GO
DECLARE #ID int = 5;
WITH rCTE AS(
SELECT ContentID,
Parent,
Title,
1 AS Level
FROM dbo.YourTable
WHERE ContentID = #ID
UNION ALL
SELECT YT.ContentID,
YT.Parent,
YT.Title,
r.level+1 AS Level
FROM dbo.YourTable YT
JOIN rCTE r ON YT.ContentID = r.Parent)
SELECT '/' + STRING_AGG(REPLACE(Title,' ','-'),'/') WITHIN GROUP (ORDER BY Level DESC) + '/' AS Path
FROM rCTE
WHERE ContentID != #ID;
GO
DROP TABLE dbo.YourTable;
An alternative method, without using STRING_AGG, and instead using an additional JOIN in the rCTE:
DECLARE #ID int = 5;
WITH rCTE AS(
SELECT YT.ContentID,
YT.Parent,
CONVERT(varchar(8000),'/' + YT.Title) AS Title,
1 AS Level
FROM dbo.YourTable YT
JOIN dbo.YourTable P ON YT.ContentID = P.Parent
WHERE P.ContentID = #ID
UNION ALL
SELECT YT.ContentID,
YT.Parent,
CONVERT(varchar(8000),'/' + YT.Title + r.Title),
r.Level + 1
FROM dbo.YourTable YT
JOIN rCTE r ON YT.ContentID = r.Parent)
SELECT TOP 1 REPLACE(Title,' ','-') + '/' AS Path
FROM rCTE
ORDER BY Level DESC;

SQL - get all parents/childs?

hopefully someone can help with this. I have recieved a table of data which I need to restructure and build a Denorm table out of. The table structure is as follows
UserID Logon ParentID
2344 Test1 2000
2345 Test2 2000
The issue I have is the ParentID is also a UserID of its own and in the same table.
SELECT * FROM tbl where ParentID=2000 gives the below output
UserID Logon ParentID
2000 Test Team 2500
Again, the ParentID of this is also stored as a UserID..
SELECT * FROM tbl where ParentID=2500 gives the below output
UserID Logon ParentID
2500 Test Division NULL
I want a query that will pull all of these relationships and the logons into one row, with my output looking like the below.
UserID Username Parent1 Parent2 Parent3 Parent4
2344 Test1 Test Team Test Division NULL NULL
2345 Test2 Test Team Test Division NULL NULL
The maximum number of parents a user can have is 4, in this case there is only 2. Can someone help me with the query needed to build this?
Appreciate any help
Thanks
Jess
You can use basicly LEFT JOIN. If you have static 4 parent it should work. If you have unknown parents you should do dynamic query.
SELECT U1.UserId
,U1.UserName
,U2.UserName AS Parent1
,U3.UserName AS Parent2
,U4.UserName AS Parent3
,U5.UserName AS Parent4
FROM Users U1
LEFT JOIN Users U2 ON U1.ParentId = U2.UserId
LEFT JOIN Users U3 ON U2.ParentId = U3.UserId
LEFT JOIN Users U4 ON U3.ParentId = U4.UserId
LEFT JOIN Users U5 ON U4.ParentId = U5.UserId
EDIT : Additional(to exclude parent users from the list) :
WHERE NOT EXISTS (SELECT 1 FROM Users UC WHERE U1.UserId = UC.ParentId)
select
tb1.UserId as UserId,
tb1.UserName as UserName,
tb2.UserName as Parent1,
tb3.UserName as Parent2,
tb4.UserName as Parent3,
tb5.UserName as Parent4
from tbl t1
left join tbl t2 on t2.UserId=t1.ParentID
left join tbl t3 on t3.UserId=t2.ParentID
left join tbl t4 on t4.UserId=t3.ParentID
left join tbl t5 on t5.UserId=t4.ParentID;
you need to do 4 left joins in order to fetch 4 parent details
Use a recursive CTE to get the levels then pivot to put them in columns:
WITH cte(UserID, Logon, ParentID, ParentLogon, ParentLevel) AS
(
SELECT UserID, Logon, ParentID, Logon, 0
FROM users
UNION ALL
SELECT u.UserID, u.Logon, u.ParentID, cte.ParentLogon, ParentLevel + 1
FROM users u
JOIN cte ON cte.UserID = u.ParentID
)
SELECT UserId, Logon, Parent1, Parent2, Parent3, Parent4 FROM cte
PIVOT (
MAX(ParentLogon)
FOR ParentLevel
IN (
1 AS Parent1,
2 AS Parent2,
3 AS Parent3,
4 AS Parent4
)
)
See SQL Fiddle example
In order to get all parent or child, it's efficient to use a recursive function which would fetch the whole hierarchy.
Sample Table:
CREATE TABLE #TEST
(
[Name] varchar(100),
ManagerName Varchar(100),
Number int
)
Insert some values
Insert into Test values
('a','b'), ('b','c'), ('c','d'), ('d','e'), ('e','f'), ('f','g')
Create recursive function as below
CREATE FUNCTION [dbo].[fnRecursive] (#EmpName Varchar(100), #incremental int)
RETURNS #ret TABLE
(
ManagerName varchar(100),
Number int
)
AS
BEGIN
Declare #MgrName varchar(100)
SET #MgrName = (Select ManagerName from test where [name] = #EmpName)
Insert into #ret values (#MgrName, #incremental)
if(#MgrName is not null)
BEGIN
SET #incremental = #incremental + 1;
Insert into #ret
Select ManagerName, Number from [fnRecursive](#MgrName, #incremental)
END
RETURN;
END
If this function is joined with table, it should list the hierarchy for all employees
CREATE TABLE #TEST
(
[Name] varchar(100),
ManagerName Varchar(100),
Number int
)
Insert into #TEST
Select x.[Name], x.ManagerName,x.number from (
select t.[Name],a.ManagerName as managerName, a.number as number from TEST t outer apply
(
select * from [fnRecursive](t.[Name],1)
) a)
x
Select * from #Test
If we do a pivot on the table (excluding the 'Number' column). Assuming we store in the table "#temp" it should list all the managers as a column.
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX);
SET #cols = STUFF((SELECT distinct ',' + QUOTENAME(c.[ManagerName] )
FROM #temp c
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'select * from #temp
pivot
(
min([managername])
for managername in (' + #cols + ')
) p '
execute(#query)
But this doesn't name the column as 'Parent1', 'Parent2' instead with the dynamic column name.
Link below should help to set custom column name for the dynamic pivot table
https://stackoverflow.com/questions/16614994/sql-server-pivot-with-custom-column-names

How to traverse a path in a table with id & parentId?

Suppose I have a table like:
id | parentId | name
1 NULL A
2 1 B
3 2 C
4 1 E
5 3 E
I am trying to write a scalar function I can call as:
SELECT dbo.GetId('A/B/C/E') which would produce "5" if we use the above reference table. The function would do the following steps:
Find the ID of 'A' which is 1
Find the ID of 'B' whose parent is 'A' (id:1) which would be id:2
Find the ID of 'C' whose parent is 'B' (id:2) which would be id:3
Find the ID of 'E' whose parent is 'C' (id:3) which would be id:5
I was trying to do it with a WHILE loop but it was getting very complicated very fast... Just thinking there must be a simple way to do this.
CTE version is not optimized way to get the hierarchical data. (Refer MSDN Blog)
You should do something like as mentioned below. It's tested for 10 millions of records and is 300 times faster than CTE version :)
Declare #table table(Id int, ParentId int, Name varchar(10))
insert into #table values(1,NULL,'A')
insert into #table values(2,1,'B')
insert into #table values(3,2,'C')
insert into #table values(4,1,'E')
insert into #table values(5,3,'E')
DECLARE #Counter tinyint = 0;
IF OBJECT_ID('TEMPDB..#ITEM') IS NOT NULL
DROP TABLE #ITEM
CREATE TABLE #ITEM
(
ID int not null
,ParentID int
,Name VARCHAR(MAX)
,lvl int not null
,RootID int not null
)
INSERT INTO #ITEM
(ID,lvl,ParentID,Name,RootID)
SELECT Id
,0 AS LVL
,ParentId
,Name
,Id AS RootID
FROM
#table
WHERE
ISNULL(ParentId,-1) = -1
WHILE ##ROWCOUNT > 0
BEGIN
SET #Counter += 1
insert into #ITEM(ID,ParentId,Name,lvl,RootID)
SELECT ci.ID
,ci.ParentId
,ci.Name
,#Counter as cntr
,ch.RootID
FROM
#table AS ci
INNER JOIN
#ITEM AS pr
ON
CI.ParentId=PR.ID
LEFT OUTER JOIN
#ITEM AS ch
ON ch.ID=pr.ID
WHERE
ISNULL(ci.ParentId, -1) > 0
AND PR.lvl = #Counter - 1
END
select * from #ITEM
Here is an example of functional rcte based on your sample data and requirements as I understand them.
if OBJECT_ID('tempdb..#Something') is not null
drop table #Something
create table #Something
(
id int
, parentId int
, name char(1)
)
insert #Something
select 1, NULL, 'A' union all
select 2, 1, 'B' union all
select 3, 2, 'C' union all
select 4, 1, 'E' union all
select 5, 3, 'E'
declare #Root char(1) = 'A';
with MyData as
(
select *
from #Something
where name = #Root
union all
select s.*
from #Something s
join MyData d on d.id = s.parentId
)
select *
from MyData
Note that if you change the value of your variable the output will adjust. I would make this an inline table valued function.
I think I have it based on #SeanLange's recommendation to use a recursive CTE (above in the comments):
CREATE FUNCTION GetID
(
#path VARCHAR(MAX)
)
/* TEST:
SELECT dbo.GetID('A/B/C/E')
*/
RETURNS INT
AS
BEGIN
DECLARE #ID INT;
WITH cte AS (
SELECT p.id ,
p.parentId ,
CAST(p.name AS VARCHAR(MAX)) AS name
FROM tblT p
WHERE parentId IS NULL
UNION ALL
SELECT p.id ,
p.parentId ,
CAST(pcte.name + '/' + p.name AS VARCHAR(MAX)) AS name
FROM dbo.tblT p
INNER JOIN cte pcte ON
pcte.id = p.parentId
)
SELECT #ID = id
FROM cte
WHERE name = #path
RETURN #ID
END

SQL Server Recursive Hierarchy Query

This is for SQL Server 2012. I need to generate a dataset containing links, and all links of links from a given starting ParentId given the following table
CREATE TABLE Relations (
Id INT NOT NULL PRIMARY KEY,
ParentId INT NOT NULL,
ChildId INT
);
So for the following dataset:
1 A B
2 B C
3 C D
4 F D
5 F G
6 X Y
7 Y Z
Starting with C, I'd expected to get back rows 1 to 5 as they're all linked to C through either parent or child hierarchies. E.g. G has parent F, which is parent of D, which is child of C.
It's not a standard hierarchy query as there's no real root, and I need to get links in both directions. So this means I can't use the CTE recursion trick.. here was my attempt:
--Hierarchical Query using Common Table Expressions
WITH ReportingTree (Id, Parent, Child, Lvl)
AS
(
--Anchor Member
SELECT Id,
ParentId,
ChildId,
0 as Lvl
FROM Relations WHERE ParentId = 9488
UNION ALL
--Recusive Member
SELECT
cl.Id,
cl.ParentId,
cl.ChildId,
r1.Lvl+1
FROM [dbo].[CompanyLinks] cl
INNER JOIN ReportingTree r1 ON ReportingTree.Parent = cl.Child
INNER JOIN ReportingTree r2 ON cl.FromCompanyId = r2.Parent <-- errors
)
SELECT * FROM ReportingTree
My second attempt involved a temp table and while loop. This works but turns out to be very slow:
BEGIN
CREATE TABLE #R (
Id INT NOT NULL PRIMARY KEY NONCLUSTERED,
ParentId INT NOT NULL,
ChildId INT
);
CREATE CLUSTERED INDEX IX_Parent ON #R (ParentId);
CREATE INDEX IX_Child ON #R (ChildId);
INSERT INTO #R
SELECT Id,ParentId ChildId
FROM Relations
WHERE ParentId = 9488 OR ChildId = 9488;
WHILE ##RowCount > 0
BEGIN
INSERT INTO #R
SELECT cl.Id,cl.ParentId, cl.ChildId
FROM #R INNER JOIN
Relations AS cl ON cl.ChildId = #R.ChildId OR cl.ParentId = #R.ParentId OR cl.ChildId = #R.Parent OR cl.ParentId = #R.Child
EXCEPT
SELECT Id,ParentId, ChildId
FROM #R;
END
SELECT * FROM Relations cl inner Join #Relations r ON cl.Id = #R.Id
DROP TABLE #R
END
Can anyone suggest a workable solution for this?
We match every row with every other row based on every combination of parent and child ids, and save the path along the way. Recursively we do this matching and make the path, in order to avoid infinite loops we check the path is not traversed previously, finally we have all nodes that has a path to the desired node(#Id):
WITH cte AS (
SELECT CompanyLinks.*, cast('(' + cast(ParentId as nvarchar(max)) + ','
+ cast(ChildId as nvarchar(max))+')' as nvarchar(max)) Path
FROM CompanyLinks
WHERE ParentId = #Id OR ChildId = #Id
UNION ALL
SELECT a.*,
cast(
c.Path + '(' +
cast(a.ParentId as nvarchar(max)) + ',' +
cast(a.ChildId as nvarchar(max)) + ')'
as nvarchar(max)
) Path
FROM CompanyLinks a JOIN cte c ON
a.ParentId = c.ChildId
OR c.ParentId = a.ChildId
OR c.ParentId = a.ParentId
OR c.ChildId = a.ChildId
where c.Path not like cast(
'%(' +
cast(a.ParentId as nvarchar(max)) + ',' +
cast(a.ChildId as nvarchar(max)) +
')%'
as nvarchar(max)
)
)
SELECT DISTINCT a.id, Company.Name, path from (
SELECT distinct ParentId as id, path FROM cte
union all
SELECT distinct ChildId as id, path FROM cte
) a inner join Company on Company.Id = a.Id
Here is a fiddle for it.
If you want distinct nodes just use:
SELECT DISTINCT id from (
SELECT distinct ParentId as id FROM cte
union all
SELECT distinct ChildId as id FROM cte
) a
at the end of query.
This query is actually a Breadth First Search on an un-directed graph.
Note: Based on Hogan comment, there is no need for checking the path, as there is a primary key in the relation table(which I did not noticed) we can look for the primary key in prior recursions to avoid infinite loops.

FK on same table

I have table with ID who is PK and I have in same table column ParentID who is in some cases ID's.(I say some cases because for example ParentID can be 0 but this ID not have in ID's column).
I try to create Query:
ID ParentID Value
1 0 Test
2 0 Test01
3 2 Test02
4 3 Test03
And when I send ID=4 I need this result:
ID ParentID Value
2 0 Test01
3 2 Test02
4 3 Test03
Any help...
This looks like a tree traversal problem to me, which doesn't lend itself particularly well to set-based operations. The following works, but it's not too elegant:
-- turn this into stored proc?
declare #inputId int
set #inputId = 4
declare #Ids table (ID int)
insert into #Ids values (#inputId)
declare #reccount int
set #reccount = 1
declare #lastreccount int
set #lastreccount = 0
while #reccount <> #lastreccount
begin
set #lastreccount = #reccount
insert into #Ids
select ParentID from recursiveTest
where ID in (select ID from #Ids)
and ParentID not in (select ID from #Ids)
set #reccount = (select COUNT(*) from #Ids)
end
select * from recursiveTest where ID in (select ID from #Ids);
Don't use 0 (zero) but null to signal "no parent present".
try this:
Declare #Id Integer Set #Id = 4
With Child(Id, ParentId, Value, Level) As
( Select Id, ParentId, Value, 0 as Level
From table
Where id = #Id
Union All
Select t.Id, t.ParentId, t.Value, level + 1
From Child c Join table t
On t.Id = c.ParentId
)
Select id. ParentId, Value, Level
From Child
Order By Id