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.
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.HiI 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=1629366To 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 ALLSELECT 2, 4, 130 UNION ALLSELECT 3, 10, 93 UNION ALLSELECT 4, 10, 7 UNION ALLSELECT 10, NULL, 1 Required output:unit_code cumulative_headcount------------------------------------1 102 1303 934 14710 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 datacreate 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 ALLSELECT 2, 4, 130 UNION ALLSELECT 3, 10, 93 UNION ALLSELECT 4, 10, 7 UNION ALLSELECT 10, NULL, 1go-- Functioncreate function dbo.AncestorPath(@unit_code int) returns varchar(100) asbegin 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 @Pathendgo-- Calculationselect unit_code, (select sum(headcount) from MyTable where '/' + dbo.AncestorPath(unit_code) + '/' like '%/' + cast(a.unit_code as varchar(10)) + '/%') as cumulative_headcountfrom MyTable a/* Resultsunit_code cumulative_headcount--------- --------------------1 102 1303 934 14710 241*/ See also these:http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=101967http://www.sqlteam.com/Forums/topic.asp?TOPIC_ID=101053And, 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. |
|
|
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_headcountFROM a b ORDER BY unit_code Ryan Randall Solutions are easy. Understanding the problem, now, that's the hard part. |
|
|
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 yCROSS APPLY ( SELECT unit_code, parent_code, headcount FROM Yak AS t WHERE y.Path LIKE '%/' + cast(t.unit_code as varchar(12)) + '/%' ) AS pgroup by p.unit_code[/code] E 12°55'05.25"N 56°04'39.16" |
|
|
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 yCROSS APPLY ( SELECT unit_code, parent_code, headcount FROM Yak AS t WHERE y.Path LIKE '%/' + cast(t.unit_code as varchar(12)) + '/%' ) AS pgroup 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? |
|
|
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 |
|
|
|
|
|
|
|