Find all descendants of a parent in sql, including descendants of descendants - sql

I want to find a table like this:
Parent
Child
A
B
A
C
A
D
A
E
B
C
B
D
So, the first level under A are nodes B and C. And under B is another level of nodes including C and D, however I also want these nodes included as rows under A.
The available dataset consists out of nodes with their level.
Node
Level
A
1
B
2
C
3
D
3
E
2
And all the children below B are the rows until E as B and E have the same level.
Is it possible to create a table as I wanted from the information I have?
I have already looked into stored procedures but I have not used these before so I am a bit lost.

You may try the following self-join query:
select D.node Parent, T.node Child
from table_name T join table_name D
on T.level > D.level
order by D.node, T.node
If you want to select only one node from the nodes with the same level you may use the ROW_NUMBER() function as the following:
select D.node Parent, T.node Child
from table_name T join
(
select *,
row_number() over (partition by level order by node) rn
from table_name
) D
on T.level > D.level
where D.rn = 1
order by D.node, T.node
See a demo.

Related

Find all ancestors without direct id-parentid in same table

I have a parent-child structure across two tables. The first table has BOM_ID for bills and ITEM_ID for associated children items. The other has BOM_ID and ITEM_ID for the parent item.
I am able to find the first level of parents' ITEM_ID with the following query
SELECT item_id
FROM bomversion
WHERE bom_id IN (SELECT bom_id FROM bom WHERE item_id = 1)
So, in order to find all ancestors, I would have to repeat this step. I've tried to look at CTE and recursion techniques but all examples have parentid and childid in the same table. I cannot figure this one out.
If 1 is child of 2, 2 is child of 3, 3 is child of 4, and 2 is also child of 5, I am looking for the following result:
ChildID
ParentID
1
2
2
3
2
5
3
4
The starting point will be a specific ChildID.
S O L U T I O N
Based on Adnan Sharif's proposal, I found the solution for my problem:
WITH items_CTE AS (
-- create the mapping of items to consider
SELECT
B.ITEMID AS Child_id,
BV.ITEMID AS Parent_Id
FROM BOM AS B
INNER JOIN BOMVERSION AS BV
ON B.BOMID = BV.BOMID
), parent_child_cte AS (
-- select those items as initial query
SELECT
Child_id,
Parent_id
FROM items_CTE
WHERE Child_id = '111599' -- this is the starting point
UNION ALL
-- recursive approach to find all the ancestors
SELECT
c.Child_Id,
c.Parent_Id
FROM items_CTE c
JOIN parent_child_cte pc
ON pc.Parent_Id = c.Child_id
)
SELECT * FROM parent_child_cte
From the dbfiddle that you shared in the comment, if I understand you correctly, you want to have the rows showing all the parents of a child.
For example, lets consider the hierarchy, 1 is a child of 2, 2 is a child of 3, and 3 is a child of 4. You want the final result as,
child_id
parent_id
1
2
1
3
1
4
2
3
2
4
3
4
If that's the case, we can use recursive CTE to build that table. First of all, we need to be build a relation between those two tables based on bom_id and then we need to find out parent-child relation. After that we will add rows based on the initial query. Please see the below code.
WITH RECURSIVE items_CTE AS (
-- create the mapping of items to consider
SELECT
B.Item_id AS Child_id,
BV.Item_id AS Parent_Id
FROM BOM AS B
INNER JOIN BOMVERSION AS BV
ON B.bom_id = BV.bom_id
), parent_child_cte AS (
-- select those items as initial query
SELECT
Child_id,
Parent_id
FROM items_CTE
UNION
-- recursive approach to find all the ancestors
SELECT
parent_child_cte.Child_Id,
items_CTE.Parent_Id
FROM items_CTE
INNER JOIN parent_child_cte
ON parent_child_cte.Parent_Id = items_CTE.Child_id
)
SELECT * FROM parent_child_cte

How to get all children of parent item when all are in the same row and table

