December 6, 2005 at 2:46 pm
Table: tblEmployee
ID ParentID EmpName
1 NULL The BIG BOSS
2 1 Sam (VP)
3 1 Bill (CEO)
4 2 Steve (under VP)
5 3 James (under CEO)
6 5 Jack(under James)
7 5 Jim (under James)
I need to write a Stored Procedure (SP) with:
Input: @ID (tblEmployee.ID)
Output: All employees records under @ID
Sample Execution
Input: @ID = 3 (Bill)
Output: 3 Records; ID# 5 (James), 6 (Jack), 7 (Jim)
This SP is returning all the employees that is under that given @ID (as well as any other person under them, and so on). If the @ID = 1, this means that it'll return every record except ID#1 because everyone reports to "The BIG BOSS".
Is there a way to write this using a single SELECT statement? If not, what's the most efficient and fastest way of returning all the records under the Inputted @ID? Performance is key to this SP because it's being called about a dozen time per minute on average for this application.
Any input/suggestions is appreciated. Thanks.
December 6, 2005 at 2:52 pm
Which version of SQL Server ?
This would be an ideal candidate for a CTE under SQL 2005.
December 6, 2005 at 3:06 pm
CREATE ....
Declare @IDS TABLE (Id int)
INSERT INTO @IDS (ID)
SELECT @ID
WHILE EXISTS (select 1 from tblEmployee E
inner join @IDS I on E.ParentId = I.Id --E.ParentId in @IDS
left join @IDS I1 on E.Id = I.Id -- E.Id not in @IDS yet
where I1.Id IS NULL )
BEGIN
INSERT INTO @IDS (ID)
SELECT E.Id
FROM tblEmployee E
inner join @IDS I on E.ParentId = I.Id
WHERE NOT EXISTS (select 1 from @IDS I where I.Id = E.Id)
END
SELECT E.*
FROM tblEmployees E
INNER JOIN @IDS I ON E.ID = I.ID
WHERE I.ID <> @ID
----------
It's a lazy option, there is a way to make it better in terms of performance.
But it gives you a direction.
_____________
Code for TallyGenerator
December 6, 2005 at 3:46 pm
I'm using SQL2K.
December 6, 2005 at 4:01 pm
You're stuck with a completely flexible solution, as posted above, or alternatively you can trade off flexibility, remove the looping and place a hard cap on the number of levels you query for and just UNION ALL together a bunch of queries that pull various depths.
Select ID, EmpName -- get the employee (Level1)
From tblEmployee
Where ID = @ID
Union All -- and all immediately below (Level2)
Select L2.ID, L2.EmpName
From tblEmployee As L1
Inner Join tblEMployee As L2
On (L2.ParentId = L1.ID)
Where L1.ID = @ID
Union All -- and all 2 levels down (Level3)
Select L3.ID, L3.EmpName
From tblEmployee As L1
Inner Join tblEMployee As L2
On (L2.ParentId = L1.ID)
Inner Join tblEmployee As L3
On (L3.ParentId = L2.ID)
Where L1.ID = @ID
etc...
December 6, 2005 at 4:47 pm
Hopefully your "OrgChart" table is not being updated dozen times per minute.
So the best way to improve performance is to create separate table ReportsToBoss(BossId, ReporterId) and fill it up with results from the SP.
Than create trigger on tblEmployee to update table ReportsToBoss every time tblEmployee has been updated.
Then your select from ReportsToBoss will be really fast.
_____________
Code for TallyGenerator
December 7, 2005 at 5:48 am
I did this for a colleague the other day who had a similar problem with some help from BOL.
He had originally written a recursive function to draw out a tree menu system that was taken nearly 20 seconds to render on the website. Now it takes 1 second.
CREATE PROCEDURE [usp_asp_draw_tree]
@ID int
AS
SET nocount on
CREATE TABLE #cats (rID int identity(1,1),catid int, menu varchar(125))
DECLARE @level int
DECLARE @current char(20)
DECLARE @sql varchar(2000)
DECLARE @sql2 varchar(2000)
DECLARE @lvl int, @line char(100), @disp varchar(100)
set @current = @ID
CREATE TABLE #stack (item int, sName varchar(100), nlevel int)
INSERT INTO #stack
VALUES (@current,'', 1)
SELECT @level = 1
WHILE @level > 0
BEGIN
if EXISTS (SELECT * FROM #stack WHERE nlevel = @level)
BEGIN
SELECT
@current = item, @disp = sName
FROM
#stack
WHERE
nlevel = @level
--optional to display menu level
SELECT @line = replicate('~',@level - 1) + @disp
INSERT INTO #cats
VALUES (@current,@line)
DELETE FROM #stack
WHERE nlevel = @level AND item = @current
INSERT INTO #stack
(item, sName, nLevel)
SELECT
ID, EmpName, @level + 1
FROM
tbl_TEST
WHERE
ParentID = @current
if @@rowcount > 0
SELECT @level = @level + 1
END
else
SELECT @level = @level - 1
END
drop table #stack
select rID, * from tbl_TEST as ci join #cats as c on ci.ID = c.catid order by rID
drop table #cats
set nocount off
GO
exec usp_asp_draw_tree 1 --for everything or else pass in the ID that you wish to retrieve everything beneath.
I had to write it for 7.5 so that why I used a #table not @table
but it was deployed on 2005 and I had to add in the rID identity col into the table as for some reason 2005 was sorting it differently to 7.5.
December 7, 2005 at 6:58 am
A minor improvement on Sergiy's method... I don't know if it's faster than PW's algorithm, but the number of SQL statements seems to be the same (if you count each statement in the union all select as one SQL statement). It could be interesting to compare performance...
set nocount on
go
create table tblEmployee
(
Id int,
ParentId int NULL
)
go
insert into tblEmployee
select 1, NULL union all
select 2, 1 union all
select 3, 1 union all
select 4, 2 union all
select 5, 3 union all
select 6, 5 union all
select 7, 5
go
Declare @IDS TABLE (Id int primary key)
declare @ID int
select @ID = 1
INSERT INTO @IDS (ID) SELECT @ID
WHILE @@ROWCOUNT > 0
BEGIN
INSERT INTO @IDS (ID)
SELECT E.Id
FROM
@IDS I
inner join tblEmployee E
on E.ParentId = I.Id
left join @IDS I1
on I1.Id = E.Id
where I1.Id IS NULL
END
SELECT E.*
FROM tblEmployee E
INNER JOIN @IDS I ON E.ID = I.ID
WHERE I.ID <> @ID
drop table tblEmployee
go
December 13, 2005 at 6:45 pm
I've tested the performance using each of the 4 suggestions and these are the best in order of performance:
1. Jesper (extremely fast for all test cases)
2. PW (fastest when it's only a few levels deep)
3. Sergiy
4. Rob Reid
Jesper's tweaking made it on par with PW's method. In fact, the majority of the time, it outperforms PW's method by about 5%.
However, PW's method is faster than Jesper's method when it's only a few levels deep. When it hits the 6+ levels deep into the tree-structure, Jesper's seems to be consistently faster by about 5%. I don't know why, but that's what I'm getting.
But, Jesper's and PW's method are about as optimal as it can get from what I can tell. Given that they perform just about the same (5% is not very noticeable), I like Jesper's method since it's so much easier to maintain and read. It also doesn't have the level limitation either.
Thanks everyone for your help!
December 14, 2005 at 1:53 am
Thanks for posting the results of your test....
December 14, 2005 at 5:46 am
Old hand posted what he described a a lazy option, I changed the code slightly to use a count of the levels available, therefore limiting the loops as follows
declare @idxcount as int
select @idxcount = count(distinct parentid) from tblEmployee
declare @idxPos as int
set @IdxPos = 1
declare @ID int
Declare @IDS TABLE (Id int)
set @ID = 3
INSERT INTO @IDS (ID)
SELECT @ID
WHILE ( @IdxPos <= @IdxCount)
BEGIN
Set @IdxPos = @IdxPos+1
INSERT INTO @IDS (ID)
SELECT E.Id
FROM tblEmployee E
inner join @IDS I on E.ParentId = I.Id
WHERE NOT EXISTS (select 1 from @IDS I where I.Id = E.Id)
END
SELECT E.*
FROM tblEmployee E
INNER JOIN @IDS I ON E.ID = I.ID
WHERE I.ID <> @ID
And it worked well, Just out of interest is this not a viable option as a flexible solution?
It is a procedure that I was going to use in a similar situation.
December 14, 2005 at 7:18 am
I don't think this will perform very well. Considering the data below. Your loop is iterated 10 times, and Sergiy's loop (and my loop) only 3 times.
set nocount on
go
create table tblEmployee
(
Id int,
ParentId int NULL
)
go
insert into tblEmployee
select 1, NULL union all
select 2, 1 union all
select 3, 1 union all
select 4, 1 union all
select 5, 1 union all
select 6, 1 union all
select 7, 1 union all
select 8, 1 union all
select 9, 1 union all
select 10, 1 union all
select 11, 1 union all
select 12, 2 union all
select 13, 3 union all
select 14, 4 union all
select 15, 5 union all
select 16, 6 union all
select 17, 7 union all
select 18, 8 union all
select 19, 9 union all
select 20, 10
go
declare @idxcount as int
select @idxcount = count(distinct parentid) from tblEmployee
declare @idxPos as int
set @IdxPos = 1
declare @ID int
Declare @IDS TABLE (Id int)
set @ID = 1
INSERT INTO @IDS (ID)
SELECT @ID
WHILE ( @IdxPos <= @IdxCount)
BEGIN
Set @IdxPos = @IdxPos+1
INSERT INTO @IDS (ID)
SELECT E.Id
FROM tblEmployee E
inner join @IDS I on E.ParentId = I.Id
WHERE NOT EXISTS (select 1 from @IDS I where I.Id = E.Id)
END
SELECT E.*
FROM tblEmployee E
INNER JOIN @IDS I ON E.ID = I.ID
WHERE I.ID <> @ID
drop table tblEmployee
go
December 14, 2005 at 7:36 am
My change looped just 4 times not 10 though as I used count distinct on parentid. but I do see your point.
Thanks
Viewing 13 posts - 1 through 12 (of 12 total)
You must be logged in to reply to this topic. Login to reply