24

I have got two tables as following

Table Person

  Id   Name
   1    A
   2    B
   3    C
   4    D
   5    E

Table RelationHierarchy

ParentId   ChildId
   2         1
   3         2
   4         3

This will form a tree like structure

          D
          |
          C
          |
          B
          |
          A

ParentId and ChildId are foreign keys of Id column of Person Table

I need to write SQL that Can fetch me Top Level Parent i-e Root. Can anyone suggest any SQL that can help me accomplish this

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
  • Oracle uses `connect by Prior` . mySQL has it's own using joins or possibly SQL server uses `for XML_Path` Other databases may support `Common Table Expressions` and recursive statements using these CTEs since there is such variety knowing what DBMS effects what syntax is available. link on SQL server how to: http://stackoverflow.com/questions/10045683/t-sql-get-root-node-in-hierarchy – xQbert Jul 16 '13 at 12:58
  • possible duplicate of [Finding Top level parent of each row of a table \[SQL Server 2008\]](http://stackoverflow.com/questions/17703863/finding-top-level-parent-of-each-row-of-a-table-sql-server-2008) – Prahalad Gaggar Apr 07 '15 at 09:42

9 Answers9

35

You can use recursive CTE to achieve that:

DECLARE @childID INT 
SET @childID  = 1 --chield to search

;WITH RCTE AS
(
    SELECT *, 1 AS Lvl FROM RelationHierarchy 
    WHERE ChildID = @childID

    UNION ALL

    SELECT rh.*, Lvl+1 AS Lvl FROM dbo.RelationHierarchy rh
    INNER JOIN RCTE rc ON rh.CHildId = rc.ParentId
)
SELECT TOP 1 id, Name
FROM RCTE r
inner JOIN dbo.Person p ON p.id = r.ParentId
ORDER BY lvl DESC

SQLFiddle DEMO

EDIT - for updated request for top level parents for all children:

;WITH RCTE AS
(
    SELECT  ParentId, ChildId, 1 AS Lvl FROM RelationHierarchy 

    UNION ALL

    SELECT rh.ParentId, rc.ChildId, Lvl+1 AS Lvl 
    FROM dbo.RelationHierarchy rh
    INNER JOIN RCTE rc ON rh.ChildId = rc.ParentId
)
,CTE_RN AS 
(
    SELECT *, ROW_NUMBER() OVER (PARTITION BY r.ChildID ORDER BY r.Lvl DESC) RN
    FROM RCTE r

)
SELECT r.ChildId, pc.Name AS ChildName, r.ParentId, pp.Name AS ParentName
FROM CTE_RN r
INNER JOIN dbo.Person pp ON pp.id = r.ParentId
INNER JOIN dbo.Person pc ON pc.id = r.ChildId
WHERE RN =1

SQLFiddle DEMO

EDIT2 - to get all persons change JOINS a bit at the end:

SELECT pc.Id AS ChildID, pc.Name AS ChildName, r.ParentId, pp.Name AS ParentName
FROM dbo.Person pc 
LEFT JOIN CTE_RN r ON pc.id = r.CHildId AND  RN =1
LEFT JOIN dbo.Person pp ON pp.id = r.ParentId

SQLFiddle DEMo

Nenad Zivkovic
  • 18,221
  • 6
  • 42
  • 55
  • I think mostly CTE answer are more or less right but i found this one perfectly suited for my problem. Many thanks – InTheWorldOfCodingApplications Jul 17 '13 at 09:49
  • Can we modify this one to GET Parents of ALL persons i-e PersonName And it's Top level parent for each row in Person table – InTheWorldOfCodingApplications Jul 17 '13 at 15:13
  • @user1711287 Sure, check the edited answer. Note the slight difference in recursive part of CTE that `ChildID` are now rewritten each time from the anchor part. Also addition is `ROW_NUMBER()` function to get the top level for each child at the end. – Nenad Zivkovic Jul 17 '13 at 15:32
  • Great It works fine. Can i get All persons regards of whether parent Exist or not. If parent Exist then it's Name and Id will be display otherwise NULL as PArentID. WHich part i need to change? – InTheWorldOfCodingApplications Jul 17 '13 at 15:38
  • I guess once this CTE returns a result set, i need to do a left join between Person and ResultSet of CTE – InTheWorldOfCodingApplications Jul 17 '13 at 15:40
  • @Change Yes, change SELECT to `FROM Person LEFT JOIN CTE` and move `RN=1 in join condition`. http://sqlfiddle.com/#!6/7355f/5 – Nenad Zivkovic Jul 17 '13 at 15:42
  • Mind blowing. It's fantastic and does exactly what i needed. – InTheWorldOfCodingApplications Jul 17 '13 at 16:06
  • Sometimes it gets into infinite loop. I have checked the data and data has no circular chains. Do we need Max(lvl) thing anywhere? – InTheWorldOfCodingApplications Jul 18 '13 at 13:05
  • @user1711287 Recursive CTE by default can't get into infinite loops as there is default max level of recursion at 100. It breaks when reaches it. Adding `OPTION (MAXRECURSION n)` at the very end of query can change the max(lvl). If you have more then 100 levels of data you can set `OPTION (MAXRECURSION 0)` to have unlimited recursion. You can also add `WHERE Lvl < n` inside the bottom part of CTE to stop it manually at some level (without raising error). – Nenad Zivkovic Jul 18 '13 at 13:17
  • I have added MAXRecurrsion 0. Level is not infinite but still it stucks sometime. Do you think logic is fine? – InTheWorldOfCodingApplications Jul 18 '13 at 13:21
  • It doesn't stop even when there is no parent of a parent. It keeps finding parent e.g. it finds Combinations of parent let's say five rows then it finds those five again and again and keep doing that – InTheWorldOfCodingApplications Jul 18 '13 at 13:30
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/33703/discussion-between-nenad-zivkovic-and-user1711287) – Nenad Zivkovic Jul 18 '13 at 13:31
  • I have got the pattern where its getting into infinite loop. Lets say Parent of A is B and B has Two Parents C and D. It's stucks in such scenario – InTheWorldOfCodingApplications Jul 18 '13 at 13:36
  • @user1711287 Hi, please check the other question http://stackoverflow.com/questions/17703863/finding-top-level-parent-of-each-row-of-a-table-sql-server-2008 where I have made an update on answer - an improved query that should not have a performance issues. Also tested for multiple parents - http://sqlfiddle.com/#!6/2ae2c/2 – Nenad Zivkovic Jul 19 '13 at 07:48
  • I have a similar requirement. When I select the top parent node and click some button to populate the tree structure it works fine, but when i select intermediate node and click button, it doesnt populate the tree structure. Like in above example I want to display from C->B->A instead of D->C->B->A. Any suggestion? I have posted the question here http://stackoverflow.com/questions/41542191/tree-nodes-not-populating-child-nodes – Chethan Jan 10 '17 at 09:38
