In SQL it's not possible to fetch a hierarchy at once when the hierarchy is stored in a single table. You can use Marcus' approach, you can also use a precalculation table with some precalc code. In that table, you store for every leaf the path to the root (so leaf's parent, then that parent's parent etc.).
The precalc makes sure updates of teh parent field of a leaf updates the leaf's paths. It's not easy, but the result is you can fetch a complete graph in one select.
Example: I've a category table for my cms.:
CREATE TABLE [CESys_Categories] (
[CategoryID] [int] IDENTITY (1, 1) NOT FOR REPLICATION NOT NULL ,
[Description] [varchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[ParentCategoryID] [int] NOT NULL ,
[LastModifiedAt] [datetime] NOT NULL ,
CONSTRAINT [PK_CESys_Categories] PRIMARY KEY CLUSTERED
(
[CategoryID]
) ON [PRIMARY]
) ON [PRIMARY]
precalc table:
CREATE TABLE [CESys_CategoryParents_Precalc] (
[CategoryID] [int] NOT NULL ,
[ParentCategoryIDInPath] [int] NOT NULL ,
CONSTRAINT [PK_CESys_CategoryParents_Precalc] PRIMARY KEY CLUSTERED
(
[CategoryID],
[ParentCategoryIDInPath]
) ON [PRIMARY]
) ON [PRIMARY]
When you delete a category, the precalc's have to be deleted as well of course.
Now the procedure I use (it's a stored proc based system, little old )
--------------------------------------------------------------------
-- Stored procedure that will insert in CESys_CategoryParents_Precalc all
-- parent CategoryID's of the given CategoryID. Consider category A. A has
-- 2 childs: B and C. B has a child D. The childs plus the root in this hierarchy are stored on
-- 'leafs' (pathnodes) on the path from outerleaf (here: D or C) to the root (here: A)
-- This would result in the following parentinpath table:
--
-- CategoryID | ParentCategoryIDInPath
---------------------------------------
-- A | 0
-- B | A
-- C | A
-- D | B
-- D | A
--
-- This way, fast non recursive retrieval is possible of all categories in the
-- hierarchical path of a given category. If an Item is assigned to category D,
-- it has category B and A also, because these categories are in the path.
--
-- Gets: iCategoryID int, ID of category which parent categories should be determined
-- Gets: iLeafCategoryID int, ID of category on the pathnode (leaf) on the path from iCategoryID to the root of
-- the Category hierarchy. In the example above, this can be B or A for iCategoryID=D.
-- Returns: iErrorCode int, 0 if succeeded, > 0 if SQLserver error.
-- Uses recursion (calls itself)
--------------------------------------------------------------------
-- 08-aug-2001
-- [FB] First Version
--------------------------------------------------------------------
ALTER PROCEDURE [pr_cesys_PrecalculateCategoryIDPathsPerCategory]
@iCategoryID int,
@iLeafCategoryID int,
@iErrorCode int OUTPUT
AS
DECLARE @iParentCID int,
@bLocalTrans bit
-- User is allowed to perform the action.
IF @@TRANCOUNT >0
BEGIN
SAVE TRANSACTION transPCPPC
SET @bLocalTrans=0
END
ELSE
BEGIN
-- Start new transaction
BEGIN TRANSACTION transPCPPC
SET @bLocalTrans=1
END
-- Get the parent of the given Category.
SELECT @iParentCID=ParentCategoryID FROM CESys_Categories WHERE CategoryID=@iLeafCategoryID
SELECT @iErrorCode=@@ERROR
IF @iErrorCode<>0
GOTO Handler
-- Check if the categoryID found is a parent
IF @iParentCID > 0
BEGIN
-- has a parent. store parent as ParentCategoryIDInPath in CESys_CategoryParents_Precalc
INSERT CESys_CategoryParents_Precalc (CategoryID, ParentCategoryIDInPath)
VALUES (@iCategoryID, @iParentCID)
SELECT @iErrorCode=@@ERROR
IF @iErrorCode<>0
GOTO Handler
-- went ok. Dive into recursion. This will limit the amount of levels in a category hierarchie to 32.
EXEC pr_cesys_PrecalculateCategoryIDPathsPerCategory @iCategoryID, @iParentCID, @iErrorCode OUTPUT
IF @iErrorCode<>0
GOTO Handler
END
ELSE
BEGIN
-- No parent. Insert a record with '0' as the parent ONLY if this routine
INSERT CESys_CategoryParents_Precalc (CategoryID, ParentCategoryIDInPath)
VALUES (@iCategoryID, 0)
SELECT @iErrorCode=@@ERROR
IF @iErrorCode<>0
GOTO Handler
END
-- Done
IF @bLocalTrans=1
BEGIN
COMMIT TRANSACTION transPCPPC
END
SELECT @iErrorCode=0
RETURN
Handler:
-- iErrorCode has to have a value
ROLLBACK TRANSACTION transPCPPC
RETURN
This is doable in your own code as well without a problem (in C#/VB.NET with llblgen pro code) So in the rare cases the category gets updated (i.e. changes graph position), this routine is ran. The selects of the graph is very fast, just a join and a silly sort