Recursive query with parent-child relation - sql

I am trying to make a recursive query in SQL Server, that display data hierarchically. Here is the structure of the table
[id] [int] IDENTITY(1,1) NOT NULL,
[name] [varchar(100)] NOT NULL,
[Parent_Id] [int] NULL,
Each product has a parent. The column Parent_Id content the id of the parent. The parent_id is null for root products.
I want to make a sql query that display products hierarchically. The following image is an example of how the products could be organized.
Products can have products childs.
For the picture above, the query result should be like the following :
id name parent_id
1 P1 NULL
2 P2 NULL
3 P2-1 2
4 P2-2 2
5 P2-3 2
6 P2-3-1 5
7 P2-3-2 5
8 P3 NULL
9 P3-1 8
Here is the request I wrote to achieve it :
with tree as (select * from products
union all
select * from tree where parent_id = tree.id
)
select * from tree;
But I get a result similar to the following:
1 P1 NULL
2 P2 NULL
8 P3 NULL
3 P2-1 2
4 P2-2 2
5 P2-3 2
9 P3-1 8
6 P2-3-1 5
7 P2-3-2 5
What I want is to group each products sibling so that each product is displayed under its direct parent.

Just another option using the data type hierarchyid
There are some additional features and functions associated with hierarchyid
Example
-- Optional See 1st WHERE
Declare #Top int = null --<< Sets top of Hier Try 2
;with cteP as (
Select ID
,parent_id
,Name
,HierID = convert(hierarchyid,concat('/',ID,'/'))
From YourTable
Where IsNull(#Top,-1) = case when #Top is null then isnull(parent_id ,-1) else ID end
--Where parent_id is null -- Use this where if you always want the full hierarchy
Union All
Select ID = r.ID
,parent_id = r.parent_id
,Name = r.Name
,HierID = convert(hierarchyid,concat(p.HierID.ToString(),r.ID,'/'))
From YourTable r
Join cteP p on r.parent_id = p.ID)
Select Lvl = HierID.GetLevel()
,ID
,parent_id
,Name
From cteP A
Order By A.HierID
Results
Lvl ID parent_id Name
1 1 NULL P1
1 2 NULL P2
2 3 2 P2-1
2 4 2 P2-2
2 5 2 P2-3
3 6 5 P2-3-1
3 7 5 P2-3-2
1 8 NULL P3
2 9 8 P3-1
Just for fun, If I set #Top to 2, the results would be
Lvl ID parent_id Name
1 2 NULL P2
2 3 2 P2-1
2 4 2 P2-2
2 5 2 P2-3
3 6 5 P2-3-1
3 7 5 P2-3-2

Construct the path in the recursive query. The following does this as a string with fixed length ids:
with tree as (
select p.id, p.name, p.parentid,
format(p.parentid, '0000') as path
from products p
where p.parentid is null
union all
select p.id, p.name, p.parentid,
concat(cte.path, '->', format(p.id, '0000')
from tree join
products p
where p.parent_id = t.id
)
select *
from tree;

If I understand this correct and you have the result you want but just unordered, you should be able to just order the result by name.
with tree as (select * from products
union all
select * from tree where parent_id = tree.id
)
select * from tree order by name asc;

Related

Bottom Up Recursive SUM (lowest level only has values)

I have a tree-based structure of SKUs within a Product Hierarchy in SQL Server. The lowest level SKUs will only ever have values (these are consumption values). I then want to generate aggregates up the hierarchy at every level.
Here's is the sample table structure:
Id
ParentId
Name
Volume
IsSku
1
-1
All
0
0
2
1
Cat A
0
0
3
1
Cat B
0
0
4
2
Cat A.1
0
0
5
2
Cat A.2
0
0
6
3
Cat B.1
0
0
7
3
Cat B.2
0
0
8
4
SKU1
10
1
9
4
SKU2
5
1
10
5
SKU3
7
1
11
5
SKU4
4
1
12
6
SKU1
10
1
13
6
SKU2
5
1
14
7
SKU3
9
1
15
7
SKU4
7
1
I need a query that will start at the sku level (IsSku=1) and then working up, will sum the SKUs and carry the sum up the product category levels to get a cumulative running total.
I've seen several queries where there are recursive sums in a hierarchical structure where each level already has values, but I need one that will start at the lowest level that has values and recursively calculate the sum as it moves upward.
I was trying these, but they look like they are mainly summing hierarchical data where each node already has a value (Volume, in my case). I need to start at the lowest level and carry the aggregate up as I go up the hierarchy. I tried to emulate the answers in these posts with my data, but wasn't successful so far with my data setup.
24394601
29127163
11408878
The output for the query should be like so:
Id
ParentId
Name
Volume
IsSku
1
-1
All
54
0
2
1
Cat A
26
0
3
1
Cat B
28
0
4
2
Cat A.1
15
0
5
2
Cat A.2
11
0
6
3
Cat B.1
12
0
7
3
Cat B.2
16
0
8
4
SKU1
10
1
9
4
SKU2
5
1
10
5
SKU3
7
1
11
5
SKU4
4
1
12
6
SKU1
10
1
13
6
SKU2
2
1
14
7
SKU3
9
1
15
7
SKU4
7
1
I've got a start with a recursive CTE that returns the hierarchy can can aggregate the volume if that node has volume already, but can't seem to figure out how to start at the SKU levels and continue aggregating up the hierarchy.
Here's the start with my CTE:
DECLARE #tblData TABLE
(
[ID] INT NOT NULL,
[ParentId] INT NULL,
[Name] varchar(50) NOT NULL,
[Volume] int NOT NULL,
[IsSku] bit
)
INSERT INTO #tblData
VALUES
(1,-1,'All',0,0)
,(2,1,'Cat A',0,0)
,(3,1,'Cat B',0,0)
,(4,2,'Cat A.1',0,0)
,(5,2,'Cat A.2',0,0)
,(6,3,'Cat B.1',0,0)
,(7,3,'Cat B.2',0,0)
,(8,4,'SKU1',10,1)
,(9,4,'SKU2',5,1)
,(10,5,'SKU3',7,1)
,(11,5,'SKU4',4,1)
,(12,6,'SKU1',10,1)
,(13,6,'SKU2',5,1)
,(14,7,'SKU3',7,1)
,(15,7,'SKU4',4,1)
;WITH cte AS (
SELECT
a.ID
,a.ParentID
,a.Name
,a.Volume
,CAST('/' + cast(ID as varchar) + '/' as varchar) Node
,0 AS level
,IsSku
FROM #tblData AS a
WHERE a.ParentID = -1
UNION ALL
SELECT
b.ID
,b.ParentID
,b.Name
,b.Volume
,CAST(c.Node + CAST(b.ID as varchar) + '/' as varchar)
,level = c.level + 1
,b.IsSku
FROM #tblData AS b
INNER JOIN cte c
ON b.ParentId = c.ID
)
SELECT c1.ID, c1.ParentID, c1.Name, c1.Node
,ISNULL(SUM(c2.Volume),0)
FROM cte c1
LEFT OUTER JOIN cte c2
ON c1.Node <> c2.Node
AND LEFT(c2.Node, LEN(c1.Node)) = c1.Node
GROUP BY c1.ID, c1.ParentID, c1.Name, c1.Node
Any help is appreciated!
This should do it:
DECLARE #tbl TABLE(Id INT, ParentId INT, Name NVARCHAR(255), Volume INTEGER, IsSku BIT)
INSERT INTO #tbl
VALUES
(1,-1,'All',0,0)
,(2,1,'Cat A',0,0)
,(3,1,'Cat B',0,0)
,(4,2,'Cat A.1',0,0)
,(5,2,'Cat A.2',0,0)
,(6,3,'Cat B.1',0,0)
,(7,3,'Cat B.2',0,0)
,(8,4,'SKU1',10,1)
,(9,4,'SKU2',5,1)
,(10,5,'SKU3',7,1)
,(11,5,'SKU4',4,1)
,(12,6,'SKU1',10,1)
,(13,6,'SKU2',5,1)
,(14,7,'SKU3',7,1)
,(15,7,'SKU4',4,1)
SELECT * FROM #tbl
;
WITH cte AS (
SELECT
Id,ParentId, Name, Volume, IsSku, CAST(Id AS VARCHAR(MAX)) AS Hierarchy
FROM
#tbl
WHERE ParentId=-1
UNION ALL
SELECT
t.Id,t.ParentId, t.Name, t.Volume, t.IsSku, CAST(c.Hierarchy + '|' + CAST(t.Id AS VARCHAR(MAX)) AS VARCHAR(MAX))
FROM
cte c
INNER JOIN #tbl t
ON c.Id = t.ParentId
)
SELECT Id,ParentId, Name, ChildVolume AS Volume, IsSku
FROM (
SELECT c1.Id, c1.ParentId, c1.Name, c1. Volume, c1.IsSku, SUM(c2.Volume) AS ChildVolume
FROM cte c1
LEFT JOIN cte c2 ON c2.Hierarchy LIKE c1.Hierarchy + '%'
GROUP BY c1.Id, c1.ParentId, c1.Name, c1. Volume, c1.IsSku
) x
Basically the computation happens in three steps:
Capture the Hierarchy recursively for each descendant by concatenating the Ids: CAST(c.Hierarchy + '|' + CAST(t.Id AS VARCHAR(MAX)) AS VARCHAR(MAX))
Join the resulting table with itself so each record is joined with itself and all its descendants: FROM cte c1 LEFT JOIN cte c2 ON c2.Hierarchy LIKE c1.Hierarchy + '%'
Finally aggregate the Volume of each hierarchy by grouping: SUM(c2.Volume) AS ChildVolume
This is in reference to Ed Harper's answer to a similar question here: Hierarchy based aggregation
Due to the way that recursive CTEs work in SQL Server, it is very difficult to get this kind of logic working efficiently. It often either requires self-joining the whole resultset, or using something like JSON or XML.
The problem is that at each recursion of the CTE, although it appears you are working on the whole set at once, it actually only feeds back one row at a time. Therefore grouping is disallowed over the recursion.
Instead, it's much better to simply recurse with a WHILE loop and insert into a temp table or table variable, then read it back to aggregate
Use the OUTPUT clauses to view the intermediate results
DECLARE #tmp TABLE (
Id INTEGER,
ParentId INTEGER,
Name VARCHAR(7),
Volume INTEGER,
IsSku INTEGER,
Level INT,
INDEX ix CLUSTERED (Level, ParentId, Id)
);
INSERT INTO #tmp
(Id, ParentId, Name, Volume, IsSku, Level)
-- OUTPUT inserted.Id, inserted.ParentId, inserted.Name, inserted.Volume, inserted.IsSku, inserted.Level
SELECT
p.Id,
p.ParentId,
p.Name,
p.Volume,
p.IsSku,
1
FROM Product p
WHERE p.IsSku = 1;
DECLARE #level int = 1;
WHILE (1=1)
BEGIN
INSERT INTO #tmp
(Id, ParentId, Name, Volume, IsSku, Level)
-- OUTPUT inserted.Id, inserted.ParentId, inserted.Name, inserted.Volume, inserted.IsSku, inserted.Level
SELECT
p.Id,
p.ParentId,
p.Name,
t.Volume,
p.IsSku,
#level + 1
FROM (
SELECT
t.ParentID,
Volume = SUM(t.Volume)
FROM #tmp t
WHERE t.Level = #level
GROUP BY
t.ParentID
) t
JOIN Product p ON p.Id = t.ParentID;
IF (##ROWCOUNT = 0)
BREAK;
SET #level += 1;
END;
SELECT *
FROM #tmp
ORDER BY Id;
db<>fiddle
This solution does involve a blocking operator, due to Halloween protection (in my case I saw an "unnecessary" sort). You can avoid it by using Itzik Ben-Gan's Divide and Conquer method, utilizing two table variables and flip-flopping between them.

how to sql recursion solve first node first then move to another in CTE

suppose i have a data like that
ID ParentID Name
1 null a
2 1 b
3 2 c
4 1 d
5 4 e
if i use cte(common table expression) provided by sql it shows me result like this
ID ParentID Name
1 null a
2 1 b
4 1 d
3 2 c
5 4 e
but i want to arrange data like, query should complete first node till end , then move to other node . like
ID ParentID Name
1 null a
2 1 b
3 2 c
4 1 d
5 4 e
Note: i have a primary key with datatype :uniqueidentifier so i cannot use order by clause after CTE
Example
Declare #Top int = null --<< Sets top of Hier Try 2
;with cteP as (
Select ID
,ParentID
,Name
,Path = cast('/'+[ID]+'/' as varchar(500))
From YourTable
Where IsNull(#Top,-1) = case when #Top is null then isnull(ParentID ,-1) else ID end
Union All
Select r.ID
,r.ParentID
,r.Name
,cast(p.path + '/'+r.[ID]+'/' as varchar(500))
From YourTable r
Join cteP p on r.ParentID = p.ID)
Select ID
,ParentID
,Name
From cteP A
Order By Path
Returns
Without seeing your query, I think you can do something like this:
WITH CTE_Example
AS
(
YOUR QUERY
)
SELECT *
FROM CTE_Example
ORDER BY ID

Get both the child row and its top root row and combine IDs and Names in a special way

When i send CatelogID then need to get the catelogID with rootcatelogName and RootCatelogID
CREATE TABLE catelog
(
CatelogID BIGINT IDENTITY(1,1),
CatelogName NVARCHAR(50) NOT NULL,
ParentID BIGINT NULL
)
SELECT * from catelog
INSERT INTO catelog(catelogName,ParentID)
VALUES('Embedded Sytem',NULL),
('Library',NULL),
('Books',2),
('Pages',3),
('Chapters',4),
('Paragraph',5),
('New Sytem',1),
('College',NULL)
When i give CatelogID as 6 then output should be like this
O/P:
CatelogID RootCatelogName RootParentID
2 Library NULL
6 Library 2
If i dont pass any value can i get the result like this
CatelogID RootCatelogName RootParentID
1 Embedded Sytem NULL
2 Library NULL
3 Library 2
4 Library 2
5 Library 2
6 Library 2
7 Embedded 1
8 College NULL
Try hierarchy CTE
DECLARE #id INT = 6;
WITH h AS (
SELECT CatelogID,catelogName,ParentID
FROM catelog
WHERE CatelogID = #id
UNION ALL
SELECT p.CatelogID,p.catelogName,p.ParentID
FROM h
JOIN catelog p ON p.CatelogID = h.ParentID
)
SELECT t.*
FROM h
CROSS APPLY (
SELECT #ID AS catelogID, catelogName AS RootCatelogName, CatelogID AS RootParentID
UNION
SELECT CatelogID, catelogName AS RootCatelogName, ParentID AS RootParentID
) t
WHERE ParentID IS NULL;
Returns
catelogID RootCatelogName RootParentID
2 Library NULL
6 Library 2
EDIT
The query to answer your second question
WITH h AS (
SELECT CatelogID AS leafId, CatelogID, catelogName, ParentID
FROM catelog
WHERE ParentID IS NOT NULL
UNION ALL
SELECT h.leafId, p.CatelogID, p.catelogName, p.ParentID
FROM h
JOIN catelog p ON p.CatelogID = h.ParentID
)
SELECT leafId AS catelogID, catelogName AS RootCatelogName, CatelogID AS RootParentID
FROM h
WHERE ParentID IS NULL
UNION ALL
SELECT CatelogID,catelogName,ParentID
FROM catelog
WHERE ParentID IS NULL
ORDER BY CatelogID;
Returns
catelogID RootCatelogName RootParentID
1 Embedded Sytem NULL
2 Library NULL
3 Library 2
4 Library 2
5 Library 2
6 Library 2
7 Embedded Sytem 1
8 College NULL
Using a recursive CTE and row_number to get the top level :
declare #CatelogID BIGINT = 6;
;with cte as
(
select CatelogID as BaseCatelogID, CatelogName as BaseCatelogName, 0 as level, CatelogID, CatelogName, ParentID
from catelog t
where CatelogID = #CatelogID
union all
select cte.BaseCatelogID, cte.BaseCatelogName, level + 1, t.CatelogID, t.CatelogName, t.ParentID
from cte
join catelog t on (cte.ParentID = t.CatelogID)
)
, cte2 as
(
select *, row_number() over (partition by BaseCatelogID order by level desc) as rn
from cte
)
select CatelogID, CatelogName as RootCatelogName, ParentID as RootParentID
from cte2 where rn = 1
union all
select BaseCatelogID, CatelogName, CatelogID
from cte2 where rn = 1;
Returns:
CatelogID RootCatelogName RootParentID
2 Library NULL
6 Library 2

RECURSIVE CTE SQL - Find next available Parent

I have parent child relation SQL table
LOCATIONDETAIL Table
OID NAME PARENTOID
1 HeadSite 0
2 Subsite1 1
3 subsite2 1
4 subsubsite1 2
5 subsubsite2 2
6 subsubsite3 3
RULESETCONFIG
OID LOCATIONDETAILOID VALUE
1 1 30
2 4 15
If i provide Input as LOCATIONDETAIL 6, i should get RULESETCONFIG value as 30
because for
LOCATIONDETAIL 6, parentid is 3 and for LOCATIONDETAIL 3 there is no value in RULESETCONFIG,
LOCATIONDETAIL 3 has parent 1 which has value in RULESETCONFIG
if i provide Input as LOCATIONDETAIL 4, i should get RULESETCONFIG value 15
i have code to populate the tree, but don't know how to find the next available Parent
;WITH GLOBALHIERARCHY AS
(
SELECT A.OID,A.PARENTOID,A.NAME
FROM LOCATIONDETAIL A
WHERE OID = #LOCATIONDETAILOID
UNION ALL
SELECT A.OID,A.PARENTOID,A.NAME
FROM LOCATIONDETAIL A INNER JOIN GLOBALHIERARCHY GH ON A.PARENTOID = GH.OID
)
SELECT * FROM GLOBALHIERARCHY
This will return the next parent with a value. If you want to see all, remove the top 1 from the final select.
dbFiddle
Example
Declare #Fetch int = 4
;with cteHB as (
Select OID
,PARENTOID
,Lvl=1
,NAME
From LOCATIONDETAIL
Where OID=#Fetch
Union All
Select R.OID
,R.PARENTOID
,P.Lvl+1
,R.NAME
From LOCATIONDETAIL R
Join cteHB P on P.PARENTOID = R.OID)
Select Top 1
Lvl = Row_Number() over (Order By A.Lvl Desc )
,A.OID
,A.PARENTOID
,A.NAME
,B.Value
From cteHB A
Left Join RULESETCONFIG B on A.OID=B.OID
Where B.VALUE is not null
and A.OID <> #Fetch
Order By 1 Desc
Returns when #Fetch=4
Lvl OID PARENTOID NAME Value
2 2 1 Subsite1 15
Returns when #Fetch=6
Lvl OID PARENTOID NAME Value
1 1 0 HeadSite 30
This should do the job:
;with LV as (
select OID ID,PARENTOID PID,NAME NAM, VALUE VAL FROM LOCATIONDETAIL
left join RULESETCONFIG ON LOCATIONDETAILOID=OID
), GH as (
select ID gID,PID gPID,NAM gNAM,VAL gVAL from LV where ID=#OID
union all
select ID,PID,NAM,VAL FROM LV INNER JOIN GH ON gVAL is NULL AND gPID=ID
)
select * from GH WHERE gVAL>0
See here for e little demo: http://rextester.com/OXD40496

sql select parent child recursive in one field

I do not know how to select query recursive..
id idparent jobNO
--------------------------------
1 0 1
2 1 2
3 1 3
4 0 4
5 4 5
6 4 6
how do the results like this With SqlServer
id idparent jobNO ListJob
----------------------------------------
1 0 1 1
2 1 2 1/2
3 1 3 1/3
4 0 4 4
5 4 5 4/5
6 5 6 4/5/6
You need to use a Recursive Common Table Expression.
There are many useful articles online.
Useful Links
Simple Talk: SQL Server CTE Basics
blog.sqlauthority: Recursive CTE
Here is a solution to your question:
CREATE TABLE #TEST
(
id int not null,
idparent int not null,
jobno int not null
);
INSERT INTO #Test VALUES
(1,0,1),
(2,1,2),
(3,1,3),
(4,0,4),
(5,4,5),
(6,5,6);
WITH CTE AS (
-- This is end of the recursion: Select items with no parent
SELECT id, idparent, jobno, CONVERT(VARCHAR(MAX),jobno) AS ListJob
FROM #Test
WHERE idParent = 0
UNION ALL
-- This is the recursive part: It joins to CTE
SELECT t.id, t.idparent, t.jobno, c.ListJob + '/' + CONVERT(VARCHAR(MAX),t.jobno) AS ListJob
FROM #Test t
INNER JOIN CTE c ON t.idParent = c.id
)
SELECT * FROM CTE
ORDER BY id;