recursive sql function with rollup logic? - sql

i have a SQL that using a recursive CTE to expand a self-referancing employees table builds a result set of defects aggregated by user and severity level.
here is my CTE:
ALTER FUNCTION [dbo].[fnGetEmployeeHierarchyByUsername]
(
#NTID varchar(100) = null
)
RETURNS TABLE
AS
RETURN
(
WITH yourcte AS
(
SELECT EmployeeId, ManagerNTID, ManagerID, NTID, FullName--, Name
FROM Employees
WHERE NTID = #NTID
UNION ALL
SELECT e.EmployeeId, e.ManagerNTID, e.ManagerID, e.NTID, e.FullName--, e.Name
FROM Employees e
JOIN yourcte y ON e.ManagerNTID = y.NTID
)
SELECT EmployeeId, ManagerID, NTID, FullName--, Name
FROM yourcte
)
here is my SQL for aggregating defects by the user:
SELECT e.FullName, Urgent, High, Medium, Low
FROM fnGetEmployeeHierarchyByUsername ('ssalvati') e
LEFT OUTER JOIN(
SELECT [AssignedTo],
SUM([1-Urgent]) AS Urgent,
SUM([2-High]) AS High,
SUM([3-Medium]) AS Medium,
SUM([4-Low]) AS Low
FROM (SELECT [AssignedTo],[BusinessSeverity] FROM Defects WHERE Status <> 'Closed') D
PIVOT (COUNT([BusinessSeverity]) FOR [BusinessSeverity] IN ([1-Urgent],[2-High],[3-Medium],[4-Low])) V
GROUP BY [AssignedTo]) AS def
ON e.ntid = def.[AssignedTo]
i want to have a porc that takes a username as a param and generates a result like the SQL above but with 2 enhancements:
i need it to list the user passed in as a param to be listed as the first record of the result-set.
i need the employees that report into the manager to show only one level deep and not show the full tree. the first level should be a roll up of all the underlying defects assigned to people who roll up into all the level one users. in other words i dont want to show a entire tree under the manager like it is now, i need it to show only one level deep but with a sum of defects for all the levels.
ideas?

This isn't tested as I don't have a mssql install here nor your data, but, I think it should be generally right and at least push you in a useful direction.
First, you need to change the query in your UDF to give two additional pieces of information. The "topmost" employee for your aggregation collapsing (which I think you said is the first direct report, not the very top employee), and the overall depth. As such:
WITH yourcte AS
(
SELECT EmployeeId, ManagerNTID, ManagerID, NTID, FullName, 0 as Depth, ntid as Topmost
FROM Employees
WHERE NTID = #NTID
UNION ALL
SELECT e.EmployeeId, e.ManagerNTID, e.ManagerID, e.NTID, e.FullName, y.Depth+1, case when y.depth = 0 then e.ntid else y.Topmost end
FROM Employees e
JOIN yourcte y ON e.ManagerNTID = y.NTID
)
SELECT EmployeeId, ManagerID, NTID, FullName, Depth, Topmost
FROM yourcte
Then, your actual query needs a few extra details to extract that information and use it
SELECT
e.FullName,
Urgent,
High,
Medium,
Low
FROM fnGetEmployeeHierarchyByUsername ('ssalvati') e
LEFT OUTER JOIN(
SELECT [AssignedTo],
SUM([1-Urgent]) AS Urgent,
SUM([2-High]) AS High,
SUM([3-Medium]) AS Medium,
SUM([4-Low]) AS Low
FROM (SELECT [AssignedTo],[BusinessSeverity] FROM Defects WHERE Status <> 'Closed') D
join fnGetEmployeeHierarchyByUsername ('ssalvati') e2 on d.AssignedTo = e2.ntid
PIVOT (COUNT([BusinessSeverity]) FOR [BusinessSeverity] IN ([1-Urgent],[2-High],[3-Medium],[4-Low])) V
where e2.TopMost = e.ntid
GROUP BY [AssignedTo]) AS def
ON e.ntid = def.[AssignedTo]
where e.Depth <= 1
The double call to your UDF might be a bit expensive, so you may want to consider putting this into a sproc and using a temp table to catch the results of the UDF to join against.
Also note that the UDF could take an extra parameter as to how deep "topmost" is, making this more general that it currently is in its hardcoded form.