I have a table of parts and sub-parts where each record contains the primary part for that record along with its ChildPart.
Part - ChildPart
A - B
A - C
A - D
C - F
C - Z
F - R
Z - R
Q - B
Q - C
So for the example above, part A has 7 total descendants (B, C, D, F, Z, R, R). A parent part can have multiple children and a child part can belong to more than 1 parent; notice that part B is used for both A and Q.
How can I efficiently show all the child parts of a given parent part just using joins and not using SQL cursors or loops? The hierarchical tree could theoretically be infinitely deep.
You can use a Recursive CTE:
DECLARE #pID VARCHAR(20) = 'A'
;WITH CTE AS (
SELECT ChildPart
FROM mytable
WHERE Part = #pID
UNION ALL
SELECT t1.ChildPart
FROM mytable AS t1
INNER JOIN CTE AS t2 ON t1.Part = t2.ChildPart
)
SELECT ChildPart
FROM CTE

To arrange the hierarchy in a table

I have a table dbo.Hierarchy that contains the following data:
Level1 Level2 Level3 Level4 Level5 Level6 Level7 Level8 Level9 Level10
-----------------------------------------------------------------------
a b c d e f g h i j
k l m n o
There are a total of 10 levels and any item can have hierarchy upto any level. In the above data a is the parent of b, b is the parent of c and so on. j and o are the last levels in their respective hierarchies. How can I get the output in the below format:
Name ParentName LevelID
-------------------------------
a NULL 1
b a 2
j i 10
k NULL 1
l k 2
o n 5
Something like (untested)
with t(L1,L2,L3,L4,L5,L6,L7,L8,L9,L10) as (
values ('a','b','c','d','e','f','g','h','i','j')
, ('k','l','m','n','o',null,null,null,null,null)
)
select x.*
from t
cross apply (
values (L1,null,1),(L2,L1,2),(L3,L2,3),(L4,L3,4),(L5,L4,5)
, (L6,L5,6),(L7,L6,7),(L8,L7,8),(L9,L8,9),(L10,L9,10)) x (name, parentname, levelid)
where name is not null
Try this:
;with base as
(select *, row_number() over (order by level1) rn from tbl),
src as
(
select
valname as name,
cast(substring(colname,6,len(colname)) as int) as level,
rn from
(select * from base) s
unpivot
(
valname
for colname in ([level1],[level2],[level3],[level4],[level5],[level6],[level7], [level8],[level9],[level10])
) u
),
cte as
(select * from src
where level = 1
union all
select s.* from src s
inner join cte c on s.level = c.level + 1 and s.rn = c.rn)
select distinct s.name, t.name parentname, s.level levelid from
cte s
left join cte t on s.rn = t.rn and s.level = t.level + 1
Breakdown:
CTE base is used to generate row number as a derived column. We will use this column to keep track of which values belong to which row. This will help in uniquely mapping children to their parents.
CTE src is where we transform the table from this denormalised structure to a normalised one. Using UNPIVOT, we reduce the data set to 3 columns - name, level and the row number rn.
CTE cte is a recursive CTE that we use to get all possible combinations of parents and children (including immediate parents as well as ancestors).
Finally, we LEFT JOIN cte to itself on the condition that row number is same on both sides of the join i.e. the values belong to same record from the base table, and also that the value from the right side is the immediate ancestor (parent) of the value on the left side.
Demo
The huge mass of code above can be avoided, if you were to choose a normalised structure for your table. I would suggest something like this:
CREATE TABLE tbl
(ID int, --Keep track of values that are related to each other
Name varchar(100), --Name
Level int --The level for a particular value
)
With this proposed structure, all you would need is the recursive CTE (cte from the above code) and the left join to get the parent-child data. The beauty of this approach is that you can extend it to any number of levels that you like, without having to hard-code the level numbers.
Demo

SQL Server: querying hierarchical and referenced data

I'm working on an asset database that has a hierarchy. Also, there is a "ReferenceAsset" table, that effectively points back to an asset. The Reference Asset basically functions as an override, but it is selected as if it were a unique, new asset. One of the overrides that gets set, is the parent_id.
Columns that are relevant to selecting the heirarchy:
Asset: id (primary), parent_id
Asset Reference: id (primary), asset_id (foreignkey->Asset), parent_id (always an Asset)
---EDITED 5/27----
Sample Relevent Table Data (after joins):
id | asset_id | name | parent_id | milestone | type
3 3 suit null march shape
4 4 suit_banker 3 april texture
5 5 tie null march shape
6 6 tie_red 5 march texture
7 7 tie_diamond 5 june texture
-5 6 tie_red 4 march texture
the id < 0 (like the last row) signify assets that are referenced. Referenced assets have a few columns that are overidden (in this case, only parent_id is important).
The expectation is that if I select all assets from april, I should do a secondary select to get the entire tree branches of the matching query:
so initially the query match would result in:
4 4 suit_banker 3 april texture
Then after the CTE, we get the complete hierarchy and our result should be this (so far this is working)
3 3 suit null march shape
4 4 suit_banker 3 april texture
-5 6 tie_red 4 march texture
and you see, the parent of id:-5 is there, but what is missing, that is needed, is the referenced asset, and the parent of the referenced asset:
5 5 tie null march shape
6 6 tie_red 5 march texture
Currently my solution works for this, but it is limited to only a single depth of references (and I feel the implementation is quite ugly).
---Edited----
Here is my primary Selection Function. This should better demonstrate where the real complication lies: the AssetReference.
Select A.id as id, A.id as asset_id, A.name,A.parent_id as parent_id, A.subPath, T.name as typeName, A2.name as parent_name, B.name as batchName,
L.name as locationName,AO.owner_name as ownerName, T.id as typeID,
M.name as milestoneName, A.deleted as bDeleted, 0 as reference, W.phase_name, W.status_name
FROM Asset as A Inner Join Type as T on A.type_id = T.id
Inner Join Batch as B on A.batch_id = B.id
Left Join Location L on A.location_id = L.id
Left Join Asset A2 on A.parent_id = A2.id
Left Join AssetOwner AO on A.owner_id = AO.owner_id
Left Join Milestone M on A.milestone_id = M.milestone_id
Left Join Workflow as W on W.asset_id = A.id
where A.deleted <= #showDeleted
UNION
Select -1*AR.id as id, AR.asset_id as asset_id, A.name, AR.parent_id as parent_id, A.subPath, T.name as typeName, A2.name as parent_name, B.name as batchName,
L.name as locationName,AO.owner_name as ownerName, T.id as typeID,
M.name as milestoneName, A.deleted as bDeleted, 1 as reference, NULL as phase_name, NULL as status_name
FROM Asset as A Inner Join Type as T on A.type_id = T.id
Inner Join Batch as B on A.batch_id = B.id
Left Join Location L on A.location_id = L.id
Left Join Asset A2 on AR.parent_id = A2.id
Left Join AssetOwner AO on A.owner_id = AO.owner_id
Left Join Milestone M on A.milestone_id = M.milestone_id
Inner Join AssetReference AR on AR.asset_id = A.id
where A.deleted <= #showDeleted
I have a stored procedure that takes a temp table (#temp) and finds all the elements of the hierarchy. The strategy I employed was this:
Select the entire system heirarchy into a temp table (#treeIDs) represented by a comma separated list of each entire tree branch
Get entire heirarchy of assets matching query (from #temp)
Get all reference assets pointed to by Assets from heirarchy
Parse the heirarchy of all reference assets
This works for now because reference assets are always the last item on a branch, but if they weren't, i think i would be in trouble. I feel like i need some better form of recursion.
Here is my current code, which is working, but i am not proud of it, and I know it is not robust (because it only works if the references are at the bottom):
Step 1. build the entire hierarchy
;WITH Recursive_CTE AS (
SELECT Cast(id as varchar(100)) as Hierarchy, parent_id, id
FROM #assetIDs
Where parent_id is Null
UNION ALL
SELECT
CAST(parent.Hierarchy + ',' + CAST(t.id as varchar(100)) as varchar(100)) as Hierarchy, t.parent_id, t.id
FROM Recursive_CTE parent
INNER JOIN #assetIDs t ON t.parent_id = parent.id
)
Select Distinct h.id, Hierarchy as idList into #treeIDs
FROM ( Select Hierarchy, id FROM Recursive_CTE ) parent
CROSS APPLY dbo.SplitIDs(Hierarchy) as h
Step 2. Select the branches of all assets that match the query
Select DISTINCT L.id into #RelativeIDs FROM #treeIDs
CROSS APPLY dbo.SplitIDs(idList) as L
WHERE #treeIDs.id in (Select id FROM #temp)
Step 3. Get all Reference Assets in the branches
(Reference assets have negative id values, hence the id < 0 part)
Select asset_id INTO #REFLinks FROM #AllAssets WHERE id in
(Select #AllAssets.asset_id FROM #AllAssets Inner Join #RelativeIDs
on #AllAssets.id = #RelativeIDs.id Where #RelativeIDs.id < 0)
Step 4. Get the branches of anything found in step 3
Select DISTINCT L.id into #extraRelativeIDs FROM #treeIDs
CROSS APPLY dbo.SplitIDs(idList) as L
WHERE
exists (Select #REFLinks.asset_id FROM #REFLinks WHERE #REFLinks.asset_id = #treeIDs.id)
and Not Exists (select id FROM #RelativeIDs Where id = #treeIDs.id)
I've tried to just show the relevant code. I am super grateful to anyone who can help me find a better solution!
--getting all of the children of a root node ( could be > 1 ) and it would require revising the query a bit
DECLARE #AssetID int = (select AssetId from Asset where AssetID is null);
--algorithm is relational recursion
--gets the top level in hierarchy we want. The hierarchy column
--will show the row's place in the hierarchy from this query only
--not in the overall reality of the row's place in the table
WITH Hierarchy(Asset_ID, AssetID, Levelcode, Asset_hierarchy)
AS
(
SELECT AssetID, Asset_ID,
1 as levelcode, CAST(Assetid as varchar(max)) as Asset_hierarchy
FROM Asset
WHERE AssetID=#AssetID
UNION ALL
--joins back to the CTE to recursively retrieve the rows
--note that treelevel is incremented on each iteration
SELECT A.Parent_ID, B.AssetID,
Levelcode + 1 as LevelCode,
A.assetID + '\' + cast(A.Asset_id as varchar(20)) as Asset_Hierarchy
FROM Asset AS a
INNER JOIN dbo.Batch AS Hierarchy
--use to get children, since the parentId of the child will be set the value
--of the current row
on a.assetId= b.assetID
--use to get parents, since the parent of the Asset_Hierarchy row will be the asset,
--not the parent.
on Asset.AssetId= Asset_Hierarchy.parentID
SELECT a.Assetid,a.name,
Asset_Hierarchy.LevelCode, Asset_Hierarchy.hierarchy
FROM Asset AS a
INNER JOIN Asset_Hierarchy
ON A.AssetID= Asset_Hierarchy.AssetID
ORDER BY Hierarchy ;
--return results from the CTE, joining to the Asset data to get the asset name
---that is the structure you will want. I would need a little more clarification of your table structure
It would help to know your underlying table structure. There are two approaches which should work depending on your environment: SQL understands XML so you could have your SQL as an xml structure or simply have a single table with each row item having a unique primary key id and a parentid. id is the fk for the parentid. The data for the node are just standard columns. You can use a cte or a function powering a calculated column to determin the degree of nesting for each node. The limit is that a node can only have one parent.

Linking and counting records with one or multiple criteria

I am attempting to link together some records to identify sibling groups. The way we can do this is to identify clients who share the same parents.
A sample of the SQL follows:
SELECT
A.ClientID,
B.ParentID
FROM A
LEFT JOIN B ON B.ClientID to A.ClientID
AND B.REL_END is NULL AND B.REL_CODE = 'PAR'
Which would return data in the following format:
Client ID Parent ID
1 A
1 B
2 C
2 D
3 C
3 E
4 C
4 D
How I would like it to appear follows:
Client ID No. of Siblings
1 0
2 2
3 2
4 2
Hopefully the table shows that child 1 has 0 siblings (shares no parents with 2,3,4), child 2 has 2 siblings (3 and 4), child 3 has 2 siblings (2 and 4) and child 4 has 2 siblings (2,3). It seems like it should be simple enough to achieve this but I'm really struggling at the minute to think how! I think it's made slightly more confusing because a child may share only one parent with another child to be considered as a sibling.
Hopefully this is clear, thanks.
Edit: Just to make it clear, the relationship is identified by a child sharing a parent ID with another child (the ids are unique, but I provided generic ones for this example). So as Child 2, 3 and 4 all have a parent with an id of C they are considered siblings.
You may try this query, it displays to me the desired output.
with c_data as (
select a.clientid, b.parentid, count(a.clientid) over (partition by parentid order by parentid) as c_parents
FROM A
LEFT JOIN B ON (B.ClientID = A.ClientID)
AND B.REL_END is NULL AND B.REL_CODE = 'PAR'
)
select clientid as "Client ID", max(c_parents) -1 as "No of Siblings"
from c_data
group by clientid;
Example:
SQL> with c_data as (
2 select a.clientid, b.parentid, count(a.clientid) over (partition by parentid order by parentid) as c_parents
3 FROM A
4 LEFT JOIN B ON (B.ClientID = A.ClientID)
5 AND B.REL_END is NULL AND B.REL_CODE = 'PAR'
6 )
7 select clientid as "Client ID", max(c_parents) -1 as "No of Siblings"
8 from c_data
9 group by clientid;
Client ID No of Siblings
---------- --------------
1 0
2 2
4 2
3 2
Transcurrido: 00:00:00.03
SQL>
With the analytic function we count all client id's partitioned by the parentid related of the current tupple to count all clients that have in common the same parent.
After in the projection, we get the max number of parent in common for each client and substracts 1, the client itself.
Hope this helps!
Regards!
This is rather complicated. If you can assume exactly two parents for each child, then you can do something like:
select c.*, count(*) over (partition by min_parent, max_parent) - 1 as NumSiblings
from (SELECT A.ClientID, min(B.ParentID) as min_parent, max(b.parentid) as max_parent
FROM A LEFT JOIN
B
ON B.ClientID to A.ClientID AND B.REL_END is NULL AND B.REL_CODE = 'PAR'
group by a.clientid
) c
What this does is calculate the two parents for each client. It then uses the windows function to count the number of clients that have exactly the same parents. The "-1" is because all children are counted, and we don't want to count the current child.
If you can have more than two parents, then the query is more complicated.
If you want only one parent being shared (rather than two), then you can handle using a self join:
with cp as (SELECT A.ClientID, B.ParentID
FROM A LEFT JOIN
B
ON B.ClientID to A.ClientID AND B.REL_END is NULL AND B.REL_CODE = 'PAR'
)
select cp.client_id, count(distinct cp1.client_id) as NumSiblings
from cp left outer join
cp cp1
on cp.parent_id = cp1.parent_id and cp.client_id <> cp1.client_id
group by cp.client_id
Well, I don't understand how the table and relation are made, but you could do something like that :
SELECT ClientID, sum(NumberOfSibling) as NumberOfSibling
from(
SELECT A.ClientID, (select count(ClientID)
from A2
LEFT JOIN B2 ON B2.ClientID to A2.ClientID
AND B2.REL_END is NULL AND B2.REL_CODE = 'PAR'
where B2.ParentID = B.ParentID) as NumberOfSibling
FROM A
LEFT JOIN B ON B.ClientID to A.ClientID
AND B.REL_END is NULL AND B.REL_CODE = 'PAR'
) GROUP BY ClientID
Explanation : I took your request and modify the second result to return the sum of siblings wich possess the same Parent (that's the select count(*) ..., that's also the part I'm not sure about the conditions). And put this in another to group by ClientID to have the sum of the 2 parents.