sqlsql-serversql-server-2019

Recursive CTE for leave accrual projections


I have two tables

  1. Leave_policy:
Leave_Type_Id Leave_Rate_Per_Every_Accural Months Until Annual_Max_CarryOver
LT1 3 0 72
LT1 5 24 120
LT2 4 0 96
LT2 6 24 144
  1. Empl_Leave:
EMP_ID LEAVE_BALANCE Leave_Type_ID Hire_date Last_Accrual_Date
701 68 LT2 10/1/2002 7/27/2024
702 85 LT1 11/12/1996 7/27/2024
703 230 LT3 9/8/2007 7/27/2024
704 129 LT1 3/4/2012 7/27/2024
705 187.5 LT2 6/1/2008 7/27/2024

My goal is get employee leave projections (Current leave balance + LeaveRate * Number of Accruals) for the next six months from last accrual date and also leave projections to employee hire anniversary. Something like this:

Empl_ID Current_Balance For next six Month balance For Until Employee Hire Anniversary
701 68 (68+Running total until 6 months) (68 + Running total until Hire anniversary date)
702 85 (85+RunningTotal until 6 months) (85+Running total until Hire anniversary date)

Employee have bi-weekly leave accruals as only two per month, Months_until and LeaveType_Id columns define how much Leave_Rate_Per_Every_Accrual will be as in the first table. Months_Until is a tenure period MONTH diff between Leave Accrual Date and Hire_Date. In between these calculations I have to follow Annual_Max_Carryover if the date crosses next year. I am on SQL 2019.

I tried with a recursive CTE.


