3

I'm struggling with a optimizing a SQL query and am looking for help. T-SQL for SQL Server 2008.

I have a set of Agents that have columns Id and ManagerId. A manager is just an agent, so ManagerId is like a foreign key back to the same table. I'm writing a query to bring back the agents in an ordered list based on a managerial hierarchy.

Given the set

Id  Name    ManagerId
-----------------------
1   Charlie 4
2   Alpha   NULL
3   Echo    5
4   Bravo   2
5   Delta   1
6   Foxtrot 3
7   Golf    6
8   Hotel   7
9   Juliet  8
10  India   8

I want to return the values in this order:

Id  Name    ManagerId
2   Alpha   NULL
4   Bravo   2
1   Charlie 4
5   Delta   1
3   Echo    5
6   Foxtrot 3
7   Golf    6
8   Hotel   7
9   Juliet  8
10  India   8

The strategy I am using now works great for 10 values. The real set I will use it on is about 12,000. When I use the below query on a test set of 10,000, it takes forever ~ 20 minutes on my laptop. I'm using a loop with sub-queries so I know there must be a better way.

CREATE TABLE #hierarchy (rowNumber INT, agentId INT);
CREATE TABLE #finishedManagers (id INT);

DECLARE @index INT = 1;
DECLARE @count INT = (SELECT COUNT(Id) FROM agents);
DECLARE @thisId INT;

WHILE (@index <= @count)

BEGIN
    SET @thisId = ( 
        SELECT TOP 1
            a.Id
        FROM    
            agents a
        WHERE
            a.Id NOT IN (SELECT * FROM #finishedManagers)
            AND
            (a.ManagerId IS NULL OR a.ManagerId IN (SELECT agentId FROM #heirarchy))
            );

    INSERT INTO #hierarchy (rowNumber, agentId)
        SELECT
            @index,
            @thisId

    SET @index = @index + 1;

    INSERT INTO #finishedManagers(id)
        SELECT
            @thisId 
END
GO

SELECT 
    a.*
FROM 
    #hierarchy h
LEFT JOIN
    agents a ON h.agentId = a.Id
ORDER BY
    h.rowNumber;

DROP TABLE #hierarchy;
DROP TABLE #finishedManagers;

How would you go about this?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
stv2pointO
  • 33
  • 4

1 Answers1

1

First off, always try to avoid loops when possible.

The following is an example of using a Recursive CTE in concert with the HIERARCHY data type.

Recursive CTEs are great, and well worth your time to get comfortable with them. However, the performance can suffer a bit with larger/deeper hierarchies.

There are other techniques using TEMP tables which are more performant, but a little more involved.

Example

Declare @Top   int  = null  --<<  Sets top of Hier Just for FUN Try 3

;with cteP as (
      Select ID
            ,ManagerID 
            ,Name 
            ,HierID = convert(hierarchyid,'/'+convert(varchar(25),ID)+'/')
      From   YourTable 
      Where  IsNull(@Top,-1) = case when @Top is null then isnull(ManagerID ,-1) else ID end
      Union  All
      Select ID  = r.ID
            ,ManagerID  = r.ManagerID 
            ,Name   = r.Name
            ,HierID = convert(hierarchyid,p.HierID.ToString()+convert(varchar(25),r.ID)+'/')
      From   YourTable r
      Join   cteP p on r.ManagerID  = p.ID)
Select Lvl   = HierID.GetLevel()
      ,ID
      ,Name  
      ,ManagerID
 From cteP A
 Order By A.HierID

Returns

Lvl ID  Name    ManagerID
1   2   Alpha   NULL
2   4   Bravo   2
3   1   Charlie 4
4   5   Delta   1
5   3   Echo    5
6   6   Foxtrot 3
7   7   Golf    6
8   8   Hotel   7
9   9   Juliet  8
9   10  India   8

EDIT - Temp Table Approach 25,000 row in 2 seconds

Notice that I have max depth of 30 levels.

Declare @Top int =null 
Select *
      ,Lvl=1
      ,HierID = convert(hierarchyid,'/'+convert(varchar(25),ID)+'/')
 Into #TempBld 
 From YourTable
 Where  IsNull(@Top,-1) = case when @Top is null then isnull(ManagerID,-1) else ID end

Declare @Cnt int=1
While @Cnt<=30
    Begin
        Insert Into #TempBld 
        Select A.*
              ,Lvl=B.Lvl+1
              ,HierID = convert(hierarchyid,b.HierID.ToString()+convert(varchar(25),a.ID)+'/')
         From  YourTable A
         Join  #TempBld B on (B.Lvl=@Cnt and A.ManagerID=B.ID)
        Set @Cnt=@Cnt+1
    End

Select Lvl
      ,ID
      ,Name
      ,ManagerID
 From #TempBld
 Order by HierID
John Cappelletti
  • 79,615
  • 7
  • 44
  • 66
  • John, where could I learn more about a decent temp table approach? I should have mentioned that there are hundreds of levels in the hierarchy. Thanks so much for your response, I'll be giving your cte approach a try for now but based on your statement am guessing it will still struggle with performance... – stv2pointO Sep 16 '18 at 01:01
  • @stv2pointO What is the max depth of your current hierarchy? Give me a few and I'll post a second answer. The Temp table approach does loop BY LEVEL not record, and I can generate a 200K point hierarchy in under 6 seconds. – John Cappelletti Sep 16 '18 at 01:05
  • WoW! 5 seconds! I have just converted to the church of THE COMMON TABLE EXPRESSION! – stv2pointO Sep 16 '18 at 01:09
  • My test set had a depth of 500. Just queried the real set at work, it is 1023. Your solution seems to be great. Thanks again – stv2pointO Sep 16 '18 at 01:12
  • For the life of me, I can't figure out why I'm losing rows. With a test set of 9999 values, each having a unique Id from 1-9999 and ManagerId of Null or a random number from 1-500, I can only get back 9358 results with either method. Any ideas why this would happen? The id's are maxed at four characters, so varchar(25) is oodles. HierarchyId.tostring() returns nvarchar(4000), no way I'm over that limit. I don't see the bug... – stv2pointO Sep 16 '18 at 01:54
  • @stv2pointO In your random ManagerID any pointing to themselves or a child? We run massive hierarchies 200K+. If any dropped our ledger would not balance. – John Cappelletti Sep 16 '18 at 02:00
  • @stv2pointO I'll take a harder look to see if I can recreate your issue – John Cappelletti Sep 16 '18 at 02:01
  • 1
    PALM --> FACE. thanks. There was a little incest in my test data's family tree :) – stv2pointO Sep 16 '18 at 02:10
  • @stv2pointO Aside: [This](https://stackoverflow.com/questions/15080922/infinite-loop-cte-with-option-maxrecursion-0/15081353#15081353) answer demonstrates one way of detecting "incest" and not falling into the trap of infinite recursion. – HABO Sep 16 '18 at 15:55