Skip to Main Content
  • Questions
  • Adding hours to date, but within boundaries

Breadcrumb

Question and Answer

Chris Saxon

Thanks for the question, SABEGH.

Asked: March 05, 2020 - 4:16 pm UTC

Last updated: May 10, 2023 - 12:41 pm UTC

Version: 12.1.0.2.0

Viewed 10K+ times! This question is

You Asked

Problem:

We are working on a report, which displays tickets related metrics, based off of a custom Oracle view. TICKET's can be OPENED with any PRIORITY at any time of the day 24 x 7 x 365.

We have imposed an SLA on this ticket, there are 5 levels the tickets can be OPENED. P1 must be CLOSED in 4 hours, P2 in 8 Hours, P3 in 22 business hours, P4 in 44 business hours while P5 in 55 business hours.

Tickets with P1 and P2 are treated as if "business hours" were 24 hours a day, seven days a week, with no exceptions.

Users can also create P3, P4, and P5 any anytime of the day though, but the counter starts from 7 am and ends at 6 pm on workdays, as it is expected that the employee will resolve this ticket between the hours of 7 am to 6 pm, excluding weekends and holidays.
We maintain a DATE table, which has a flag with value 0 if it's a weekends/holidays

CREATE  TABLE DIM_FPDATE
  (
    DAY_HOLDER      DATE ,
    PERIOD_D        VARCHAR2(6) ,
    PERIOD_C        VARCHAR2(7) ,
    AGING_FLAG      DECIMAL(1,0) ,
    DAY_DESCRIPTION VARCHAR2(100) ,
    CONSTRAINT DIM_FPDATE_PK PRIMARY KEY (DAY_HOLDER)
  );
  
INSERT
INTO
  "DIM_FPDATE"
  (
    DAY_HOLDER,
    PERIOD_D ,
    PERIOD_C,
    AGING_FLAG,
    DAY_DESCRIPTION
  )
WITH
  SM AS
  (
SELECT
  to_date ('01-JAN-2017', 'dd-mon-yyyy') + rownum - 1 DAY_HOLDER
FROM
  DUAL
  CONNECT BY LEVEL < 1 + TO_DATE ('01-JAN-2025', 'dd-mon-yyyy') - TO_DATE (
  '01-JAN-2015', 'dd-mon-yyyy')                           +1
  )
SELECT
  DAY_HOLDER ,
  TO_CHAR(DAY_HOLDER,'YYYYMM') PERIOD_D ,
  TO_CHAR(DAY_HOLDER,'MM/YYYY') PERIOD_C ,
  CASE
    WHEN TO_CHAR(DAY_HOLDER,'DY') IN ('SAT','SUN')
    THEN 0
    ELSE 1
  END AGING_FLAG ,
  CASE
    WHEN TO_CHAR(DAY_HOLDER,'DY') IN ('SAT','SUN')
    THEN TO_CHAR(DAY_HOLDER,'Dy')
      ||', Weekend'
    ELSE TO_CHAR(DAY_HOLDER,'Dy')
      ||', ACI Working Day'
  END DAY_DESCRIPTION
FROM
  SM;

--Update Company Holiday for 4th of Jul 20
UPDATE
  DIM_FPDATE
SET
  AGING_FLAG      = 0 ,
  DAY_DESCRIPTION = TO_CHAR(DAY_HOLDER,'Dy')
  ||', Independence Day'
WHERE
  DAY_HOLDER = '03-JUL-2020';


The User needs now a new column, which will have the exact date/time when the ticket would miss its SLA.

For e.g

For P1 we can just add 4 hours to the OPEN_DATE, while for P2 add 8 hours, it's for P3, P4, and P5, where I am finding it tricky as we have to restrict our windows between 7, am to 6 pm and excludes weekends and holidays.