If you modified your cte to include the depth i.e.
WITH yourcte AS
(
SELECT EmployeeId, ManagerNTID, ManagerID, NTID, FullName, 0 AS Depth
FROM Employees
WHERE NTID = #NTID
UNION ALL
SELECT e.EmployeeId, e.ManagerNTID, e.ManagerID, e.NTID, e.FullName, y.Depth + 1
FROM Employees e
JOIN yourcte y ON e.ManagerNTID = y.NTID
)
You can then order your output by depth (as the user in the input parameter should be at depth zero). Using this you should also be able to limit the depths you return and aggregate defects where depth >= 1
Edit
With the SQL I added above you basically want to rollup all defects to the user at Level 1? So, the NTID of the user at this level becomes the group by item for all records with depth >= 1. Another edit to the cte below adds the NTID as GroupingID which you can use to group by / rollup
WITH yourcte AS
(
SELECT EmployeeId, ManagerNTID, ManagerID, NTID
,FullName, 0 AS Depth, NTID as GroupingID
FROM Employees
WHERE NTID = #NTID
UNION ALL
SELECT e.EmployeeId, e.ManagerNTID, e.ManagerID, e.NTID
,e.FullName, y.Depth + 1, CASE
WHEN y.Depth + 1 = 1 THEN e.NTID
ELSE y.GroupingId
END
FROM Employees e
JOIN yourcte y ON e.ManagerNTID = y.NTID
)

here is the long dummy way of doing it. i have it working but the solution could be much better. i am hoping someone will post a SQL2005 way of getting this done...
alter PROC sel_DefectReportByManagerNTID_rollup
(#ManagerNTID NVARCHAR(100))
AS
CREATE TABLE #DefectCounts
(
id INT IDENTITY(1, 1) ,
MgrRolledInto NVARCHAR(100) NULL,
AltBusinessSeverity NVARCHAR(100) NULL,
DefectCount INT NULL
);
CREATE TABLE #directReports
(
pk INT IDENTITY(1, 1) ,
directReportNTID NVARCHAR(100) NULL
);
INSERT INTO #directReports
SELECT NTID FROM Employees WHERE ManagerNTID = #ManagerNTID
--select * from #directReports
DECLARE #maxPK INT;
SELECT #maxPK = MAX(PK) FROM #directReports
DECLARE #pk INT;
SET #pk = 1
INSERT INTO #DefectCounts (MgrRolledInto,AltBusinessSeverity,DefectCount)
SELECT #ManagerNTID, d.AltBusinessSeverity, COUNT(*)
FROM Defects d
JOIN StatusCode C ON C.CodeName = d.Status AND c.scid = 10
WHERE d.AssignedTo = #ManagerNTID
GROUP BY d.AltBusinessSeverity
WHILE #pk <= #maxPK
BEGIN
/* Get one direct report at a time to aggregate their defects under them... */
DECLARE #dirRptNTID NVARCHAR(100);
SET #dirRptNTID = (SELECT directReportNTID
FROM #directReports
WHERE PK = #pk)
INSERT INTO #DefectCounts (MgrRolledInto,AltBusinessSeverity,DefectCount)
SELECT #dirRptNTID, d.AltBusinessSeverity, COUNT(*)
FROM Defects d
JOIN StatusCode C ON C.CodeName = d.Status AND c.scid = 10
JOIN (SELECT * FROM fnGetEmployeeHierarchyByUsername(#dirRptNTID) ) emp ON emp.NTID = d.AssignedTo
WHERE d.AssignedTo IS NOT NULL
GROUP BY d.AltBusinessSeverity
SELECT #pk = #pk + 1
END
SELECT e.FullName,
isnull(Urgent,0) as Urgent,
isnull(High,0) as High,
isnull(Medium,0) as Medium,
isnull(Medium3000,0) as Medium3000,
isnull(Low,0) as Low
FROM ( select * from fnGetEmployeeHierarchyByUsername (#ManagerNTID) where depth <= 1) e
left outer join (
SELECT MgrRolledInto,
SUM([1-Urgent]) AS Urgent,
SUM([2-High]) AS High,
SUM([3-Medium]) AS Medium,
SUM([3-Medium (3000)]) AS Medium3000,
SUM([4-Low]) AS Low
FROM #DefectCounts dfs
PIVOT
(sum(DefectCount) FOR AltBusinessSeverity IN ([1-Urgent],[2-High],[3-Medium],[3-Medium (3000)],[4-Low])) V
GROUP BY MgrRolledInto
) def_data on def_data.MgrRolledInto = e.NTID
order by e.depth

Related

Find NON-duplicate column value combinations

This is for a migration script.
CompanyTable:
EmployeeId
DivisionId
abc
div1
def
div1
abc
div1
abc
div2
xyz
div2
In the below code I am Selecting duplicate EmployeeId-DivisionId combinations, that is, the records that have the same EmployeeId and DivisionId will be selected. So from the above table, the two rows that have abc-div1 combination will be selected by the below code.
How can I invert it? It seems so simple but I can't figure it out. I tried replacing with HAVING count(*) = 0 instead of > 1, I've tried fiddling with the equality signs in the ON and AND lines. Basically from the above table, I want to select the other three rows that don't have the abc-div1 combination. If there is a way to select all the unique EmployeeID-DivisionId combinations, let me know.
SELECT a.EmployeeID, a.DivisionId FROM CompanyTable a
JOIN ( SELECT EmployeeID, DivisionId
FROM CompanyTable
GROUP BY EmployeeID, DivisionId
HAVING count(*) > 1 ) b
ON a.EmployeeID = b.EmployeeID
AND a.DivisionId = b.DivisionId;
EmployeeId and DivisionId are both nvarchar(50) columns.
A windowed count would seem a suitable method:
select employeeid, divisionid
from (
select *, Count(*) over(partition by employeeid, divisionid) ct
from t
)t
where ct = 1;
As already mentioned, you must replace > 1 by its real opposite <= 1, this works: db<>fiddle
First, let's try rewriting your query using a common table expression (CTE), instead of a subquery:
WITH cteCompanyTableStats as (
SELECT
EmployeeID, DivisionId,
HasDuplicates = CASE WHEN count(*) > 1 THEN1 ELSE 0 END
FROM CompanyTable
GROUP BY EmployeeID, DivisionId
)
SELECT ct.*
FROM CompanyTable ct
inner join cteCompanyTableStats cts on
ct.EmployeeId = cts.EmployeeId
and ct.DivisionId = cts.DivisionId
and cts.HasDuplicates = 1
Notice how I've removed the HAVING clause & added a new HasDuplicates column? We're going to use that new column to find all of the table rows that -DON'T- have duplicates:
WITH cteCompanyTableStats as (
SELECT
EmployeeID, DivisionId,
HasDuplicates = CASE WHEN count(*) > 1 THEN1 ELSE 0 END
FROM CompanyTable
GROUP BY EmployeeID, DivisionId
)
SELECT ct.*
FROM CompanyTable ct
inner join cteCompanyTableStats cts on
ct.EmployeeId = cts.EmployeeId
and ct.DivisionId = cts.DivisionId
and cts.HasDuplicates = 0
The only character of SQL code that changed between the two queries was the last line, where and cts.HasDuplicates = ### is set.

Using Multiple CTE with one INSERT statement. [Error:More Columns than Specified in Column List]

I have CTE1 and CTE2 as below. The CTE2 shows error
CTE2 has more columns than specified in the column list
I would like to know what I am doing wrong. It cannot be because Insert statement has more columns than the CTE2 because CTE1 worked fine before. CTE1 and CTE2 are both using different tables. Is that the problem?
If I remove the parameters in CTE2(NewRoomCost,NewQuantity) Then I get the error
No Columns specified for Column3 of CTE2
Below is the code that I tried. Any help would be appreciated.
CREATE PROCEDURE dbo.SpTransactionGenerate
AS
BEGIN
SET NOCOUNT ON
DECLARE #MinReservationId INT = (SELECT MIN(f.ReservationId) FROM dbo.Reservation AS f)
DECLARE #MaxReservationId INT = (SELECT MAX(f.ReservationId) FROM dbo.Reservation AS f)
DECLARE #FirstSeasonEndDate DATE= '2018-02-13';
DECLARE #SecondSeasonEndDate DATE='2018-02-14';
DECLARE #ThirdSeasonEndDate DATE='2018-12-31';
WHILE #MinReservationId<=#MaxReservationId
BEGIN
WITH CTE1(ServiceId,ServiceRate,Quantity) AS
(
SELECT ServiceId,
ServiceRate,
ABS(CHECKSUM(NEWID())%3) + 1 AS Quantity
FROM dbo.[Service]
),
CTE2(NewRoomCost,NewQuantity) AS
(
SELECT
(SELECT roomRate.RoomCost FROM dbo.RoomRate as roomRate WHERE roomRate.RoomTypeId=
(SELECT room.RoomTypeId FROM dbo.Room as room
JOIN dbo.Reservation as res ON res.RoomId=room.RoomId WHERE res.ReservationId=#MinReservationId
AND roomRate.SeasonId=(
CASE WHEN (SELECT resv.CheckInDate FROM dbo.Reservation as resv WHERE resv.ReservationId=#MinReservationId)<=#FirstSeasonEndDate
THEN (SELECT sea.SeasonId FROM dbo.Season as sea WHERE sea.SeasonEndDate=#FirstSeasonEndDate)
WHEN (SELECT resv.CheckInDate FROM dbo.Reservation as resv WHERE resv.ReservationId=#MinReservationId)<=#SecondSeasonEndDate
THEN (SELECT sea.SeasonId FROM dbo.Season as sea WHERE sea.SeasonEndDate=#SecondSeasonEndDate)
ELSE (SELECT sea.SeasonId FROM dbo.Season as sea WHERE sea.SeasonEndDate=#ThirdSeasonEndDate) END
)
)) AS NewRoomCost,
DATEDIFF(DAY,(SELECT r.CheckinDate FROM dbo.Reservation AS r WHERE r.ReservationId=#MinReservationId), (SELECT r.CheckOutDate FROM dbo.Reservation AS r WHERE r.ReservationId=#MinReservationId)) AS NewQuantity,
)
INSERT INTO dbo.[Transaction]
(
ReservationId,
ServiceId,
Rate,
Quantity,
Amount
)
SELECT
#MinReservationId,
ServiceId,
ServiceRate,
Quantity,
ServiceRate*Quantity
FROM CTE1
UNION
SELECT
#MinReservationId,
NULL,
NewRoomCost,
NewQuantity,
NewRoomCost*NewQuantity
FROM CTE2
SELECT #MinReservationId=#MinReservationId+1
END
END
UPDATE : The error resulted because of a single extra comma in the CTE2. Sorry for the unnecessary question asked.
The issue in CTE2 is that you have an extra comma at the end of this line:
DATEDIFF(DAY,(SELECT r.CheckinDate FROM dbo.Reservation AS r WHERE r.ReservationId=#MinReservationId), (SELECT r.CheckOutDate FROM dbo.Reservation AS r WHERE r.ReservationId=#MinReservationId)) AS NewQuantity,
A sidenote: I suggest not writing explicit column names in the future but rather just naming them as you already did with the AS keyword. It just gives more flexibility overall.
Because your 2nd CTE defines two columns:
CTE2(NewRoomCost,NewQuantity) AS
But your select statement returns 3.
(SELECT roomRate.RoomCost FROM...
DATEDIFF(DAY,(SELECT r.CheckinDate...
(SELECT r.CheckOutDate FROM dbo.Reservation...

In SQL Server, how to create while loop

I Have the following :
I used the select to got the Id from table as follow :
Select Id from t
data will be like this:
id
DG1
FS2
DD4
I want to pass result the result to the following sql statement using while or case
Depend of the result of the select statement
SELECT f.age_days, f.Body_wt,f.Act_Fcr_Day,f.Act_Growth ,
f.growth_gm as Growth ,f2.growth_gm as Growth1,
COALESCE(
(
SELECT TOP 1 Body_wt
FROM [dbo].[Broiler_Farms_Data] mi
WHERE mi.Age_Days > f.Age_Days and mi.flock_id = ??????
ORDER BY
Age_Days
), 0) - f.Body_wt AS diff
FROM [dbo].[Broiler_Farms_Data] f
How can I do it .
thanks
I find this type of logic easier to follow with outer apply:
SELECT f.age_days, f.Body_wt, f.Act_Fcr_Day, f.Act_Growth,
f.growth_gm as Growth, f2.growth_gm as Growth1,
(COALESCE(mi.Body_wt, 0) - f.Body_wt0 AS diff
FROM [dbo].[Broiler_Farms_Data] f OUTER APPLY
(SELECT TOP 1 mi.*
FROM [dbo].[Broiler_Farms_Data] mi
WHERE mi.Age_Days > f.Age_Days and mi.flock_id = ??????
ORDER BY mi.Age_Days
) mi;
Then, if I understand correctly:
SELECT t.id, f.age_days, f.Body_wt, f.Act_Fcr_Day, f.Act_Growth,
f.growth_gm as Growth, f2.growth_gm as Growth1,
(COALESCE(mi.Body_wt, 0) - f.Body_wt0 AS diff
FROM [dbo].[Broiler_Farms_Data] f CROSS JOIN
(Select Id from t
) t OUTER APPLY
(SELECT TOP 1 mi.*
FROM [dbo].[Broiler_Farms_Data] mi
WHERE mi.Age_Days > f.Age_Days and mi.flock_id = t.id
ORDER BY mi.Age_Days
) mi;
This also includes the id in the SELECT, because that seems desirable.
Aggregate functions in VARCHAR columns are not advised. If you have a Primary Key or a surrogate key which can be used, then it will be helpful. Use the below query to use WHILE LOOP.
DECLARE #Id VARCHAR(10) = ''
WHILE 1=1
BEGIN
SELECT #Id = MIN(Id) from t WHERE Id > #Id
IF #Id IS NULL
BREAK
SELECT f.age_days, f.Body_wt,f.Act_Fcr_Day,f.Act_Growth ,
f.growth_gm as Growth ,f2.growth_gm as Growth1,
COALESCE(
(
SELECT TOP 1 Body_wt
FROM [dbo].[Broiler_Farms_Data] mi
WHERE mi.Age_Days > f.Age_Days and mi.flock_id = #Id
ORDER BY
Age_Days
), 0) - f.Body_wt AS diff
FROM [dbo].[Broiler_Farms_Data] f
END

SQL Server : CTE going a level back in the where clause

I would like to set a limit to a recursive query. But I don't want to lose the last entry on which i limited the recursion.
Right now the query looks like this:
WITH MyCTE (EmployeeID, FirstName, LastName, ManagerID, level, SPECIAL)
AS (
SELECT a.EmployeeID, a.FirstName, a.LastName, a.ManagerID, 0 as Level
FROM MyEmployees as a
WHERE ManagerID IS NULL
UNION ALL
SELECT b.EmployeeID, b.FirstName, b.LastName, b.ManagerID, level + 1
FROM MyEmployees as b
INNER JOIN MyCTE ON b.ManagerID = MyCTE.EmployeeID
WHERE b.ManagerID IS NOT NULL and b.LastName != 'Welcker'
)
SELECT *
FROM MyCTE
OPTION (MAXRECURSION 25)
And it gets limited by the Manager Welcker, the result looks like this:
EmployeeID; FirstName; LastName; ManagerID; level
1 Ken Sánchez NULL 0
What I want is to have the employee 'Welcker' included. The problem is I only have the name of the last person I need to have on my list. There are some entries below in the command structure but I don't know them and I don't want to see them.
Here's a example of how I imagine the result of my query to look.
EmployeeID FirstName LastName ManagerID level
1 Ken Sánchez NULL 0
273 Brian Welcker 1 1
Any help would be very much appreciated
WITH MyCTE (EmployeeID, FirstName, LastName, ManagerID, level, NotMatched)
AS (
SELECT a.EmployeeID, a.FirstName, a.LastName, a.ManagerID, 0 as Level, 0 AS NotMatched
FROM MyEmployees as a
WHERE ManagerID IS NULL
UNION ALL
SELECT b.EmployeeID, b.FirstName, b.LastName, b.ManagerID, level + 1,
CASE WHEN (MyCTE.LastName = 'Welcker' AND MyCTE.level = 1) THEN 1 ELSE MyCTE.NotMatched END
FROM MyEmployees as b
INNER JOIN MyCTE ON b.ManagerID = MyCTE.EmployeeID
)
SELECT *
FROM MyCTE
WHERE NotMatched != 1
Can you just change that one line to b.LastName = 'Welcker'?
select distinct
t.b_eid as empId,
t.b_name as name,
case t.b_mid when 0 then null else t.b_mid end as managerId
--convert(varchar,t.b_eid) + ', ' +
--t.b_name + ', ' +
--case t.b_mid when '0' then 'null' else convert(varchar,t.b_mid) end
--as bvals
from(
select
coalesce(a.mid, 0) as a_mid,
a.eid as a_eid,
a.name as a_name,
coalesce(b.mid, 0) as b_mid,
b.eid as b_eid,
b.name as b_name
from
(values(null,1,'adam'),(null,2,'barb'),(201,3,'chris')) as a(mid,eid,name) cross join
(values(null,1,'adam'),(null,2,'barb'),(201,3,'chris')) as b(mid,eid,name)
) as t
where (t.a_mid = t.b_eid or t.a_mid = 0 or t.b_mid = 0) and t.a_name != 'chris'

passing null to CTE to get all records as a default?

I have the CTE as a UDF and am trying to get it to take a default value of nothing in which case the result returned should be everything.
I want to call it as a default like this:
select * from fnGetEmployeeHierarchyByUsername
my UDF/ CTE is:
alter FUNCTION [dbo].[fnGetEmployeeHierarchyByUsername]
(
#AMRSNTID varchar(100) = null
)
RETURNS TABLE
AS
RETURN
(
WITH yourcte AS
(
SELECT EmployeeId, ManagerAMRSNTID, ManagerID, AMRSNTID, FullName, 0 as depth--, Name
FROM Employees
WHERE AMRSNTID = #AMRSNTID
UNION ALL
SELECT e.EmployeeId, e.ManagerAMRSNTID, e.ManagerID, e.AMRSNTID, e.FullName, y.depth+1 as depth--, e.Name
FROM Employees e
JOIN yourcte y ON e.ManagerAMRSNTID = y.AMRSNTID
)
SELECT EmployeeId, ManagerID, AMRSNTID, FullName, depth--, Name
FROM yourcte
)
How can I get it to work like this?
Try this in the UDF
WHERE AMRSNTID = ISNULL(#AMRSNTID, AMRSNTID)
And call it thus
select * from fnGetEmployeeHierarchyByUsername(DEFAULT)
--or
select * from fnGetEmployeeHierarchyByUsername(NULL)
Edit: without seeing your data or how the hierarchy looks, you need to add the an OPTION clause
If it runs forever, you have a circular link somewhere
select * from fnGetEmployeeHierarchyByUsername(DEFAULT) OPTION (MAXRECURSION 0)
Note: you can't have the OPTION inside the UDF
Edit2: my mistake sorry. You need to start at employees who have no manager (i.e. start at the top)
Something like
WHERE
(#AMRSNTID IS NOT NULL AND AMRSNTID = #AMRSNTID)
OR
(#AMRSNTID IS NULL AND ManagerAMRSNTID IS NULL)
alter FUNCTION [dbo].[fnGetEmployeeHierarchyByUsername]
(
#AMRSNTID varchar(100) = null
)
RETURNS TABLE
AS
RETURN
(
WITH yourcte AS
(
SELECT EmployeeId, ManagerAMRSNTID, ManagerID, AMRSNTID, FullName, 0 as depth--, Name
FROM Employees
WHERE (AMRSNTID = #AMRSNTID) or (#AMRSNTID is null and managerID is null)
UNION ALL
SELECT e.EmployeeId, e.ManagerAMRSNTID, e.ManagerID, e.AMRSNTID, e.FullName, y.depth+1 as depth--, e.Name
FROM Employees e
JOIN yourcte y ON e.ManagerAMRSNTID = y.AMRSNTID
)
SELECT EmployeeId, ManagerID, AMRSNTID, FullName, depth--, Name
FROM yourcte
)