34

Given a table with a hierarchyid type column, how do you write a query to return all rows that are ancestors of a specific node?

There is an IsDescendantOf() function, which is perfect for getting the children, but there's no corresponding IsAncestorOf() function to return ancestors (and the absence of a GetAncestors() function seems like quite an oversight.)

gotqn
  • 42,737
  • 46
  • 157
  • 243
marc esher
  • 4,871
  • 3
  • 36
  • 51

6 Answers6

39

The most commonly used approach would be a recursive Common Table Expression (CTE)

WITH Ancestors(Id, [Name], AncestorId) AS
(
      SELECT
            Id, [Name], Id.GetAncestor(1)
      FROM
            dbo.HierarchyTable
      WHERE
            Name = 'Joe Blow'  -- or whatever you need to select that node

      UNION ALL

      SELECT
            ht.Id, ht.[Name], ht.Id.GetAncestor(1)
      FROM
            dbo.HierarchyTable ht
      INNER JOIN 
            Ancestors a ON ht.Id = a.AncestorId
)
SELECT *, Id.ToString() FROM Ancestors

(adapted from a Simon Ince blog post)

Simon Ince also proposes a second approach where he just basically reverses the condition - instead of detecting those person entries that are an ancestor of the target person, he turns the check around:

DECLARE @person hierarchyid

SELECT @person = Id
FROM dbo.HierachyTable
WHERE [Name] = 'Joe Blow';

SELECT
    Id, Id.ToString() AS [Path], 
    Id.GetLevel() AS [Level],
    Id.GetAncestor(1),
    Name
FROM 
    dbo.HierarchyTable
WHERE 
    @person.IsDescendantOf(Id) = 1

This will select all the rows from your table, where the target person you're interested in is a descendant of - any level down the hierarchy. So this will find that target person's immediate and non-immediate ancestors all the way up to the root.

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
  • 5
    In that blogpost, isn't this CTE solution then followed by a simpler one ("This works fine, but is it the optimum way of achieving it? Nope. Let’s try again!") ? – AakashM Jun 25 '10 at 17:07
  • @AakashM: yes, there is a second option, indeed - not one that I would probably use, but it will work, too, from the looks of it. – marc_s Jun 25 '10 at 17:12
  • 2
    I know this is very old, but I write this for future readers: The method from "Simon Ince blog post" is nearly 100 times slower than the "CTE" method when the execution plan doesn't exist. – Achilles Apr 20 '17 at 13:36
  • 1
    Yup @Achilles, it is probably because the query becomes non-sargable when you apply IsDescendantOf function to ID of every row. I am always getting an index scan with the query. The recursive CTE seems to be a much better option. – Nisarg Shah Jan 11 '19 at 12:32
17

Here's an answer rolled into a single select:

SELECT t1.Id.ToString() as Path, t1.Name
    FROM (SELECT * FROM HierarchyTable
        WHERE Name = 'Joe Blow') t2,
    HierarchyTable t1
    WHERE t2.Id.IsDescendantOf(t1.Id) = 1
Rahlon
  • 29
  • 10
Richard
  • 6,215
  • 4
  • 33
  • 48
  • The first predicate of the where clause is redundant since a parent is always a descendant of itself. http://msdn.microsoft.com/en-us/library/bb677203(v=sql.105).aspx – influent Dec 17 '14 at 17:53
4
Declare @hid hierarchyid=0x5D10 -- Child hierarchy id

SELECT
*
FROM 
  dbo.TableName
WHERE 
  @hid.IsDescendantOf(ParentHierarchyId) = 1
muthuvel
  • 1,092
  • 10
  • 17
  • Even if you have an index on the hierarchyID, it will have to evaluate IsDesendentOf for every row, no? I think I have a better way (see my answer) – Ben Thul Feb 14 '16 at 01:48
1

I wrote a user-defined table-valued function that expands a hierarchyid value into its constituent ancestors. The output can then be joined back on the hierarchyid column to get those ancestors specifically.

alter function dbo.GetAllAncestors(@h hierarchyid, @ReturnSelf bit)
returns table
as return
 select @h.GetAncestor(n.Number) as h
 from dbo.Numbers as n
 where n.Number <= @h.GetLevel()
  or (@ReturnSelf = 1 and n.Number = 0)

 union all

 select @h
 where @ReturnSelf = 1
go

To go about using it:

select child.ID, parent.ID
from dbo.yourTable as child
cross apply dbo.GetAllAncestors(child.hid, 1) as a
join dbo.yourTable as parent
   on parent.hid = a.h
Ben Thul
  • 31,080
  • 4
  • 45
  • 68
  • please help me to solve this problem. http://stackoverflow.com/questions/44016261/how-do-you-get-recursivelevel-using-sql-server-2012-hierarchyid – Manojkanth May 17 '17 at 05:36
  • I recently had to solve this problem and I believe this is the best solution offered here. The other solutions that pass a column reference into IsDescendantOf() have to resolve that predicate in the Query Executor and not the Storage Engine. If you have any amount of data, performance will be terrible. – Jason Pease Jul 31 '18 at 11:31
  • I've since written aCLR function that does the same thing. – Ben Thul Jul 31 '18 at 12:21
  • I like how you think. I'm going to steal that idea. – Jason Pease Jul 31 '18 at 13:59
0

Perfecting Ben Thui's answer which I find to be the best one so far...

The approach below allows to retrieve not only one but potentially several leaf rows and their ascendants in one single query.

Create Or Alter Function dbo.GetAllAncestors
(
    @Path       HierarchyId,
    @WithSelf   Bit = 1,
    @MinLevel   Int = 0,
    @MaxLevel   Int = Null
)
Returns Table
As
Return

With Ancestor As
(
    Select  @Path As Path
    Union All

    Select  Path.GetAncestor(1)
    From    Ancestor
    Where   Path.GetLevel() > 0
)

Select  Path, Path.GetLevel() As Level
From    Ancestor
Where   (@WithSelf = 1 Or Path <> @Path)
And     Path.GetLevel() >= Case When @MinLevel < 0 Or @MinLevel Is Null Then 0 Else @MinLevel End
And     (@MaxLevel Is Null Or Path.GetLevel() <= @MaxLevel)

To use:

-- This assumes the table has a Path HierarchyId colum, and the values are unique and indexed.

-- If you know the path
Select *
From MyTable
Where Path In
(
    Select Path From dbo.GetAllAncestors(@ThePath, Default, Default, Default)
)

-- If you don't know the path
Select *
From MyTable t1
Where Path In 
(
    Select Path
    From   MyTable t2
           Cross Apply dbo.GetAllAncestors(t2.Path, Default, Default, Default)
    Where  /* Find the leaf record(s) here.
              Note that if multiple rows match, they will all be returned as well as their parents in a single roundtrip. */
)
James Dingle
  • 10,581
  • 2
  • 14
  • 4
-1
DECLARE @hid_Specific HIERARCHYID 
SET @hid_Specific = '/1/1/3/1/';

SELECT hrchy_id,* FROM tblHierarchyData 
WHERE PATINDEX(hrchy_id.ToString() + '%', @hid_Specific.ToString()) = 1
wuerfelfreak
  • 2,363
  • 1
  • 14
  • 29