I've been trying to build a running (cumulative) calculation up to a certain value, on two columns. The problem I'm facing:
Calculate overtime hours for employees ('EmpID') for a specific pay period ('Grouping'), spread over 5 weeks ('YearWeek'). The employees can claim a maximum of 20 hours of cumulative overtime.
Each column carries different premiums ('OT30': 30%, 'OT50': 50%), so I cannot combine them.
I need to obtain a total of maximum 20 hours in new columns 'OT30_NT' and 'OT50_NT'.
Input data below:
EmpID | Grouping | YearWeek | OT30 | OT50 | Comments |
---|---|---|---|---|---|
248 | G15 | 202449 | 3 | 0 | |
248 | G15 | 202450 | 8 | 10 | max of 20 hours achieved here |
248 | G15 | 202451 | 8 | 24 | |
248 | G15 | 202452 | 8 | 2 | |
248 | G15 | 202501 | 1 | 0 |
I've been trying to build a running (cumulative) calculation up to a certain value, on two columns. The problem I'm facing:
Calculate overtime hours for employees ('EmpID') for a specific pay period ('Grouping'), spread over 5 weeks ('YearWeek'). The employees can claim a maximum of 20 hours of cumulative overtime.
Each column carries different premiums ('OT30': 30%, 'OT50': 50%), so I cannot combine them.
I need to obtain a total of maximum 20 hours in new columns 'OT30_NT' and 'OT50_NT'.
Input data below:
EmpID | Grouping | YearWeek | OT30 | OT50 | Comments |
---|---|---|---|---|---|
248 | G15 | 202449 | 3 | 0 | |
248 | G15 | 202450 | 8 | 10 | max of 20 hours achieved here |
248 | G15 | 202451 | 8 | 24 | |
248 | G15 | 202452 | 8 | 2 | |
248 | G15 | 202501 | 1 | 0 |
Desired Output: - Max 20 hours condition achieved
EmpID | Grouping | OT30_NT | OT50_NT | Comments |
---|---|---|---|---|
248 | G15 | 3 | 0 | |
248 | G15 | 8 | 9 | 3+0+8+9 =20 - max 20 hours condition achieved |
248 | G15 | 0 | 0 | (all other values should be zero) |
248 | G15 | 0 | 0 | |
248 | G15 | 0 | 0 |
My calculation attempt below:
- unpivot table to convert 'OT30' and 'OT50' columns to rows (ending up with the columns 'Metric' and 'Value')
- add a RowNumber column to get a proper cumulative sum (ending up with the 'RowNo' column, to be used in the Partition By / Order By clause)
- get a cumulative sum by using the 'Partition By' clause (ending up with the 'OTCum' column, to determine where the max value of 20 hours is achieved)
- I have tried various CASE clauses in the 'CalcAttempt' column, but I cannot figure out how to achieve the maximum limit of 20 hours.
SQL code here:
SELECT
m.[EmployeeId]
,m.[Grouping]
,m.[GroupingForOrder]
,m.[Metric]
,m.RowNo
,m.Value
,SUM([Value]) OVER (PARTITION BY [EmployeeID], [Grouping] ORDER BY RowNo) AS 'OTCum'
,CASE
WHEN SUM([Value]) OVER (PARTITION BY [EmployeeID], [Grouping] ORDER BY RowNo)<=20 THEN Value
ELSE 20-SUM([Value]) OVER (PARTITION BY [EmployeeID], [Grouping] ORDER BY RowNo) END AS CalcAttempt
FROM
-- Second Query to add Row Numbers to get the correct Cumulative Value
(
SELECT
[EmployeeId]
,[Grouping]
,[GroupingForOrder]
,[Metric]
,[Value]
,ROW_NUMBER() OVER (PARTITION BY EMployeeID, Grouping ORDER BY GroupingForOrder) AS 'RowNo'
FROM
(
-- Initial query to pivot OT30 and OT50 Values
SELECT [EmployeeId]
,[Grouping]
,[GroupingForOrder]
,[OT30]
,[OT50]
FROM [lookups].[Timekeeping_WeeklyBreakdown]
) AS SourceTable
UNPIVOT
(Value for Metric IN (
[OT30]
,[OT50]
)) AS ALIAS
)AS m
EmpID | Grouping | Metric | RowNo | Value | OTCum | CalcAttempt | Comments |
---|---|---|---|---|---|---|---|
248 | G15 | OT30 | 1 | 3 | 3 | 3 | |
248 | G15 | OT50 | 2 | 0 | 3 | 0 | |
248 | G15 | OT30 | 3 | 8 | 11 | 8 | |
248 | G15 | OT50 | 4 | 10 | 21 | -1 | -1 should be 9 (3+0+8+9=20) |
248 | G15 | OT30 | 5 | 8 | 29 | -9 | should be 0 (cumulative value above 20 hours) |
248 | G15 | OT50 | 6 | 24 | 53 | -33 | should be 0 |
248 | G15 | OT30 | 7 | 8 | 61 | -41 | should be 0 |
248 | G15 | OT50 | 8 | 2 | 63 | -43 | should be 0 |
248 | G15 | OT30 | 9 | 0 | 63 | -43 | should be 0 |
248 | G15 | OT50 | 10 | 0 | 63 | -43 | should be 0 |
If I can get the 'CalcAttempt' column to work as per the above comments, I can then pivot by 'Metric' and finalize calculations. Of course, assuming there's another solution that doesn't require me to do the above.
Using SQL Azure 12.0.2000.8.
Any suggestions would be highly appreciated.
Share Improve this question edited Feb 16 at 6:28 Dale K 27.3k15 gold badges57 silver badges83 bronze badges asked Feb 16 at 4:42 Dan VDMDan VDM 11 silver badge1 bronze badge New contributor Dan VDM is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct. 1- Thank you, hkt, serg and valnik. Interesting solutions. I've tested all of them, and they worked perfectly. I will upload an entire dataset and do some performance testing to decide which one to use. Thank you very much for your help. – Dan VDM Commented Feb 16 at 19:01
3 Answers
Reset to default 1You are indeed on the right track.
Unpivot OT
calculate cumulative sum of OT
case expression to determine the claimable OT value.
The logic :-
c.CumulativeOT <= @max_hours
: max_hours is greater than Cumulative OT full OT amount is claimablec.CumulativeOT - c.OT <= @max_hours
: when reach this condition, Cumulative is > @max_hours, so it need to check for previous row Cumulative value is <= @max_hours. (c.CumulativeOT - c.OT
is previous row Cumulative value.) and calculate the balance of @max_hours available =@max_hours - c.CumulativeOT + c.OT
declare @max_hours int = 20; -- Max 20 hours with cte as ( select EmpID, [Grouping], [YearWeek], Seq, OT30, OT50, OT, -- Calculate cumulative sum of OT CumulativeOT = sum(OT) over (partition by EmpID, [Grouping] order by [YearWeek], Seq) from YourTable t cross apply -- Unpivot OT30 & OT50 ( values (1, OT30), (2, OT50) ) o (seq, OT) ), cte2 as ( select *, ClaimableOT = case when c.CumulativeOT <= @max_hours then c.OT when c.CumulativeOT - c.OT <= @max_hours then @max_hours - c.CumulativeOT + c.OT else 0 end from cte c ) -- Pivot back the OT column select EmpID, [Grouping], YearWeek, OT30, OT50, -- original OT value ClaimableOT30 = SUM(case when seq = 1 then ClaimableOT else 0 end), ClaimableOT50 = SUM(case when seq = 2 then ClaimableOT else 0 end) from cte2 group by EmpID, [Grouping], YearWeek, OT30, OT50 order by EmpID, [Grouping], YearWeek;
Provided only two OT-kind columns are in the table you can skip unpivot/pivot steps at a price of a bit more complex CASEs.
with cum as (
select [EmpId]
,[Grouping]
,[YearWeek]
,[OT30]
,[OT50]
,coalesce(SUM([OT30]+[OT50]) OVER (PARTITION BY [EmpID], [Grouping] ORDER BY [YearWeek] rows between unbounded preceding and 1 preceding),0) AS [OTCum-1]
from tbl
)
select [EmpId]
,[Grouping]
--,[YearWeek]
,case when [OTCum-1] >= @max_hours then 0
when [OTCum-1] + [OT30] >= @max_hours then @max_hours - [OTCum-1]
else [OT30] end [OT30_NT]
,case when [OTCum-1] + [OT30] >= @max_hours then 0
when [OTCum-1] + [OT30] + [OT50] >= @max_hours then @max_hours - ([OTCum-1] + [OT30])
else [OT50] end [OT50_NT]
from cum
order by [EmpId], [Grouping], [YearWeek]
Sql Sever fiddle
- Calculate running total (OT30+OT50) ->
tSum
. - On row where tSum>20 if (tSum-OT30-OT50)<20 calculate new OT30_NT and OT50_NT.
Other rows where tSum>20 - set OT30_NT and OT50_NT to 0.
First change OT50. If this is not enough, change OT30.
For OT30
,case when tSum<@maxOverTimeHours then OT30 -- nothing to do
when tSum>=@maxOverTimeHours and (tSum-OT30-OT50)<@maxOverTimeHours then
-- reduce OT50 and save OT30
case when (tSum-OT50)<@maxOverTimeHours then OT30
-- reduce OT50 and OT30
else OT30-((tSum-OT50)-@maxOverTimeHours)
end
else 0
end OT30_NT
For OT50
,case when tSum<@maxOverTimeHours then OT50
when tSum>=@maxOverTimeHours and (tSum-OT30-OT50)<@maxOverTimeHours then
--reduce OT50 and save OT30
case when (tSum-OT50)<@maxOverTimeHours then OT50-(tSum-@maxOverTimeHours)
-- reduce OT50 to 0 and reduce OT30
else 0
end
else 0
end OT50_NT
See example
Test data for several cases (Grouping
):
EmpID | Grouping | YearWeek | OT30 | OT50 | Comments |
---|---|---|---|---|---|
248 | G15 | 202449 | 3 | 0 | null |
248 | G15 | 202450 | 8 | 10 | OT50 10->9 |
248 | G15 | 202451 | 8 | 24 | null |
248 | G15 | 202452 | 8 | 2 | null |
248 | G15 | 202501 | 1 | 0 | null |
248 | G16 | 202449 | 5 | 8 | null |
248 | G16 | 202450 | 8 | 10 | OT30 8->7 OT50 10->0 |
248 | G16 | 202451 | 8 | 24 | null |
248 | G16 | 202452 | 8 | 2 | null |
248 | G16 | 202501 | 1 | 0 | null |
248 | G17 | 202449 | 12 | 11 | OT50 12->8 |
248 | G18 | 202449 | 21 | 11 | OT30 21->20 OT50 11->0 |
248 | G19 | 202449 | 1 | 22 | OT50 22->19 |
248 | G20 | 202449 | 23 | 22 | OT30 23->20 OT50 22->0 |
declare @maxOverTimeHours int = 20;
select *
,sum(OT30_NT+OT50_NT)over(partition by EmpID,Grouping order by YearWeek) SumOT_NT
from( -- correct OT50 and/or OT30
select *
,case when tSum<@maxOverTimeHours then OT30
when tSum>=@maxOverTimeHours and (tSum-OT30-OT50)<@maxOverTimeHours then
case when (tSum-OT50)<@maxOverTimeHours then OT30
else OT30-((tSum-OT50)-@maxOverTimeHours)
end
else 0
end OT30_NT
,case when tSum<@maxOverTimeHours then OT50
when tSum>=@maxOverTimeHours and (tSum-OT30-OT50)<@maxOverTimeHours then
case when (tSum-OT50)<@maxOverTimeHours then OT50-(tSum-@maxOverTimeHours)
else 0
end
else 0
end OT50_NT
from( -- running total
select *
,sum(OT30+OT50)over(partition by EmpID,Grouping order by YearWeek) tSum
from Timekeeping_WeeklyBreakdown
)a
)b
EmpID | Grouping | YearWeek | OT30 | OT50 | Comments | tSum | OT30_NT | OT50_NT | SumOT_NT |
---|---|---|---|---|---|---|---|---|---|
248 | G15 | 202449 | 3 | 0 | null | 3 | 3 | 0 | 3 |
248 | G15 | 202450 | 8 | 10 | OT50 10->9 | 21 | 8 | 9 | 20 |
248 | G15 | 202451 | 8 | 24 | null | 53 | 0 | 0 | 20 |
248 | G15 | 202452 | 8 | 2 | null | 63 | 0 | 0 | 20 |
248 | G15 | 202501 | 1 | 0 | null | 64 | 0 | 0 | 20 |
248 | G16 | 202449 | 5 | 8 | null | 13 | 5 | 8 | 13 |
248 | G16 | 202450 | 8 | 10 | OT30 8->7 OT50 10->0 | 31 | 7 | 0 | 20 |
248 | G16 | 202451 | 8 | 24 | null | 63 | 0 | 0 | 20 |
248 | G16 | 202452 | 8 | 2 | null | 73 | 0 | 0 | 20 |
248 | G16 | 202501 | 1 | 0 | null | 74 | 0 | 0 | 20 |
248 | G17 | 202449 | 12 | 11 | OT50 12->8 | 23 | 12 | 8 | 20 |
248 | G18 | 202449 | 21 | 11 | OT30 21->20 OT50 11->0 | 32 | 20 | 0 | 20 |
248 | G19 | 202449 | 1 | 22 | OT50 22->19 | 23 | 1 | 19 | 20 |
248 | G20 | 202449 | 23 | 22 | OT30 23->20 OT50 22->0 | 45 | 20 | 0 | 20 |
fiddle
More short query
declare @maxOverTimeHours int = 20;
select *
,case when rsum>(OT30+OT50) then 0
when rSum>0 then
case when rSum<=OT50 then OT30
when rSum>OT50 then OT30-(rSum-OT50)
else 0
end
else OT30
end OT30_NT
,case when rsum>(OT30+OT50) then 0
when rSum>0 then
case when rSum>=OT50 then 0
else OT50-rSum
end
else OT50
end OT50_NT
from(
select *
,sum(OT30+OT50)over(partition by EmpID,Grouping order by YearWeek)-@maxOverTimeHours rSum
from Timekeeping_WeeklyBreakdown
)a
fiddle