So for P3 tickets, if it was OPENED at 06-MAR-2020 01:00:00, it should be RESOLVED by 09-MAR-2020 18:00:00 (For P3 it's 22 hours (2 days - 7 am to 6 pm: 11 hours). 06-MAR-2020 01:00:00 is Friday, User will start at 7 am, so will have 11 hours for Friday, following Sat/Sun will not be considered and on Monday again he/she will have 11 hours to get it resolved unless Monday is a Holiday. So ticket 3000 should be resolved by 09-MAR-2020 18:00:00.

Sample Sql:

WITH DATA_SET AS  (  
SELECT 1000 TICKET_NO, 1 PRIORITY, TO_DATE('03-MAR-2020 07.00.00','DD-MON-YYYY HH24.MI.SS') SUBMITTED_DATE from dual  
UNION ALL  
SELECT 2000 TICKET_NO, 2 PRIORITY, TO_DATE('03-MAR-2020 09.00.00','DD-MON-YYYY HH24.MI.SS') SUBMITTED_DATE FROM DUAL  
UNION ALL  
SELECT 3000 TICKET_NO, 3 PRIORITY, TO_DATE('06-MAR-2020 01.00.00','DD-MON-YYYY HH24.MI.SS') SUBMITTED_DATE from dual  
UNION ALL  
SELECT 4000 TICKET_NO, 4 PRIORITY, TO_DATE('02-JUL-2020 12.00.00','DD-MON-YYYY HH24.MI.SS') SUBMITTED_DATE FROM DUAL  
UNION ALL  
SELECT 5000 TICKET_NO, 5 PRIORITY, TO_DATE('26-FEB-2020 01.00.00','DD-MON-YYYY HH24.MI.SS') SUBMITTED_DATE from dual  
) 
SELECT 
 A.*, CASE 
WHEN PRIORITY = 1 THEN SUBMITTED_DATE + 4/24
WHEN PRIORITY = 2 THEN SUBMITTED_DATE + 8/24
WHEN PRIORITY = 3 THEN SUBMITTED_DATE + 2
WHEN PRIORITY = 4 THEN SUBMITTED_DATE + 4
WHEN PRIORITY = 5 THEN SUBMITTED_DATE + 5
ELSE sysdate  END EXPECTED_CLOSE_TIME
from data_set A;



So the output (EXPECTED_CLOSE_TIME) we are expecting is

TICKET_NO   PRIORITY    SUBMITTED_DATE          EXPECTED_CLOSE_TIME
1000      1         03-MAR-2020 07.00.00    03-MAR-2020 11.00.00
2000      2         03-MAR-2020 09.00.00    03-MAR-2020 17.00.00
3000      3         06-MAR-2020 01.00.00    09-MAR-2020 18.00.00
4000      4         02-JUL-2020 13.00.00    09-JUL-2020 13.00.00
5000      5         26-FEB-2020 01.00.00    03-MAR-2020 18.00.00


Thanks,
Saby

and Chris said...

OK, here you go:

First up, I'd create a table of priorities storing the SLA in hours. And add start/end hour columns to the dates table:

alter table DIM_FPDATE
  add (
    business_start_hour int, 
    business_close_hour int
  );

create table priorities (
  priority_id integer not null primary key,
  sla_hours   integer not null,
  business_hours_only varchar2(1) not null
    check ( business_hours_only in ( 'Y', 'N' ) )
);

insert into priorities values ( 1, 4, 'N' );
insert into priorities values ( 2, 8, 'N' );
insert into priorities values ( 3, 22, 'Y' );
insert into priorities values ( 4, 44, 'Y' );
insert into priorities values ( 5, 55, 'Y' );

update dim_fpdate
set    business_start_hour = 7, 
       business_close_hour = 18
where  to_char ( day_holder, 'dy' ) in (
  'mon', 'tue', 'wed', 'thu', 'fri'
)
and    aging_flag = 1;

commit;


This makes it easier to change the SLAs & deal with one-off changes to working hours (e.g. finishing early on Christmas Eve). It also makes the query a lot easier to write :)

Calculating P1 & 2 breach times is trivial, so for simplicity I'm ignoring them. It's probably easiest to find these in a separate subquery. Then union (all) together with the other priorities.

In outline, what you want to do is find:

- The running total for the number of working hours elapsed for the current and previous (working) date
- Find the day where the SLA hours falls between these two values

To find the number of SLA hours consumed in a day, you need to find:

The datetime work stops ( date + business_close_hour ) MINUS
The largest of:
submit date
The datetime work starts ( date + business_start_hour )

Then SUM these as an analytic:

 partition by ticket_no
 order  by day_holder


To find the previous hour total, set the window to:

rows between unbounded preceding and 1 preceding


Altogether this gives:

with data_set as  (  
  select 3000 ticket_no, 3 priority, to_date('06-mar-2020 07.00.00','dd-mon-yyyy hh24.mi.ss') submitted_date from dual  
  union all  
  select 4000 ticket_no, 4 priority, to_date('02-jul-2020 13.00.00','dd-mon-yyyy hh24.mi.ss') submitted_date from dual  
  union all  
  select 5000 ticket_no, 5 priority, to_date('26-feb-2020 01.00.00','dd-mon-yyyy hh24.mi.ss') submitted_date from dual  
), days as (
  SELECT 
    A.*,
    round ( 
      sum (
        ( day_holder + ( business_close_hour / 24 ) ) -
        greatest (
          ( day_holder + ( business_start_hour / 24 ) ) ,
           submitted_date
        )
      ) over ( 
        partition by ticket_no
        order  by day_holder
      ) * 24 
    ) tot,
    round ( 
      sum (
        ( day_holder + ( business_close_hour / 24 ) ) -
        greatest (
          ( day_holder + ( business_start_hour / 24 ) ) ,
          submitted_date
        )
      ) over ( 
        partition by ticket_no
        order  by day_holder
          rows between unbounded preceding and 1 preceding
      ) * 24 
    ) prev_tot, d.*, sla_hours
  from data_set A
  join  dim_fpdate d
  on    trunc ( SUBMITTED_DATE ) <= day_holder
  and   submitted_date + 10 > day_holder
  join  priorities p
  on    a.priority = p.priority_id
)
  select ticket_no,
         priority,
         submitted_date,
         day_holder + ( business_start_hour / 24 )+ 
           ( ( sla_hours - prev_tot ) / 24 ) breach_datetime
  from   days d
  where  sla_hours > prev_tot 
  and    sla_hours <= tot
  order  by ticket_no;
  
