Click here to Skip to main content
Click here to Skip to main content

T- SQL - How to get all descendants of a given element in a hierarchical table

By , 13 May 2009
 

Introduction

It's common in an e-commerce website to retrieve all products from a given category, including all descendant categories. This article provides a T-SQL table valued function with a simple solution to this problem. We assume that categories are defined in a hierarchical table with a self referencing foreign key.

Using the code

First, we have to create the hierarchical table (in order to keep things simple, we use Name as the table key):

CREATE TABLE [dbo].[Category](
    [ParentName] [varchar](20) NULL,
    [Name] [varchar](20) NOT NULL,
 CONSTRAINT [PK_Category] PRIMARY KEY CLUSTERED 
(
    [Name] ASC
)
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[Category]  ADD  
CONSTRAINT [FK_Category_Category] FOREIGN KEY([ParentName])
REFERENCES [dbo].[Category] ([Name])
GO

Then, we create a recursive Stored Procedure that we will use to populate the table with test data:

CREATE PROC spCreateChilds

@ParentName    varchar(20),
@BaseName        varchar(20),
@Level        integer,
@MaxDepth        integer,
@ChildNumber    integer
        
AS

IF @Level < @MaxDepth
 BEGIN

  DECLARE @I Integer

  SET @I = 1

  WHILE (@I < @ChildNumber AND @ParentName IS NOT NULL) OR @I = 1
   BEGIN

    INSERT INTO Category
    (Name, ParentName)
    VALUES        
    (ISNULL(@BaseName, '') + CASE WHEN 
          @BaseName IS NULL THEN '' ELSE '.' END  
          + CAST(@I as varchar), @ParentName)

    DECLARE @ExecParentName varchar(20)
    DECLARE @ExecBaseName varchar(20)
    DECLARE @ExecLevel int

    SET @ExecParentName = ISNULL(@BaseName, '') + 
         CASE WHEN @BaseName IS NULL 
     THEN '' ELSE '.' END  + CAST(@I as varchar)
    SET @ExecBaseName = ISNULL(@BaseName, '') 
         + CASE WHEN @BaseName IS NULL THEN '' ELSE '.' END  + 
         CAST(@I as varchar)
    SET @ExecLevel = @Level +1

    EXECUTE [dbo].[spCreateChilds] 
            @ExecParentName
           ,@ExecBaseName
           ,@ExecLevel
           ,@MaxDepth
           ,@ChildNumber

    SET @I = @I + 1

 END                
END

And, of course, we call it!

EXECUTE [dbo].[spCreateChilds] 
            NULL
           ,NULL
           ,1
           ,4  -- 4 hierarchical levels
           ,30 -- 30 childs per category

Now, our table is full of test data, it's time to define our functions.

The first function retrieves all the ancestors, and it's quite simple:

CREATE FUNCTION [dbo].[GetAscendantCategories] 
(    
    @CategoryName varchar(20)
)
RETURNS @Result TABLE  (Name varchar(20))
AS
    BEGIN

    WHILE @CategoryName IS NOT NULL
        BEGIN
            INSERT INTO @Result
            SELECT    @CategoryName
            
            SELECT     @CategoryName = ParentName
            FROM     dbo.Category
            WHERE   Name = @CategoryName
    
        END
    
    RETURN     
END

The second function is the one we will actually use. It retrieves all the descendants of a row:

CREATE FUNCTION [dbo].[GetDescendantCategories] 
(    
    @CategoryName varchar(20)
)
RETURNS @Result TABLE  (Name varchar(20))
AS
    BEGIN

    INSERT INTO @Result
    SELECT 
            Name
    FROM     
            dbo.Category C
    WHERE     @CategoryName in (
                       SELECT P.Name 
                       FROM GetAscendantCategories(C.Name) P)
    
    RETURN     
END

And now, we can test it, passing a test key!

SELECT * 
FROM [dbo].[GetDescendantCategories] ('1.2')

It's easy to use this function in product retrieval queries. Here's an example (let's suppose we have a table called Product with a field CategoryName and a foreign key from this field to the Category table):

SELECT *
FROM Product P
INNER JOIN [dbo].[GetDescendantCategories] ('1.2') C
ON P.CategoryKey = C.Name

Optimization

OK, that's cool, but I realized this solution is really very slow. It's much better if we use a common table expression. Here's the new function (thanks to Eddie de Bears):

CREATE FUNCTION [dbo].[GetDescendantCategories]
(
    @CategoryName varchar(20)
)
RETURNS @Result TABLE (Name varchar(20)) AS BEGIN


    WITH Result (Name)
    AS
    (
        SELECT Name
        FROM Category
        WHERE Name = @CategoryName
        UNION ALL
        SELECT C.Name FROM Category C
        INNER JOIN Result R ON C.ParentName = R.Name
    )
    INSERT INTO @Result
    SELECT Name
    FROM Result

    RETURN

END

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Paolo Costa
Software Developer (Senior)
Italy Italy
Member
Paolo Costa is a software developer with long experience on any kind of .NET application. He lives in Italy and works in Switzerland for a credit card payment acquiring company.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralIncorrect syntaxmemberGiorgio Bozio29 Oct '09 - 7:01 
Hi Paolo, I was trying your functions but the one that gets descendants yields a syntax error near '.' on the line "... FROM dbo.GetAscendantRoles (C.Category) P) ..." has this something to do with version compatibility?
Thanks,
Giorgio
GeneralRe: Incorrect syntaxmemberPaolo Costa29 Oct '09 - 7:12 
What version of SQL Server are you using? I just run it on SQL SERVER 2005 and it was fine.
GeneralRe: Incorrect syntaxmemberPaolo Costa29 Oct '09 - 7:14 
I looked at the code, I forgot to qualify the function call with the schema. Try to change it to dbo.GetAscendantCategories
GeneralRe: Incorrect syntaxmemberGiorgio Bozio29 Oct '09 - 22:19 
The server in 2005 but the db is set on compatibility level 80. is your solution requiring compatibility 90?
Hope not, since your functions would be extremely handy...!
Thanks
GeneralRe: Incorrect syntaxmemberGiorgio Bozio29 Oct '09 - 22:32 
Solved! the improved descendant function you posted in the comments works even on compatibility 80! Thanks a lot!
GeneralJust in timememberThoWeib30 Apr '09 - 1:32 
I needed this kind of solution for an internal database. I arrived at those Common Table Expressions myself, but could not get the recursion to work for me the way I wanted them to.
 
After reading your article I saw the light, and now it just works... Cool | :cool:
 
Thanks a lot. Thumbs Up | :thumbsup:
 
Any problem may be solved by using a bigger hammer.

GeneralRe: Just in timememberPaolo Costa30 Apr '09 - 10:07 
Really glad this article could help you. Let's thank together Eddie de Bear too, who gave the idea of using a recursive CTE Smile | :)
GeneralCommon Table ExpressionsmemberEddie de Bear28 Apr '09 - 12:38 
Heya, Nice job. This is an approach I've used in older versions of SQL Server. However, you may want to take a look at CTEs (Common Table Expressions) introduced in SQL Server 2005. It allows you to define a single T-SQL select statement that will handle the recursion for you. Easy as Big Grin | :-D
GeneralRe: Common Table Expressionsmemberpaolocosta28 Apr '09 - 13:04 
Thanks, can you post an example?
GeneralRe: Common Table Expressionsmemberpaolocosta28 Apr '09 - 13:28 
I did myself, here's the code:
 
CREATE FUNCTION [dbo].[GetDescendantCategories]
(
@CategoryName varchar(20)
)
RETURNS @Result TABLE (Name varchar(20)) AS BEGIN
 

WITH Result (Name)
AS
(
SELECT Name
FROM Category
WHERE Name = @CategoryName
UNION ALL
SELECT C.Name FROM Category C
INNER JOIN Result R ON C.ParentName = R.Name
)
INSERT INTO @Result
SELECT Name
FROM Result
 
RETURN
 
END
 

I tested it on a big table and it seems to have the same perfomance of the second query (the original posted in the article is very slow). Thanks for suggestion!

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130523.1 | Last Updated 13 May 2009
Article Copyright 2009 by Paolo Costa
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid