最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

sql - Calculate running (cumulative) sum up to a certain value on multiple columns - Stack Overflow

programmeradmin3浏览0评论

I've been trying to build a running (cumulative) calculation up to a certain value, on two columns. The problem I'm facing:

  1. 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.

  2. Each column carries different premiums ('OT30': 30%, 'OT50': 50%), so I cannot combine them.

  3. 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:

  1. 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.

  2. Each column carries different premiums ('OT30': 30%, 'OT50': 50%), so I cannot combine them.

  3. 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:

  1. unpivot table to convert 'OT30' and 'OT50' columns to rows (ending up with the columns 'Metric' and 'Value')
  2. 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)
  3. 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)
  4. 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
Add a comment  | 

3 Answers 3

Reset to default 1

You are indeed on the right track.

  1. Unpivot OT

  2. calculate cumulative sum of OT

  3. 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 claimable

    c.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

  1. Calculate running total (OT30+OT50) -> tSum.
  2. 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

发布评论

评论列表(0)

  1. 暂无评论