TICKET_NO    PRIORITY    SUBMITTED_DATE          BREACH_DATETIME        
        3000           3 06-MAR-2020 07:00:00    09-MAR-2020 18:00:00    
        4000           4 02-JUL-2020 13:00:00    09-JUL-2020 13:00:00    
        5000           5 26-FEB-2020 01:00:00    03-MAR-2020 18:00:00   


In the dates to tickets join I've added 10 days to the submit date:

  on    trunc ( SUBMITTED_DATE ) <= day_holder
  and   submitted_date + 10 > day_holder


This number of days this is somewhat arbitrary. But it needs to at least as many days for your longest SLA (5), plus however many non-working days in a row you can have.

Set it too low and the query won't work! So err on the side of caution here and pick a bigger value ;)

I discuss this problem further and discuss other options in this article on adding N working days to a date:

https://blogs.oracle.com/sql/how-to-find-the-next-business-day-and-add-or-subtract-n-working-days-with-sql

Rating

  (6 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

Worked 99%

Sabegh, March 08, 2020 - 4:53 pm UTC

Thank you very much for the elegant solution, it really helped!

I used this SQL with actual data set, where I have sporadic Submission_Date, it worked on most of them. It looks like its rounding the Minutes to the nearest HOURS hence going OFF by little.

For Example

if a P2 (22 hours == 2days ) ticket came in at 06-MAR-2020 07.25.13, the O/P should be 10-MAR-2020 07:25:13, but it is showing 09-MAR-2020 18.00.00.

Chris Saxon
March 09, 2020 - 10:40 am UTC

Decide what your unit of granularity is, then convert everything to this.

For example, if the nearest minute is good enough:

* Convert the *HOURS columns to *MINUTES columns (multiply my examples up by 60)
* Instead of dividing by 24 (hours in a day), divide by 1,440 (minutes in a day)

If you need to the second, multiply everything up again by another 60.

Updated Review

Sabegh, March 09, 2020 - 2:27 pm UTC

Thanks again!

Just by removing those ROUND calls around those ANALYTICAL functions, solved my problem.

I appreciate your help!
Chris Saxon
March 09, 2020 - 2:35 pm UTC

Ah yes, that would do it too :)

sum of date + int

Lara, August 11, 2022 - 10:05 am UTC

What is the expect result of :

day_holder + ( business_close_hour / 24 )


I have the same requirement that this solution addresses but I'm working in a SQL Server db. Dates and Integers are not playing nicely together! What is happening to the day_holder value to allow it to be added to an integer?

Appreciate this not an Oracle query but any advice will be appreciated. Thanks.
Chris Saxon
August 11, 2022 - 1:37 pm UTC

It adds BUSINESS_CLOSE_HOUR hours to the date DAY_HOLDER. You can add any number to a date in Oracle Database, this adds that many days to the date. Any fractional component to the number is the corresponding fraction of a day (in hours/minutes/seconds).

If you need help with SQL Server you'll need to find another forum - we specialize in Oracle Database here.

SLA 2h + only on working days

A reader, April 26, 2023 - 2:03 pm UTC

Hi Chris.

Is the code above including the scenario when the ticket's SLA is 2h and support can work on them only during working days? I can't make it work; the minimum SLA is 12h, but I may have missed something.
Chris Saxon
April 27, 2023 - 1:08 pm UTC

What exactly have you tried?

SLA 2h + only on working days

Piotr, April 26, 2023 - 9:17 pm UTC

Maybe nvl(prev_tot, 0) would fix this?

SLA 2h + only on working days

Piotr, April 28, 2023 - 4:44 pm UTC

@Chris: I changed P3 SLA to 2h in table PRIORITIES, which is the case in my company:
insert into priorities values ( 3, 2, 'Y' );

Chris Saxon
May 10, 2023 - 12:41 pm UTC

Mapping null prev_tots to zero is part of the solution (nvl ( round ( sum ( ... ) ), 0 ) prev_tot)

You then also need to change the final breach time calculation to be the greatest of the submission date or the day holder + start hour:

greatest ( day_holder + ( business_start_hour / 24 ), submitted_date ) + ...

More to Explore

Analytics

Analytic SQL got you confused? Check out Connor McDonald's complete video course.