Showing posts with label Hierarchy. Show all posts
Showing posts with label Hierarchy. Show all posts

Monday, April 12, 2010

usp_DrawTree

Here is the source code for the usp_DrawTree procedure that was highlighted in my last blog post that was part of T-SQL Tuesday #005: Reporting.

You can also download this code from my SkyDrive.

Update on Jun13,2012: The code below has been updated since 2010. The code unfortunately made an assumption that the data in the temp table #TreeResult would be pulled out (via SELECT) in the same order it was INSERTed. That's incorrect. So I added an IDENTITY KEY to the table and an ORDER BY to the SELECT.

use TempDB /* Set to whatever database you wish */
go

if object_id('usp_DrawTree','P') is not null drop procedure usp_DrawTree
go

create procedure usp_DrawTree
@ID
int = null /* Desired ID value */
,@HangStyle bit = 0 /* Draw in Hanging Style? (vs. Left/Right Balanced) */
,@Direction char(1) = 'T' /* Branch Direction: R=Right L=Left M=Middle T=Top */
,@Level int = 1 /* Level to generate */
as

set nocount on

/* Make sure the Direction is valid */
if @Direction not in ('R','L','M','T')
begin
raiserror('Invalid @Direction... Must be R or L or M or T.',16,1)
return -1
end

/* Make sure the Level is valid */
if @Direction='T' set @Level=1
if @Level<1
begin
raiserror('Invalid @Level value.',16,1)
return -1
end

/*
If we are at Level 1 (the top Level), then...
Make sure our input table exists and that it has the required columns.
Also create some temp tables that will be needed as we build the tree.
*/
if @Level=1
begin
if object_id('tempdb..#TreeData','U') is null
begin
raiserror('You must have a #TreeData table prepared.',16,1)
return -1
end

declare @TreeDataColumns table
(
ColumnName nvarchar(128)
,DataType varchar(30)
)

insert @TreeDataColumns
select Name
,type_name(System_Type_ID)
from TempDB.sys.columns
where Object_ID=object_id('TempDB..#TreeData','U')

if not exists(select *
from @TreeDataColumns
where ColumnName='ID' and DataType='int')
begin
raiserror('#TreeData must contain an int column called ID.',16,1)
return -1
end
if not exists(select *
from @TreeDataColumns
where ColumnName='ParentID' and DataType='int')
begin
raiserror('#TreeData must contain an int column called ParentID.',16,1)
return -1
end
if not exists(select *
from @TreeDataColumns
where ColumnName='DataForBox')
begin
raiserror('#TreeData must contain a column called DataForBox.',16,1)
return -1
end
if not exists(select *
from @TreeDataColumns
where ColumnName='ExtraInfo')
begin
raiserror('#TreeData must contain a column called ExtraInfo.',16,1)
return -1
end
if not exists(select *
from @TreeDataColumns
where ColumnName='SortColumn')
begin
raiserror('#TreeData must contain a column called SortColumn.',16,1)
return -1
end

/* This table houses information about the branches at each level */
if object_id('TempDB..#BranchInfo','U') is not null drop table #BranchInfo
create table #BranchInfo
(
TreeLevel int primary key /* The Level 1..n */
,BoxWidth int /* The Width of the Box at that level */
,InProcess bit /* Is branch in process at that level? */
)
/* This table houses our final result that we will return to the user */
if object_id('TempDB..#TreeResult','U') is not null drop table #TreeResult
create table #TreeResult
(
TreeID int identity(1,1)
,TreeContent nvarchar(max)
)

end


/*
If the desired ID is null, then initialize it to whichever ID
in the table has a null ParentID, which will be the head honcho.
Note that TOP 1 is used just in case there is more than 1
row in the table with a ParentID equal to null
*/
if @ID is null
begin
set @ID=(select top 1 ID
from #TreeData
where ParentID is null)
end

/* Acquire the DataForBox and ExtraInfo columns into variables */
declare @DataForBox nvarchar(max)
,@ExtraInfo nvarchar(max)
select @DataForBox=cast(DataForBox as nvarchar(max))
,@ExtraInfo=cast(coalesce(ExtraInfo,'') as nvarchar(max))
from #TreeData
where ID=@ID

/* Define some more variables */
declare @MaxWidth int /* Maximum Width of Box */
,@NumBoxLines int /* Number of Lines of Data in Box */
,@PrimaryBoxLine int /* The Box Line from which we sprout a branch */
,@NumSubords int /* Quantity of Subordinates */
,@NumOnRight int /* Number of Subordinates on Right Side */
,@Counter int /* All-purpose counting variable */
,@Output nvarchar(max) /* String that we build for each line of output */
,@BoxData nvarchar(max) /* String containing line of data to put in Box */

declare @IDToProcess int /* The ID to process when we call recursively */
,@DirectionToProcess char(1) /* And the Direction to process */
,@LevelToProcess int /* And the Level to process */

/*
The @DataForBox variable contains the data we want to put in a box.
Optional vertical bars (|) can be used to indicate multiple lines to
display in the box.
The following @BoxLines table variable will split the @DataForBox
variable into rows.
*/
declare @BoxLines table
(
SeqNo int identity(1,1) primary key
,BoxData nvarchar(max)
)
insert @BoxLines
select XMLNode.value('(./text())[1]','nvarchar(max)')
from (select XMLString='<i>'+replace(@DataForBox,'|','</i><i>')+'</i>') X
cross apply (select XMLList=cast(XMLString as xml).query('.')) F1
cross apply XMLList.nodes('i') F2(XMLNode)

/*
What's the maximum width of a line in the box?
How many lines are there?
And which is the primary line (i.e. middle line) from which we sprout a child branch?
*/
select @MaxWidth=max(len(BoxData))
,@NumBoxLines=count(*)
,@PrimaryBoxLine=ceiling(1.0*count(*)/2)
from @BoxLines

