简体   繁体   中英

SQL Server 2008 - Sum business minutes between two dates taking into account custom holidays and weekends

Using SQL Server 2008, I need to sum the business minutes between two datetime fields, while taking into consideration non working hours and weekends/company holidays. I would like to incorporate a calendar if possible so that should I need to edit any holiday it could be easily done.

Eg

OpenCall              CloseCall 
05/08/2013 14:00:00   06/08/2013 09:30:00             

The result for the above, needs to return: 240 -- (4 hours) working hours are: 08:30-17:00.

If the call was open on Friday and was closed on Tuesday, then it should only calculate the minutes between the working hours on Friday, Monday and Tuesday (ie not the weekend).

I'm new to SQL/T-SQL so please explain any code/variables clearly - IF you can find a neat solution!

Thanks in advance!

First, This is the structure I have used, I don't think it will take much adapting to fit it into your structure.

(Note I would recommend a lot more fields in your calendar table, but IsWorkingDay is the only one required for this example)

SET DATEFIRST 1;
CREATE TABLE dbo.Calendar
(       [Date]          DATE NOT NULL,
        IsWorkingDay    BIT NOT NULL
    CONSTRAINT PK_Calendar_Date PRIMARY KEY ([Date])
);

-- INSERT DATES IN 2013 (NOT DOING A FULL TABLE AS IT'S JUST AN EXAMPLE)
INSERT dbo.Calendar ([Date], IsWorkingDay)
SELECT  [Date] = DATEADD(DAY, Number, '20130101'), 1
FROM    Master..spt_values
WHERE   Type = 'P'
AND     Number < 365;

-- UPDATE NON WORKING DAYS
UPDATE  dbo.Calendar
SET     IsWorkingDay = 0
WHERE   DATEPART(WEEKDAY, [Date]) IN (6, 7)
OR      [Date] IN ('20130101', '20130329', '20130401', '20130506', '20130527', '20130826', '20131225', '20131226');

-- CREATE SAMPLE DATA
CREATE TABLE T (OpenCall DATETIME NOT NULL, CloseCall DATETIME NOT NULL);
INSERT T (OpenCall, CloseCall)
VALUES 
    ('20130805 14:00:00', '20130806 09:30:00'),
    ('20130823 16:00:00', '20130828 10:30:00'); -- CROSS BANK HOLIDAY AND WEEKEND

The first step is to get all days between your two dates. You can do this by joining to the calendar table where the date in the calender table is between the opening and closing datetimes:

SELECT  T.OpenCall,
        T.CloseCall,
        Calendar.[Date],
        StartTime = CASE WHEN CAST(T.OpenCall AS DATE) = Calendar.[Date] THEN CAST(T.OpenCall AS TIME) ELSE CAST('08:30' AS TIME) END,
        EndTime = CASE WHEN CAST(T.CloseCall AS DATE) = Calendar.[Date] THEN CAST(T.CloseCall AS TIME) ELSE CAST('17:00' AS TIME) END
FROM    T
        INNER JOIN Calendar
            ON Calendar.Date >= CAST(T.OpenCall AS DATE)
            AND Calendar.Date <= CAST(T.CloseCall AS DATE)
            AND Calendar.IsWorkingDay = 1;

For the example data, this would give

+---------------------+---------------------+------------+----------+----------+
| OpenCall            | CloseCall           |   Date     |StartTime | EndTime  |
|---------------------+---------------------+------------+----------+----------|
| 2013-08-05 14:00:00 | 2013-08-06 09:30:00 | 2013-08-05 | 14:00:00 | 17:00:00 |
| 2013-08-05 14:00:00 | 2013-08-06 09:30:00 | 2013-08-06 | 08:30:00 | 09:30:00 |
|---------------------+---------------------+------------+----------+----------|
| 2013-08-23 16:00:00 | 2013-08-28 10:30:00 | 2013-08-23 | 16:00:00 | 17:00:00 |
| 2013-08-23 16:00:00 | 2013-08-28 10:30:00 | 2013-08-27 | 08:30:00 | 17:00:00 |
| 2013-08-23 16:00:00 | 2013-08-28 10:30:00 | 2013-08-28 | 08:30:00 | 09:30:00 |
+---------------------+---------------------+------------+----------+----------+

As you can see, on the first day it uses the open time from the source data , and on the last day of each range it uses the close time from source data, for all other start/end times it uses hard coded business hours (in this case 9am-5.30pm).

The last step would just be to sum up the difference between the starttime and the endtime for each range:

WITH Data AS
(   SELECT  T.OpenCall,
            T.CloseCall,
            StartTime = CASE WHEN CAST(T.OpenCall AS DATE) = Calendar.[Date] THEN CAST(T.OpenCall AS TIME) ELSE CAST('08:30' AS TIME) END,
            EndTime = CASE WHEN CAST(T.CloseCall AS DATE) = Calendar.[Date] THEN CAST(T.CloseCall AS TIME) ELSE CAST('17:00' AS TIME) END
    FROM    T
            INNER JOIN Calendar
                ON Calendar.Date >= CAST(T.OpenCall AS DATE)
                AND Calendar.Date <= CAST(T.CloseCall AS DATE)
                AND Calendar.IsWorkingDay = 1
)
SELECT  OpenCall,
        CloseCall,
        BusinessMinutes = SUM(DATEDIFF(MINUTE, StartTime, EndTime))
FROM    Data
GROUP BY OpenCall, CloseCall;

Giving an end result of:

+---------------------+---------------------+--------------------+
| OpenCall            | CloseCall           |   BusinessMinutes  |
|---------------------+---------------------+--------------------+
| 2013-08-05 14:00:00 | 2013-08-06 09:30:00 |        240         |
| 2013-08-23 16:00:00 | 2013-08-28 10:30:00 |        690         |
+---------------------+---------------------+--------------------+

Example on SQL Fiddle

Here's my attempt. The goal was to get this query without table of dates for each date in period. I think this could work faster for long periods, but have not tested it.

declare @Start_Time time = '08:30', @End_Time time = '17:00'
declare @Whole_Date_Minutes int = datediff(mi, @Start_Time, @End_Time)

;with cte as (
    select
        C.OpenCall, C.CloseCall,
        cast(C.OpenCall as date) as OpenCallDate,
        case when cast(C.OpenCall as time) < @Start_Time then @Start_Time else cast(C.OpenCall as time) end as OpenCallTime,
        cast(C.CloseCall as date) as CloseCallDate,
        case when cast(C.CloseCall as time) > @End_Time then @End_Time else cast(C.CloseCall as time) end as CloseCallTime
    from @Calls as C
), cte2 as (
    select
        OpenCall, CloseCall, OpenCallDate, OpenCallTime,
        case when CloseCallDate > OpenCallDate then OpenCallDate else CloseCallDate end as CloseCallDate,
        case when CloseCallDate > OpenCallDate then @End_Time else CloseCallTime end as CloseCallTime
    from cte
    union all
    select
        OpenCall, CloseCall, dateadd(dd, 1, OpenCallDate) as OpenCallDate, @Start_Time as OpenCallTime,
        CloseCallDate, CloseCallTime
    from cte
    where CloseCallDate > OpenCallDate
)
select
    c.OpenCall, c.CloseCall,
    sum(
        @Whole_Date_Minutes + 
        datediff(dd, c.OpenCallDate, CloseCallDate) * @Whole_Date_Minutes - 
        datediff(mi, @Start_Time, c.OpenCallTime) - 
        datediff(mi, c.CloseCallTime, @End_Time) -
        H.[Days] * @Whole_Date_Minutes
    ) as BusinessMinutes 
from cte2 as c
    outer apply (select count(*) as [Days] from @Holidays as H where H.[Date] >= c.OpenCallDate and H.[Date] <= c.CloseCallDate) as H
group by c.OpenCall, c.CloseCall

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM