Please start any new threads on our new site at https://forums.sqlteam.com. We've got lots of great SQL Server experts to answer whatever question you can come up with.

 All Forums
 SQL Server 2005 Forums
 Transact-SQL (2005)
 Running total of records related to hierarchy

Author  Topic 

pootle_flump

1064 Posts

Posted - 2008-04-30 : 06:32:02
SQL Server 2005.
Schema is not fixed at this stage - small project, schema can be adapted if neccessary.
This needs to be produced in SQL Server - there is no client application.

Hi

I am really struggling to write the SQL for the following requirement and have got to the stage where I think I need someone to give me some pointers. I can't believe this is a novel requirement but I haven't had to do this before nor can find anything similar when googling.

I have a hierarchy detailing an organisation structure. Eventually the table will store many structures and these may one day be "pseudo structures" to enable really granular reporting. As such I cannot really say "there will never be more than N levels to this hierarchy". Currently I'm using a standard adjacency design. From now on I'll call the items in the table "business units".

There is a further table with a related FK declared which associates people with business units.

Requirement: Return all business units and cumulative total total of employees for each of these units (i.e. the total for each business unit will be the number of people in that unit plus the sum of the people in all the child units).

I've asked this question on dbforums but ended up a blind alley despite Peter's attempt to help. I'm starting again from scratch but feel free to review the (now dead) thread here:
http://www.dbforums.com/showthread.php?t=1629366

To summarise - I tried the CTE route but of course this evaluates the hierarchy from the top down. To fulfill the requirement, the bottom of the structure needs to be the starting point. I also tried reversing the hierarchy so that children, rather than the parent, are stored but my query fails if there are 3+ levels.

Code to reproduce problem (note - I expect that I will need to get the employee count per business unit prior to working with the hierarchy so I have flattened the schema to reflect this - I think I can handle the SQL to get the non-cumulative count per business unit ):
DECLARE    @table TABLE 
(
unit_code TINYINT
, parent_code TINYINT
, headcount INT
, PRIMARY KEY CLUSTERED (unit_code)
)

INSERT INTO @table (unit_code, parent_code, headcount)
SELECT 1, 4, 10 UNION ALL
SELECT 2, 4, 130 UNION ALL
SELECT 3, 10, 93 UNION ALL
SELECT 4, 10, 7 UNION ALL
SELECT 10, NULL, 1


Required output:

unit_code cumulative_headcount
------------------------------------
1 10
2 130
3 93
4 147
10 241


Many thanks

RyanRandall
Master Smack Fu Yak Hacker

1074 Posts

Posted - 2008-04-30 : 07:17:47
When dealing with hierarchical data like this, it can be useful to create a function which returns the 'path' of ancestors. You can then use that path in various ways...

-- Structure and data
create table MyTable
(
unit_code TINYINT
, parent_code TINYINT
, headcount INT
, PRIMARY KEY CLUSTERED (unit_code)
)

INSERT INTO MyTable (unit_code, parent_code, headcount)
SELECT 1, 4, 10 UNION ALL
SELECT 2, 4, 130 UNION ALL
SELECT 3, 10, 93 UNION ALL
SELECT 4, 10, 7 UNION ALL
SELECT 10, NULL, 1
go

-- Function
create function dbo.AncestorPath(@unit_code int) returns varchar(100) as
begin
declare @Path varchar(100)
while 0 = 0
begin
select @Path = cast(unit_code as varchar(5)) + isnull('/' + @Path, ''), @unit_code = parent_code
from dbo.MyTable where unit_code = @unit_code

if @@rowcount = 0 break
end

return @Path
end
go

-- Calculation
select unit_code, (select sum(headcount) from MyTable where '/' + dbo.AncestorPath(unit_code) + '/'
like '%/' + cast(a.unit_code as varchar(10)) + '/%') as cumulative_headcount
from MyTable a

/* Results
unit_code cumulative_headcount
--------- --------------------
1 10
2 130
3 93
4 147
10 241
*/
See also these:
http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=101967
http://www.sqlteam.com/Forums/topic.asp?TOPIC_ID=101053

And, as in the first of these links, a recursive CTE solution will be possible. The tradeoff is usually between understandability and speed.