10

I've used this pattern to associate items in a hierarchy with the item's root node.

Essentially recursing the hierarchies maintaining the values of the root node as additional columns appended to each row. Hope this helps.

with allRows as (
    select ItemId, ItemName, ItemId [RootId],ItemName [RootName] 
    from parentChildTable
    where ParentItemId is null
    union all
    select a1.ItemId,a1.ItemName,a2.[RootId],a2.[RootName]
    from parentChildTable a1
    join allRows a2 on a2.ItemId = a1.ParentItemId
)   

select * from allRows
Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
Tinman
  • 101
  • 1
  • 3
6

To find all top-level parents, use a query like:

select p.Name
from Person p
where not exists
(select null
 from RelationHierarchy r
 where r.ChildId = p.Id)

SQLFiddle here.

To find the top-level parent of a specific child, use:

with cte as
(select t.ParentId TopParent, t.ChildId 
 from RelationHierarchy t
 left join RelationHierarchy p on p.ChildId = t.ParentId
 where p.ChildId is null
 union all
 select t.TopParent TopParent, c.ChildId 
 from cte t
 join RelationHierarchy c on t.ChildId = c.ParentId)
select p.name
from cte h
join Person p on h.TopParent = p.Id
where h.ChildId=3 /*or whichever child is required*/