Solution

  • See example:

    I will create Accrual calendar. This calendar table can be created separately as a temporary or permanent table, with corresponding indexes. Especially if such calculations are regular. This will simplify the query and improve performance.

    Test data. Added LT3 and users 905,906 for test Leave_Rate_Per_Every_Accural,From_Months and Annual_Max_CarryOver changes.

    create table Leave_policy(Leave_Type_Id varchar(3), Leave_Rate_Per_Every_Accural int
         ,From_Months int,  Annual_Max_CarryOver int);
    insert into Leave_policy values
     ('LT1',    3,  0,  72)
    ,('LT1',    5,  24, 120)
    ,('LT2',    4,  0,  96)
    ,('LT2',    6,  24, 144)
    ,('LT3',    1,  0,  96)
    ,('LT3',    2,  24, 144)
    ,('LT3',    3,  28, 144)
    ,('LT3',    4,  32, 144)
    ,('LT3',    5,  36, 144)
    ;
    create table Empl_Leave(id int identity,EMP_ID int, LEAVE_BALANCE float, Leave_Type_ID varchar(3)
         ,Hire_date date,   Last_Accrual_Date date);
    insert into Empl_leave(emp_id,leave_balance,Leave_type_id,Hire_date,Last_Accrual_date) values
     (701,  68,     'LT2','10/1/2002','7/27/2024')
    ,(905,  110,    'LT2','11/2/2022','7/27/2024')
    ,(906,  110,    'LT3','05/3/2022','7/27/2024')
    ;
    
    Leave_Type_Id Leave_Rate_Per_Every_Accural From_Months Annual_Max_CarryOver
    LT1 3 0 72
    LT1 5 24 120
    LT2 4 0 96
    LT2 6 24 144
    LT3 1 0 96
    LT3 2 24 144
    LT3 3 28 144
    LT3 4 32 144
    LT3 5 36 144
    id EMP_ID LEAVE_BALANCE Leave_Type_ID Hire_date Last_Accrual_Date
    1 701 68 LT2 2002-10-01 2024-07-27
    2 905 110 LT2 2022-11-02 2024-07-27
    3 906 110 LT3 2022-05-03 2024-07-27

    Query

    -- version for SQL Server 2019
    -- generate series replaced by recursive series generator
    -- and greates(...) by case when ...
    with params as( -- query parameters
      select startDt  -- calculation start date
        ,datefromparts(year(startDt)+1,1,1) nyDt  -- next new year
        ,dateadd(month,6,startDt) projDt  -- projection date 6 month after startDt
      from (select max(Last_accrual_date) startDt from Empl_Leave) sDt
    )
    ,series as (
          select dateadd(week,2,startDt) Nad -- Next_accrual_date
              ,dateadd(week,26*2,startDt) endDt
          from params
          union all
          select dateadd(week,2,r.Nad) Nad, endDt
          from series r
          where r.Nad<=endDt
    )
    ,AccrualCalendar as( -- Accrual calendar to year
    select Nad  -- Next accrual dates in calendar
    from(
      select *,row_number()over(partition by year(Nad),month(Nad) order by Nad)rn
      from series 
        -- (
        --   select dateadd(week,n*2,startDt) Nad -- Next_accrual_date
        --   from  (select value n from generate_series(1,26)) wn
        --  cross apply params
        --  )a
      )b
      where rn<=2 -- take only 2 date for every months
    )
    ,lp as( -- Leave_policy with To_Months
      select *,lead(From_months,1,999999)
                 over(partition by Leave_Type_Id order by From_months)-1 To_Months
      from Leave_policy
    )
    

    main part

    ,empDates as( -- employee params and dates, including next hire Anniversary
    select *
      ,case when nyDt>projDt then
        case when nyDt>haDt then 
          case when haDt>projDt then haDt else projDt end
        else haDt end
       else 
        case when projDt>haDt then projDt else haDt end 
       end lastDt
    from(
    select id,EMP_ID,LEAVE_BALANCE lb,  Leave_Type_ID lt, Hire_date hdt
      , startDt,nyDt,projDt
      ,case when datefromparts(year(startDt),month(Hire_date),day(Hire_date))<startDt
           then datefromparts(year(startDt)+1,month(Hire_date),day(Hire_date))
       else datefromparts(year(startDt),month(Hire_date),day(Hire_date))
       end haDt -- next hire Anniversary
    from Empl_Leave el
    cross apply params
      )t
    )
    ,allEmpDates as( -- all dates for employee -- accrual,next hire Anniversary
      -- new year,projection
     -- Accruals
    select ed.*,'ac' as sk,Nad as pointDt,datediff(month,hDt,Nad) pointM
    from empDates ed
    left join AccrualCalendar ac on Nad between startDt and lastDt -- greatest(haDt,nyDt,projDt)
      -- Control points
    union all
    select * 
      ,case when sk='ny'then nyDt 
              when sk='proj'then projDt
              when sk='ha'then haDt 
         end pointDt
      ,datediff(month,hDt,case when sk='ny'then nyDt 
              when sk='proj'then projDt
              when sk='ha'then haDt 
         end) pointM
    from  empDates
    left join (values('ha'),('ny'),('proj'))t(sk)
         on sk in('ha','proj')
             or (sk='ny' and projDt>=nyDt)
      )
    ,balance1 as(-- rolling sum - balance for every date
    select ad.* 
      ,sum(case when sk='ac' then Leave_Rate_Per_Every_Accural else 0 end)
        over(partition by emp_id order by pointDt)
      +lb currentB
      ,case when sk='ac' then Leave_Rate_Per_Every_Accural else 0 end leaveQty
      ,From_Months,To_Months,Annual_Max_CarryOver maxCarry
    from allEmpDates  ad
    left join lp on lp.Leave_Type_id=ad.lt
      and lp.From_Months<=pointM and lp.To_Months>=pointM
    )
    ,AccrualsNYover as( -- correction for max carry - Annual_Max_CarryOver
    select  *
      ,case when sk='ny' and currentB>maxCarry then maxCarry-currentB
       else 0 end newyearCorrection
    from balance1
    )
    ,balance2 as( -- rolling balance with correction (max carry over)
    select *
    from(
    select * 
      ,sum(case when sk='ac' then leaveQty else 0 end
           +case when sk='ny' then newyearCorrection else 0 end)
        over(partition by emp_id order by pointDt) 
      +lb as CurrentBalance
    from AccrualsNYover
    )t 
     where sk in('ny','ha','proj') -- after balance calculation take only Control points
    )
    select EMP_ID,min(lt) Leave_type_id,min(hdt)HireDate
      ,min(startDt) Last_Accrual_date,min(lb) Leave_balance
      ,max(case when sk='proj' then CurrentBalance end) projBalance
      ,max(case when sk='ha' then CurrentBalance end) Hire_Anniversary_Balance
      ,max(case when sk='ny' then CurrentBalance end) CarryOver_Balance
     ,max(nyDt)NewYearDt,max(projDt)ProjectionDate,max(haDt)Hire_Anniversary_date
    from balance2
    group by emp_id
    order by emp_id 
    
    EMP_ID Leave_type_id HireDate Last_Accrual_date Leave_balance projBalance Hire_Anniversary_Balance CarryOver_Balance NewYearDt ProjectionDate Hire_Anniversary_date
    701 LT2 2002-10-01 2024-07-27 68 140 92 128 2025-01-01 2025-01-27 2024-10-01
    905 LT2 2022-11-02 2024-07-27 110 156 140 144 2025-01-01 2025-01-27 2024-11-02
    906 LT3 2022-05-03 2024-07-27 110 146 175 138 2025-01-01 2025-01-27 2025-05-03

    Query for 2022

    -- version for SQL Server 2022
    with params as( -- query parameters
      select startDt  -- calculation start date
        ,datefromparts(year(startDt)+1,1,1) nyDt  -- next new year
        ,dateadd(month,6,startDt) projDt  -- projection date 6 month after startDt
      from (select max(Last_accrual_date) startDt from Empl_Leave) sDt
    )
    ,AccrualCalendar as( -- Accrual calendar to year
    select Nad  -- Next accrual dates in calendar
    from(
      select *,row_number()over(partition by year(Nad),month(Nad) order by Nad)rn
      from(
          select dateadd(week,n*2,startDt) Nad -- Next_accrual_date
          from  (select value n from generate_series(1,26)) wn
          cross apply params
         )a
      )b
      where rn<=2 -- take only 2 date for every months
    )
    ,lp as( -- Leave_policy with To_Months
      select *,lead(From_months,1,999999)
                 over(partition by Leave_Type_Id order by From_months)-1 To_Months
      from Leave_policy
    )
    ,empDates as( -- employee params and dates, including next hire Anniversary
    select id,EMP_ID,LEAVE_BALANCE lb,  Leave_Type_ID lt, Hire_date hdt
      , startDt,nyDt,projDt
      ,case when datefromparts(year(startDt),month(Hire_date),day(Hire_date))<startDt
           then datefromparts(year(startDt)+1,month(Hire_date),day(Hire_date))
       else datefromparts(year(startDt),month(Hire_date),day(Hire_date))
       end haDt -- next hire Anniversary
    from Empl_Leave el
    cross apply params
      )
    ,allEmpDates as( -- all dates for employee -- accrual,next hire Anniversary
      -- new year,projection
    select ed.*,'ac' as sk,Nad as pointDt,datediff(month,hDt,Nad) pointM
    from empDates ed
    left join AccrualCalendar ac on Nad between startDt and greatest(haDt,nyDt,projDt)
    union all
    select * 
      ,case when sk='ny'then nyDt 
              when sk='proj'then projDt
              when sk='ha'then haDt 
         end pointDt
      ,datediff(month,hDt,case when sk='ny'then nyDt 
              when sk='proj'then projDt
              when sk='ha'then haDt 
         end) pointM
    from  empDates
    left join (values('ha'),('ny'),('proj'))t(sk)
         on sk in('ha','proj')
             or (sk='ny' and projDt>=nyDt)
      )
    ,balance1 as(-- rolling sum - balance for every date
    select ad.* 
      ,sum(case when sk='ac' then Leave_Rate_Per_Every_Accural else 0 end)
        over(partition by emp_id order by pointDt)
      +lb currentB
      ,case when sk='ac' then Leave_Rate_Per_Every_Accural else 0 end leaveQty
      ,From_Months,To_Months,Annual_Max_CarryOver maxCarry
    from allEmpDates  ad
    left join lp on lp.Leave_Type_id=ad.lt
      and lp.From_Months<=pointM and lp.To_Months>=pointM
    )
    ,AccrualsNYover as( -- correction for max carry - Annual_Max_CarryOver
    select  *
      ,case when sk='ny' and currentB>maxCarry then maxCarry-currentB
       else 0 end newyearCorrection
    from balance1
    )
    ,balance2 as( -- rolling balance with correction (max carry over)
    select *
    from(
    select * 
      ,sum(case when sk='ac' then leaveQty else 0 end
           +case when sk='ny' then newyearCorrection else 0 end)
        over(partition by emp_id order by pointDt) 
      +lb as CurrentBalance
    from AccrualsNYover
    )t 
    where sk in('ny','ha','proj')
    )
    select EMP_ID,min(lt) Leave_type_id,min(hdt)HireDate
      ,min(startDt) Last_Accrual_date,min(lb) Leave_balance
      ,max(case when sk='proj' then CurrentBalance end) projBalance
      ,max(case when sk='ha' then CurrentBalance end) Hire_Anniversary_Balance
      ,max(case when sk='ny' then CurrentBalance end) CarryOver_Balance
      ,max(nyDt)NewYearDt,max(projDt)ProjectionDate,max(haDt)Hire_Anniversary_date
    from balance2
    group by emp_id
    order by emp_id 
    

    Output before grouping

    EMP_ID lb lt sk pointDt currentB leaveQty maxCarry newyearCorrection CurrentBalance startDt hdt nyDt projDt haDt lastDt pointM From_Months To_Months
    701 68 LT2 ac 2024-08-10 74 6 144 0 74 2024-07-27 2002-10-01 2025-01-01 2025-01-27 2024-10-01 2025-01-27 262 24 999998
    701 68 LT2 ac 2024-08-24 80 6 144 0 80 2024-07-27 2002-10-01 2025-01-01 2025-01-27 2024-10-01 2025-01-27 262 24 999998
    701 68 LT2 ac 2024-09-07 86 6 144 0 86 2024-07-27 2002-10-01 2025-01-01 2025-01-27 2024-10-01 2025-01-27 263 24 999998
    701 68 LT2 ac 2024-09-21 92 6 144 0 92 2024-07-27 2002-10-01 2025-01-01 2025-01-27 2024-10-01 2025-01-27 263 24 999998
    701 68 LT2 ha 2024-10-01 92 0 144 0 92 2024-07-27 2002-10-01 2025-01-01 2025-01-27 2024-10-01 2025-01-27 264 24 999998
    701 68 LT2 ac 2024-10-05 98 6 144 0 98 2024-07-27 2002-10-01 2025-01-01 2025-01-27 2024-10-01 2025-01-27 264 24 999998
    701 68 LT2 ac 2024-10-19 104 6 144 0 104 2024-07-27 2002-10-01 2025-01-01 2025-01-27 2024-10-01 2025-01-27 264 24 999998
    701 68 LT2 ac 2024-11-02 110 6 144 0 110 2024-07-27 2002-10-01 2025-01-01 2025-01-27 2024-10-01 2025-01-27 265 24 999998
    701 68 LT2 ac 2024-11-16 116 6 144 0 116 2024-07-27 2002-10-01 2025-01-01 2025-01-27 2024-10-01 2025-01-27 265 24 999998
    701 68 LT2 ac 2024-12-14 122 6 144 0 122 2024-07-27 2002-10-01 2025-01-01 2025-01-27 2024-10-01 2025-01-27 266 24 999998
    701 68 LT2 ac 2024-12-28 128 6 144 0 128 2024-07-27 2002-10-01 2025-01-01 2025-01-27 2024-10-01 2025-01-27 266 24 999998
    701 68 LT2 ny 2025-01-01 128 0 144 0 128 2024-07-27 2002-10-01 2025-01-01 2025-01-27 2024-10-01 2025-01-27 267 24 999998
    701 68 LT2 ac 2025-01-11 134 6 144 0 134 2024-07-27 2002-10-01 2025-01-01 2025-01-27 2024-10-01 2025-01-27 267 24 999998
    701 68 LT2 ac 2025-01-25 140 6 144 0 140 2024-07-27 2002-10-01 2025-01-01 2025-01-27 2024-10-01 2025-01-27 267 24 999998
    701 68 LT2 proj 2025-01-27 140 0 144 0 140 2024-07-27 2002-10-01 2025-01-01 2025-01-27 2024-10-01 2025-01-27 267 24 999998
    905 110 LT2 ac 2024-08-10 114 4 96 0 114 2024-07-27 2022-11-02 2025-01-01 2025-01-27 2024-11-02 2025-01-27 21 0 23
    905 110 LT2 ac 2024-08-24 118 4 96 0 118 2024-07-27 2022-11-02 2025-01-01 2025-01-27 2024-11-02 2025-01-27 21 0 23
    905 110 LT2 ac 2024-09-07 122 4 96 0 122 2024-07-27 2022-11-02 2025-01-01 2025-01-27 2024-11-02 2025-01-27 22 0 23
    905 110 LT2 ac 2024-09-21 126 4 96 0 126 2024-07-27 2022-11-02 2025-01-01 2025-01-27 2024-11-02 2025-01-27 22 0 23
    905 110 LT2 ac 2024-10-05 130 4 96 0 130 2024-07-27 2022-11-02 2025-01-01 2025-01-27 2024-11-02 2025-01-27 23 0 23
    905 110 LT2 ac 2024-10-19 134 4 96 0 134 2024-07-27 2022-11-02 2025-01-01 2025-01-27 2024-11-02 2025-01-27 23 0 23
    905 110 LT2 ha 2024-11-02 140 0 144 0 140 2024-07-27 2022-11-02 2025-01-01 2025-01-27 2024-11-02 2025-01-27 24 24 999998
    905 110 LT2 ac 2024-11-02 140 6 144 0 140 2024-07-27 2022-11-02 2025-01-01 2025-01-27 2024-11-02 2025-01-27 24 24 999998
    905 110 LT2 ac 2024-11-16 146 6 144 0 146 2024-07-27 2022-11-02 2025-01-01 2025-01-27 2024-11-02 2025-01-27 24 24 999998
    905 110 LT2 ac 2024-12-14 152 6 144 0 152 2024-07-27 2022-11-02 2025-01-01 2025-01-27 2024-11-02 2025-01-27 25 24 999998
    905 110 LT2 ac 2024-12-28 158 6 144 0 158 2024-07-27 2022-11-02 2025-01-01 2025-01-27 2024-11-02 2025-01-27 25 24 999998
    905 110 LT2 ny 2025-01-01 158 0 144 -14 144 2024-07-27 2022-11-02 2025-01-01 2025-01-27 2024-11-02 2025-01-27 26 24 999998
    905 110 LT2 ac 2025-01-11 164 6 144 0 150 2024-07-27 2022-11-02 2025-01-01 2025-01-27 2024-11-02 2025-01-27 26 24 999998
    905 110 LT2 ac 2025-01-25 170 6 144 0 156 2024-07-27 2022-11-02 2025-01-01 2025-01-27 2024-11-02 2025-01-27 26 24 999998
    905 110 LT2 proj 2025-01-27 170 0 144 0 156 2024-07-27 2022-11-02 2025-01-01 2025-01-27 2024-11-02 2025-01-27 26 24 999998
    906 110 LT3 ac 2024-08-10 112 2 144 0 112 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 27 24 27
    906 110 LT3 ac 2024-08-24 114 2 144 0 114 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 27 24 27
    906 110 LT3 ac 2024-09-07 117 3 144 0 117 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 28 28 31
    906 110 LT3 ac 2024-09-21 120 3 144 0 120 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 28 28 31
    906 110 LT3 ac 2024-10-05 123 3 144 0 123 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 29 28 31
    906 110 LT3 ac 2024-10-19 126 3 144 0 126 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 29 28 31
    906 110 LT3 ac 2024-11-02 129 3 144 0 129 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 30 28 31
    906 110 LT3 ac 2024-11-16 132 3 144 0 132 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 30 28 31
    906 110 LT3 ac 2024-12-14 135 3 144 0 135 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 31 28 31
    906 110 LT3 ac 2024-12-28 138 3 144 0 138 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 31 28 31
    906 110 LT3 ny 2025-01-01 138 0 144 0 138 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 32 32 35
    906 110 LT3 ac 2025-01-11 142 4 144 0 142 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 32 32 35
    906 110 LT3 ac 2025-01-25 146 4 144 0 146 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 32 32 35
    906 110 LT3 proj 2025-01-27 146 0 144 0 146 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 32 32 35
    906 110 LT3 ac 2025-02-08 150 4 144 0 150 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 33 32 35
    906 110 LT3 ac 2025-02-22 154 4 144 0 154 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 33 32 35
    906 110 LT3 ac 2025-03-08 158 4 144 0 158 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 34 32 35
    906 110 LT3 ac 2025-03-22 162 4 144 0 162 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 34 32 35
    906 110 LT3 ac 2025-04-05 166 4 144 0 166 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 35 32 35
    906 110 LT3 ac 2025-04-19 170 4 144 0 170 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 35 32 35
    906 110 LT3 ac 2025-05-03 175 5 144 0 175 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 36 36 999998
    906 110 LT3 ha 2025-05-03 175 0 144 0 175 2024-07-27 2022-05-03 2025-01-01 2025-01-27 2025-05-03 2025-05-03 36 36 999998

    fiddle

    The case has not been verified when the new year and the date of accruals coincide.