Ryan Randall
Solutions are easy. Understanding the problem, now, that's the hard part.
Go to Top of Page

RyanRandall
Master Smack Fu Yak Hacker

1074 Posts

Posted - 2008-04-30 : 07:31:36
Here's a possible recursive CTE approach...

; WITH a AS (
SELECT unit_code, parent_code, CAST(unit_code AS VARCHAR(MAX)) AS Path, headcount
FROM MyTable WHERE parent_code is null
UNION ALL
SELECT b.unit_code, b.parent_code, a.Path + '/' + CAST(b.unit_code AS VARCHAR(MAX)), b.headcount
FROM MyTable b INNER JOIN a ON a.unit_code = b.parent_code
)
SELECT unit_code, (SELECT SUM(headcount) FROM a WHERE '/' + Path + '/'
like '%/' + CAST(b.unit_code AS VARCHAR(10)) + '/%') as cumulative_headcount
FROM a b ORDER BY unit_code


Ryan Randall
Solutions are easy. Understanding the problem, now, that's the hard part.
Go to Top of Page

SwePeso
Patron Saint of Lost Yaks

30421 Posts

Posted - 2008-04-30 : 08:00:30
[code];WITH Yak (unit_code, parent_code, headcount, path)
AS (
SELECT unit_code,
parent_code,
headcount,
'/' + cast(unit_code as varchar(max)) + '/'
FROM mytable AS ts
WHERE parent_code is null

UNION ALL

select t.unit_code,
t.parent_code,
t.headcount,
y.path + cast(t.unit_code as varchar(max)) + '/'
FROM Yak AS y
INNER JOIN mytable AS t ON t.parent_code = y.unit_code
)

SELECT p.unit_code,
SUM(y.headcount)
FROM Yak AS y
CROSS APPLY (
SELECT unit_code,
parent_code,
headcount
FROM Yak AS t
WHERE y.Path LIKE '%/' + cast(t.unit_code as varchar(12)) + '/%'
) AS p
group by p.unit_code[/code]


E 12°55'05.25"
N 56°04'39.16"
Go to Top of Page

pootle_flump

1064 Posts

Posted - 2008-04-30 : 09:36:23
Ryan - thank you very much. That example makes total sense. As I understand it, you are essentially create a materialised path out of the adjacency table which then makes the solution possible.
quote:
Originally posted by RyanRandall

And, as in the first of these links, a recursive CTE solution will be possible.
I was rewriting into a CTE and got as far your second attempt. Trying to do it in a non-scalar fashion was troublesome and then I checked back and saw Peter had posted code....
;WITH Yak (unit_code, parent_code, headcount, path)
AS (
SELECT unit_code,
parent_code,
headcount,
'/' + cast(unit_code as varchar(max)) + '/'
FROM mytable AS ts
WHERE parent_code is null

UNION ALL

select t.unit_code,
t.parent_code,
t.headcount,
y.path + cast(t.unit_code as varchar(max)) + '/'
FROM Yak AS y
INNER JOIN mytable AS t ON t.parent_code = y.unit_code
)

SELECT p.unit_code,
SUM(y.headcount)
FROM Yak AS y
CROSS APPLY (
SELECT unit_code,
parent_code,
headcount
FROM Yak AS t
WHERE y.Path LIKE '%/' + cast(t.unit_code as varchar(12)) + '/%'
) AS p
group by p.unit_code
I didn't even know you could do the bit in red! I thought CROSS APPLY was only for table functions. I had previously tried the same logic with INNER JOIN (expecting the error SQL Server threw). Peter - AFAICT this is undocumented. Do you have any links\ info regarding this?


Thanks again guys - awesome ... although I believe you missed the 6 minutes stipulated in my SLA - who do I speak to so I can get my money back?
Go to Top of Page

pootle_flump

1064 Posts

Posted - 2008-04-30 : 09:41:06
Ooops. I think I read this a bit ago. Obviously it did not sink in very far.....
http://weblogs.sqlteam.com/jeffs/jeffs/archive/2007/10/18/sql-server-cross-apply.aspx
Go to Top of Page
   

- Advertisement -