/* Create an entry in #BranchInfo for the current Level if it doesn't exist */
if not exists (select * from #BranchInfo where TreeLevel=@Level)
insert #BranchInfo (TreeLevel) values (@Level)

/*
Populate #BranchInfo with information about the Level's BoxWidth
and the status of the Level in terms of Branches
*/
update #BranchInfo
set InProcess=case when @Direction in ('M','L') then 1 else 0 end
,BoxWidth=@MaxWidth+4+1 /* 4=LeftBorder+Space+Space+RightBorder, 1=BranchOutput */
where TreeLevel=@Level

/*
Collect all the subordinates of @ID into a table variable.
Order them by the SortColumn.
*/
declare @Subordinates table
(
SeqNo int identity(1,1) primary key
,ID int
)
insert @Subordinates
select ID
from #TreeData
where ParentID=@ID
order by SortColumn

/*
How many subordinates are there?
And How many on the right side?
*/
select @NumSubords=count(*)
,@NumOnRight=case
when @HangStyle=1
then 0
else count(*)/2
end
from
@Subordinates

/*
Before we draw our box, we must recursively process
all children on our right side
*/
set @Counter=0
while @Counter<@NumOnRight
begin
set @Counter=@Counter+1
select @IDToProcess=(select ID from @Subordinates where SeqNo=@Counter)
,@DirectionToProcess=case when @Counter=1 then 'R' else 'M' end
,@LevelToProcess=@Level+1
exec usp_DrawTree @IDToProcess
,@HangStyle
,@DirectionToProcess
,@LevelToProcess
end

/* Draw all branches in process for previous levels */
set @Output=''
select @Output=@Output+case
when InProcess=1
then N'│ '
else N' '
end
+space(BoxWidth)
from #BranchInfo
where TreeLevel<@Level

/*
And now draw...
1) Any in-process branch for our current level
2) The top border of our box
3) Any branch that might connect to right-hand children
*/
set @Output=@Output+case
when @Direction in ('L','M')
then N'│ '
else N' '
end
+N'┌'+replicate(N'─',@MaxWidth+2)+N'┐'
+case
when @NumSubords=0 then N' '
when @NumOnRight=0 then N' '
else N' │'
end

/* Save that to our result table */
insert #TreeResult (TreeContent)
select @Output

/*
Now it's time to output the data within the the box itself.
We will perform a loop for each line to be written in the box.
*/
set @Counter=0
while @Counter<@NumBoxLines
begin
set @Counter=@Counter+1

/* Bet the @BoxData for the line to be written */
select @BoxData=ltrim(rtrim(BoxData))
from @BoxLines
where SeqNo=@Counter

/* Pad it with spaces to center it in the box */
set @BoxData=space((@MaxWidth-len(@BoxData))/2)+@BoxData
set @BoxData=@BoxData+space(@MaxWidth-len(@BoxData))


/* Draw all branches in process for previous levels */
set @Output=''
select @Output=@Output+case
when InProcess=1
then N'│ '
else N' '
end
+space(BoxWidth)
from #BranchInfo
where TreeLevel<@Level

/*
And now draw...
1) Any in-process branch for our current level and the left border of our box
2) The line in the box (@BoxData) padded with leading and trailing space
3) The right border of our box and any any branch that might connect to children
4) Any Extra Info to appear to the right of the box
*/
set @Output=@Output+case
when @Counter<@PrimaryBoxLine
then case
when @Direction='R' then N' │'
when @Direction='L' then N'│ │'
when @Direction='M' then N'│ │'
when @Direction='T' then N' │'
end
when @Counter=@PrimaryBoxLine
then case
when @Direction='R' then N'┌─┤'
when @Direction='L' then N'└─┤'
when @Direction='M' then N'├─┤'
when @Direction='T' then N' │'
end
else case
when @Direction='R' then N'│ │'
when @Direction='L' then N' │'
when @Direction='M' then N'│ │'
when @Direction='T' then N' │'
end
end
+N' '+@BoxData+N' '
+case
when @Counter<@PrimaryBoxLine
then case
when @NumSubords=0 then N'│'
when @NumOnRight=0 then N'│'
else N'│ │'
end
when @Counter=@PrimaryBoxLine
then case
when @NumSubords=0 then N'│'
when @NumOnRight=0 then N'├─┐'
else N'├─┤'
end
else case
when @NumSubords=0 then N'│'
when @NumOnRight=0 then N'│ │'
else N'│ │'
end
end
+case
when @ExtraInfo<>'' and @Counter=@PrimaryBoxLine
then ' '+@ExtraInfo
else ''
end

/* Save that to our result table */
insert #TreeResult (TreeContent)
select @Output

end

/* Draw all branches in process for previous levels */
set @Output=''
select @Output=@Output+case
when InProcess=1
then N'│ '
else N' '
end
+space(BoxWidth)
from #BranchInfo
where TreeLevel<@Level

/*
And now draw...
1) Any in-process branch for our current level
2) The bottom border of our box
3) Any branch that might connect to left-hand children
*/
set @Output=@Output+case
when @Direction in ('R','M')
then N'│ '
else N' '
end
+N'└'+replicate(N'─',@MaxWidth+2)+N'┘'
+case
when @NumSubords=0 then N' '
when @NumOnRight=0 then N' │'
else N' │'
end

/* Save that to our result table */
insert #TreeResult (TreeContent)
select @Output

/*
Populate #BranchInfo with information about the current
Level's status in terms of Branches
*/
update #BranchInfo
set InProcess=case when @Direction in ('R','M') then 1 else 0 end
where
TreeLevel=@Level

/*
Now that we've finished drawing our box, we may recursively
process all children on our left side
*/
set @Counter=@NumOnRight
while @Counter<@NumSubords
begin
set @Counter=@Counter+1
select @IDToProcess=(select ID from @Subordinates where SeqNo=@Counter)
,@DirectionToProcess=case when @Counter=@NumSubords then 'L' else 'M' end
,@LevelToProcess=@Level+1
exec usp_DrawTree @IDToProcess
,@HangStyle
,@DirectionToProcess
,@LevelToProcess
end

/*
If we are at this point with Level 1, that means we have
successfully finished everything. So return our result
and clean up our temporary tables that we created.
*/
if @Level=1
begin
select TreeContent from #TreeResult order by TreeID
drop table #BranchInfo
drop table #TreeResult
end

T-SQL Tuesday #005: Reporting

This blog entry is participating in T-SQL Tuesday #005, hosted this month by Aaron Nelson. You are invited to visit his blog to join the party and read more blogs participating in this month’s theme: Reporting.

Speaking of “Reporting”, we now join Action News in progress…



Maria the Anchorwoman… fear that one airline charging $45 for carry-on bags will create a domino effect, with other airlines most likely following suit and charging as well.

In a related story, one airline is considering offering discounts for children as long as their parents are willing to store their children in overhead bins.

A spokesman for the airlines also confirmed that all meals will eventually be eliminated from flights and passengers will instead receive buckets of peanuts.

And now, Action News technical reporter Ella Vader has some exciting news from the SQL Server world. Ella?


Ella Vader, ReporterThank you, Maria. Since the introduction of SQL Server 2005, developers have had the ability to create recursive CTE’s that can process a self-referential table and produce a simple hierarchical report of manager/employee relationships or a bill of materials report.

For example, Books Online provides a crude example of displaying a hierarchical list using vertical bars to represent relationships in the AdventureWorks database. Here is an example of an organizational chart of EmployeeID #3, Roberto Tamburello:

with DirectReports(Name,EmployeeID,EmployeeLevel,Sort) as
(
select cast(c.LastName+', '+c.FirstName as varchar(255))
,e.EmployeeID
,1
,cast(c.LastName+', '+c.FirstName as varchar(255))
from AdventureWorks.HumanResources.Employee e
join AdventureWorks.Person.Contact c on e.ContactID=c.ContactID
where e.EmployeeID=3 /* Roberto Tamburello */
union all
select cast(replicate('| ',EmployeeLevel)+c.LastName+', '+c.FirstName as varchar(255))
,e.EmployeeID
,d.EmployeeLevel+1
,cast(rtrim(Sort)+'| '+c.LastName+', '+FirstName as varchar(255))
from AdventureWorks.HumanResources.Employee e
join AdventureWorks.Person.Contact c on e.ContactID=c.ContactID
join DirectReports d on e.ManagerID=d.EmployeeID
)
select Name
from DirectReports
order by Sort
/*
Tamburello, Roberto
| Cracium, Ovidiu
| | D'Hers, Thierry
| | Galvin, Janice
| Erickson, Gail
| Goldberg, Jossef
| Miller, Dylan
| | Margheim, Diane
| | Matthew, Gigi
| | Raheem, Michael
| Salavaria, Sharon
| Sullivan, Michael
| Walters, Rob
*/
This report is functional, but the final output is about as exciting as watching a CURSOR routine finish up.

However, all is not lost. SQL Blogger Brad Schulz has come up with a routine that will produce a hierarchical report in T-SQL with a little more pizazz. Simply create a temp table with specific columns called #TreeData and call his stored procedure called usp_DrawTree:

if object_id('tempdb..#TreeData','U') is not null drop table #TreeData
select ID=EmployeeID
,ParentID=ManagerID
,DataForBox=LastName+', '+FirstName
,ExtraInfo=''
,SortColumn=LastName+', '+FirstName
into #TreeData
from AdventureWorks.HumanResources.Employee e
join AdventureWorks.Person.Contact c on e.ContactID=c.ContactID

exec usp_DrawTree 3 /* Employee ID #3 = Roberto Tamburello */
/*
┌─────────────────┐
┌─┤ D'Hers, Thierry │
│ └─────────────────┘
┌─────────────────┐ │
┌─┤ Cracium, Ovidiu ├─┤
│ └─────────────────┘ │
│ │ ┌────────────────┐
│ └─┤ Galvin, Janice │
│ └────────────────┘
│ ┌────────────────┐
├─┤ Erickson, Gail │
│ └────────────────┘
│ ┌──────────────────┐
├─┤ Goldberg, Jossef │
│ └──────────────────┘
┌─────────────────────┐ │
│ Tamburello, Roberto ├─┤
└─────────────────────┘ │
│ ┌─────────────────┐
│ ┌─┤ Margheim, Diane │
│ │ └─────────────────┘
│ ┌───────────────┐ │
├─┤ Miller, Dylan ├─┤
│ └───────────────┘ │
│ │ ┌───────────────┐
│ ├─┤ Matthew, Gigi │
│ │ └───────────────┘
│ │ ┌─────────────────┐
│ └─┤ Raheem, Michael │
│ └─────────────────┘
│ ┌───────────────────┐
├─┤ Salavaria, Sharon │
│ └───────────────────┘
│ ┌───────────────────┐
├─┤ Sullivan, Michael │
│ └───────────────────┘
│ ┌──────────────┐
└─┤ Walters, Rob │
└──────────────┘
*/
This stored procedure accepts two optional parameters. The first parameter, @ID, is an integer indicating the identifying key of the person at the top of the hierarchy. If one does not pass a value for @ID, then the stored procedure will automatically use the ID in the #TreeData table that has a ParentID of NULL.

The second parameter, @HangStyle, is a parameter of datatype bit. A value of 0 (the default) indicates that one wants a balanced tree output, as shown previously, while a value of 1 indicates that a hanging style is desired, as illustrated here:

exec usp_DrawTree 3,1  /* or exec usp_DrawTree @ID=3, @HangStyle=1 */
/*
┌─────────────────────┐
│ Tamburello, Roberto ├─┐
└─────────────────────┘ │
│ ┌─────────────────┐
├─┤ Cracium, Ovidiu ├─┐
│ └─────────────────┘ │
│ │ ┌─────────────────┐
│ ├─┤ D'Hers, Thierry │
│ │ └─────────────────┘
│ │ ┌────────────────┐
│ └─┤ Galvin, Janice │
│ └────────────────┘
│ ┌────────────────┐
├─┤ Erickson, Gail │
│ └────────────────┘
│ ┌──────────────────┐
├─┤ Goldberg, Jossef │
│ └──────────────────┘
│ ┌───────────────┐
├─┤ Miller, Dylan ├─┐
│ └───────────────┘ │
│ │ ┌─────────────────┐
│ ├─┤ Margheim, Diane │
│ │ └─────────────────┘
│ │ ┌───────────────┐
│ ├─┤ Matthew, Gigi │
│ │ └───────────────┘
│ │ ┌─────────────────┐
│ └─┤ Raheem, Michael │
│ └─────────────────┘
│ ┌───────────────────┐
├─┤ Salavaria, Sharon │
│ └───────────────────┘
│ ┌───────────────────┐
├─┤ Sullivan, Michael │
│ └───────────────────┘
│ ┌──────────────┐
└─┤ Walters, Rob │
└──────────────┘
*/
The key to making this all work is that the procedure calls itself recursively. For each node in the hierarchy, the child nodes on the right (if you look at the chart by turning your head counter-clockwise) are recursively drawn first, then the node itself, and then the child nodes on the left are recursively drawn. In the case of the hanging style output, there are no nodes on the “right” side.

The #TreeData table that acts as the source data is required to consist of the following 5 columns:
  • ID: An integer column that contains a unique identifier of the node in the hierarchy.

  • ParentID: An integer column that contains an identifier indicating the node’s parent in the hierarchy.

  • DataForBox: A column (convertible to nvarchar) that contains the data that should appear in the node’s box in the report. You can introduce a multiple lines within the box by using a vertical bar character to indicate where the line breaks will occur.

  • ExtraInfo: A column (convertible to nvarchar) that contains an extra (single) line of information that you would like to include in the report for that node.

  • SortColumn: A column that the procedure will use to sort the child nodes under their parent.

Here is another example using the NorthWind database. The DataForBox column contains the Employee’s FirstName and LastName and Phone Extension, separated by vertical bars, indicating that they should be output as three separate lines within the box. The ExtraInfo column contains the Employee’s title. Note that no parameters are passed, indicating that the procedure should produce the report for the Employee whose ParentID is NULL:

if object_id('tempdb..#TreeData','U') is not null drop table #TreeData
select ID=EmployeeID
,ParentID=ReportsTo
,DataForBox=FirstName+'|'+LastName+'|'+'Ext'+cast(Extension as varchar(20))
,ExtraInfo=Title
,SortColumn=LastName+', '+FirstName
into #TreeData
from Northwind.dbo.Employees

exec usp_DrawTree
/*
┌───────────┐
│ Anne │
┌─┤ Dodsworth │ Sales Representative
│ │ Ext452 │
│ └───────────┘
┌──────────┐ │
│ Steven │ │
┌─┤ Buchanan ├─┤ Sales Manager
│ │ Ext3453 │ │
│ └──────────┘ │
│ │ ┌────────┐
│ │ │ Robert │
│ ├─┤ King │ Sales Representative
│ │ │ Ext465 │
│ │ └────────┘
│ │ ┌─────────┐
│ │ │ Michael │
│ └─┤ Suyama │ Sales Representative
│ │ Ext428 │
│ └─────────┘
│ ┌──────────┐
│ │ Laura │
├─┤ Callahan │ Inside Sales Coordinator
│ │ Ext2344 │
│ └──────────┘
┌─────────┐ │
│ Andrew │ │
│ Fuller ├─┤ Vice President, Sales
│ Ext3457 │ │
└─────────┘ │
│ ┌─────────┐
│ │ Nancy │
├─┤ Davolio │ Sales Representative
│ │ Ext5467 │
│ └─────────┘
│ ┌───────────┐
│ │ Janet │
├─┤ Leverling │ Sales Representative
│ │ Ext3355 │
│ └───────────┘
│ ┌──────────┐
│ │ Margaret │
└─┤ Peacock │ Sales Representative
│ Ext5176 │
└──────────┘
*/
And here is one more example, back in AdventureWorks, of a Bill Of Materials hierarchy for ProductID #826, the LL Road Rear Wheel. Note that the boxes contain a Product Number and a Color, if one is defined for the component, and the ExtraInfo column contains the Weight, if one is defined:

if object_id('tempdb..#TreeData','U') is not null drop table #TreeData
select ID=ComponentID
,ParentID=ProductAssemblyID
,DataForBox=Name+'|'+ProductNumber+coalesce('|('+Color+')','')
,ExtraInfo=coalesce('(Weight='
+convert(varchar(10),Weight)
+rtrim(WeightUnitMeasureCode)+') '
,'')
,SortColumn=Name
into #TreeData
from AdventureWorks.Production.BillOfMaterials b
join AdventureWorks.Production.Product p on b.ComponentID=p.ProductID
where b.StartDate<=getdate()
and (b.EndDate is null or b.EndDate>=getdate())

exec usp_DrawTree 826
/*
┌──────────┐
┌─┤ LL Shell │
│ │ SH-4562 │
│ └──────────┘
┌─────────┐ │
┌─┤ LL Hub ├─┤
│ │ HU-6280 │ │
│ └─────────┘ │
│ │ ┌─────────────────┐
│ └─┤ LL Spindle/Axle │
│ │ SD-2342 │
│ └─────────────────┘
│ ┌───────────┐
├─┤ LL Nipple │
│ │ NI-4127 │
│ └───────────┘
│ ┌─────────────┐
├─┤ LL Road Rim │ (Weight=445.00G)
│ │ RM-R436 │
│ └─────────────┘
┌────────────────────┐ │
│ LL Road Rear Wheel │ │
│ RW-R623 ├─┤ (Weight=1050.00G)
│ (Black) │ │
└────────────────────┘ │
│ ┌──────────────┐
├─┤ LL Road Tire │
│ │ TI-R092 │
│ └──────────────┘
│ ┌───────────┐
├─┤ Reflector │
│ │ RF-9198 │
│ └───────────┘
│ ┌────────────────┐
├─┤ Road Tire Tube │
│ │ TT-R982 │
│ └────────────────┘
│ ┌─────────┐
└─┤ Spokes │
│ SK-9283 │
└─────────┘
*/
We don’t have the room to show you the complete BOM for the entire bicycle, ProductID #770, the Black Road-650-52, but you can download the code from Mr Schulz’s SkyDrive and give it a try yourself.

So will this technique of hierarchy reporting revolutionize the SQL Server industry? Of course not. But it is our duty to report on these new developments for our viewers as they occur.

This is Ella Vader, reporting to you from Silicon Valley, California. Back to you, Maria.


Thank you for that very informative report, Ella. May I do the usual anchor-desk gimmick of asking you a seemingly insightful follow-up question that appears to be impromptu, but in reality, is something we rehearsed earlier?

You just did, Maria.

Well, thank you again, Ella.

In other news, Starbucks has responded to customer feedback by introducing two new drink sizes, the 128-ounce Plenta and the 2-ounce Micra. Starbucks CEO Howard Schultz, who has an extraneous letter “t” in his name and is therefore not related to Brad Schulz in any way, said that he is very excited about the introduction of…





Note: Go to my subsequent blog entry to see the source code for usp_DrawTree. It used to be right here, but it made my feed too large to show up in Google Reader.