SQLFiddle here.

3

Try this.

The recursive CTE will find the person and walk up the hierarchy until it finds no parent.

-- This CTE will find the ancestors along with a measure of how far up
-- the hierarchy each ancestor is from the selected person.
with ancestor as (
  select ParentId as AncestorId, 0 as distance
  from RelationHierarchy
  where CHildId = ?

  union all

  select h.ParentId, a.distance + 1
  from ancestor a inner join RelationHierarchy rh on a.AncestorId = rh.ChildId
)
select AncestorId
from ancestor
where distance = (select max(distance) from ancestor)
Brandon
  • 9,822
  • 3
  • 27
  • 37
  • 1
    Looking back at this answer I wrote 5.5 years later, it would probably have been simpler to use an `order by` with `select top 1` in the query rather than the scalar sub-query. `select top 1 AncestorId from ancestor order by distance desc`. – Brandon Jan 03 '19 at 16:22
2

Something like this will work for above example:

SELECT ParentId FROM RelationHierarchy 
WHERE ParentId NOT IN (SELECT CHildId FROM RelationHierarchy)
jmarceli
  • 19,102
  • 6
  • 69
  • 67
1

The only way you can do this in "standard" SQL is to assume a maximum depth for the tree, and then do joins for each level. The following gets the top level id:

select rh1.ChildId,
       coalesce(rh4.parentid, rh3.parentid, rh2.parentid, rh1.parentid) as topLevel
from RelationshipHierarchy rh1 left outer join
     RelationshipHierarchy rh2
     on rh1.parentId = rh2.childId left outer join
     RelationshipHierarchy rh3
     on rh2.parentId = rh3.childId left outer join
     RelationshipHierarchy rh4
     on rh3.parentId = rh4.childId;

If you want the name, you can just join it in:

select rh1.ChildId,
       coalesce(rh4.parentid, rh3.parentid, rh2.parentid, rh1.parentid) as topLevel,
       p.name
from RelationshipHierarchy rh1 left outer join
     RelationshipHierarchy rh2
     on rh1.parentId = rh2.childId left outer join
     RelationshipHierarchy rh3
     on rh2.parentId = rh3.childId left outer join
     RelationshipHierarchy rh4
     on rh3.parentId = rh4.childId left outer join
     Person p
     on p.id = coalesce(rh4.parentid, rh3.parentid, rh2.parentid, rh1.parentid);
Gordon Linoff
  • 1,242,037
  • 58
  • 646
  • 786
1

Get all top parents using path

path format: rootId/.../parentId/nodeId/

select t1.path from nodes t1 inner join nodes t2
on t1.path like t2.path+'%' 
group by t1.path 
having len(t1.path)-len(replace(t1.path, '/', ''))
=min(len(t2.path)-len(replace(t2.path, '/', '')))
Eqbal Sajadi
  • 97
  • 1
  • 4
0

Give this a go:

    select id,name
    from person p
    where not exists
    (
    select 1 
    from relationhierarchy r
    where r.childid= p.id
    )
    and exists
    (
    select 1 
    from relationhierarchy r
    where r.parentid= p.id
    )

It is not enough to just see if a child id exists as in your example E is present in the person table but not in the relationshiphierarchy table.

0
WITH CTE_MyTable AS (
    SELECT        Id, ParentId, NULL As RootParent, 1 As Lvl
    FROM            dbo.Ministry
    UNION ALL
    SELECT        a.id, b.ParentId, a.ParentId As RootParent, Lvl + 1
    FROM            CTE_MyTableAS a INNER JOIN
                                               dbo.MyTableAS b ON a.ParentId = b.Id
)
, CTE_Ministry_RN AS  (
    SELECT Id, RootParent, ROW_NUMBER() OVER (PARTITION BY Id ORDER BY Lvl DESC) RN
    FROM CTE_Ministry
)

SELECT Id, ISNULL(RootParent, Id) As RootParent
FROM CTE_Ministry_RN
WHERE RN = 1
Zanyar Jalal
  • 1,407
  • 12
  • 29