Calculating how many Working Days between 2 Dates - T-SQL? - sql

I realise different solutions will have different variations of what "Working Days" means but in my case I mean Monday to Friday inclusive.
Basically I have Created a function to do the calculation for me and my current solution works. My concern (and reason for asking this question) is that I am worried that this is a bad way of achieving this because the function is being called with a very high frequency. In the last 3 months it has been called 12 million times on a production system, with the average worker time 44ms.
This lead me to wonder if this is the correct way of achieving solution.
Firstly here is the function I created:
CREATE FUNCTION [dbo].[fn_WorkDays]
(
#StartDate DATETIME,
#EndDate DATETIME = NULL --#EndDate replaced by #StartDate when DEFAULTed
)
RETURNS INT
AS
BEGIN
--===== Declare local variables
--Temporarily holds #EndDate during date reversal
DECLARE #Swap DATETIME
--===== If the Start Date is null, return a NULL and exit
IF #StartDate IS NULL
RETURN NULL
--===== If the End Date is null, populate with Start Date value
-- so will have two dates (required by DATEDIFF below)
IF #EndDate IS NULL
SELECT #EndDate = #StartDate
--===== Strip the time element from both dates (just to be safe) by converting
-- to whole days and back to a date. Usually faster than CONVERT.
-- 0 is a date (01/01/1900 00:00:00.000)
SELECT #StartDate = DATEADD(dd,DATEDIFF(dd,0,#StartDate),0),
#EndDate = DATEADD(dd,DATEDIFF(dd,0,#EndDate) ,0)
--===== If the inputs are in the wrong order, reverse them
IF #StartDate > #EndDate
SELECT #Swap = #EndDate,
#EndDate = #StartDate,
#StartDate = #Swap
--===== Calculate and return the number of workdays using the
-- input parameters. This is the meat of the function.
-- This is really just one formula with a couple of parts
-- that are listed on separate lines for documentation
-- purposes.
RETURN (
SELECT
--Start with total number of days including weekends
(DATEDIFF(dd,#StartDate,#EndDate)+1)
--Subtact 2 days for each full weekend
-(DATEDIFF(wk,#StartDate,#EndDate)*2)
--If StartDate is a Sunday, Subtract 1
-(CASE WHEN DATENAME(dw,#StartDate) = 'Sunday'
THEN 1
ELSE 0
END)
--If EndDate is a Saturday, Subtract 1
-(CASE WHEN DATENAME(dw,#EndDate) = 'Saturday'
THEN 1
ELSE 0
END)
)
END
As a simple example of its use I would run this type of query:
SELECT MYTABLE.EntryDate
,dbo.fn_WorkDays(MYTABLE.EntryDate, getutcdate()) as WorkingDays
FROM MYTABLE
MyTable could contain 5000 rows all with different Dates in the EntryDate Column (5000 calls to Function)
My question is I am missing something here in the way that I am doing this, would it be beneficial to create a lookup table for this (but that is a lot of combinations of dates)
Any thoughts, improvements or recommendations would be appreciated...

I don't think there's a lot you can do with the UDF tbh - having it calculated at run-time like this in SQL is always going to incur a hit to some degree.
So, ideally (and this may not be possible as I don't know the full picture), I think what I'd do is store the WorkingDays number in your table and calculate it ONCE when the record is created. If that's not possible (i.e. when the record is created, you don't have an "end date" so it has to be worked out using "now") then I'd be considering a nightly scheduled job to go and recalculate all those particular records so that they are updated each day - then when an "end date" does get entered, that record does not get included in this batch update.
The benefits of this, are you offload the calculations to a quieter period, and only do the calculations once per day. The query becomes a lot simpler and more performant as it can just read the WorkingDays number from the table.
If that's not an option, then I'd suggest doing the calculations in the front end, remove the hit from the DB.

There's two problems here:
Calculating the number of days between two dates,
Identifying whether or not a give Date is a "business day".
The second includes easy ones like "weekday" versus "weekend", holidays (secular, religious, and legal), etc.
You'll need to solve both.
The first is easier, because relational databases will have functions to help you. It's the second that's harder and more variable, because it changes by locale and business.

Related

Improper calculation of a date difference in sql

I need to calculate the difference between DFU.HISTSTART and the last Sunday which is for today is 2/27. It should be dynamic and change every Sunday.
For some reason for this calculation I am getting 3 and should get 4.
,ABS(DATEDIFF(wk,
DATEADD(wk,
DATEDIFF(wk,6,GETDATE()), 0), DFU.HISTSTART))
AS '#WKS of Hist'
Does someone have any ideas?
You have two problems... the first is that you're trying to do the old offset trick with the "6". That works on other date parts but not on week. From the Microsoft Documentation...
For a week (wk, ww) or weekday (dw) datepart, the DATEPART return
value depends on the value set by SET DATEFIRST.
If your DATEFIRST is set to 7 (you can verify by running SELECT ##DATEFIRST;) AND your weeks start on Sundays, the following will work just fine and return a "4".
--===== Setup just the dates in question for a demo
DECLARE #HistStart DATE = '01-30-22'
,#Today DATE = '02-27-22'
;
--===== Demo the "right" way to use "wk".
-- I say "right" way because I don't trust DATEFIRST.
SELECT DATEDIFF(wk,#HistStart,#Today)
;
GO
The second thing is that it's generally a really bad practice to depend on the DATEFIRST setting in this global computing environment. Instead, do the much more universal/bullet-proof method of using Integer math to calculate the number of weeks it's been since date-serial 6, which you correctly identified as a Sunday.
--===== Setup just the dates in question for a demo
DECLARE #HistStart DATE = '01-30-22'
,#Today DATE = '02-27-22'
;
--===== Demo "Bulletproof" Way to calculate the difference in Weeks starting on Sunday
SELECT DATEDIFF(dd,6,#Today)/7 - DATEDIFF(dd,6,#HistStart)/7
;
If you need to calculate week differences in weeks a lot, you might want to turn that into a function so that if the company decides to change the day of the week that is the start of the week, you'll only need to change it in one place. In fact, you might want to have the function read it (the date-serial for the starting day of the week) from a "general settings table".
Another way of doing this:
with last_sunday as (
SELECT
case DAYNAME(current_date())
when 'Sun' then current_date()
when 'Mon'then current_date()-1
when 'Tue'then current_date()-2
when 'Wed'then current_date()-3
when 'Thu'then current_date()-4
when 'Fri'then current_date()-5
when 'Sat'then current_date()-6
else '2020-01-01' end "SUNDAY_DATE"
)
SELECT
DFU.HIST_START_DATE
,LAST_SUNDAY.SUNDAY_DATE
,datediff(week,DFU.HIST_START_DATE,LAST_SUNDAY.SUNDAY_DATE) weeks_diff
FROM DFU
JOIN LAST_SUNDAY
;

SSRS. Workday Function

I'm trying to convert the below Excel formula into SSRS but having looked around I cannot seem to find a solution. I can calculate the number of working days between two dates but what I'm trying to do is add on a number of working days to a date. Essentially I don't have the 2nd date.
I guess it would be something along the lines of the DATEADD function?
=WORKDAY($A1,B$1)
Hope someone can help
Many thanks
Here is a tsql solution to add X Business Days to a date.
declare #calendar as table (theDate date, dayOfWeek varchar (10));
declare #startDate as date = '20170704';
declare #businessDaysToAdd as integer = 10;
insert into #calendar
select theDate
, datename(dw, theDate) dow
from
dbo.dateTable('20170701', '20170720') ;
with temp as (
select theDate
, dayOfWeek
, rank() over (order by theDate) theRank
from #calendar
where theDate > #startDate
and dayOfWeek not in ('Saturday', 'Sunday')
)
select * from temp
where theRank = #businessDaysToAdd;
Notes
dbo.DateTable is a table valued function that just happens to exist in the database I was using. In real life, you might have an actual calendar table of some sort.
This example does not include holidays.
This is only the start of the answer to the posted question. It only solves the problem of Essentially I don't have the 2nd date.
Type this into the expression for the textbox. (From SSRS 2008 Datediff for Working Days)
=(DateDiff(DateInterval.day,Parameters!STARTDATE.Value,Parameters!ENDDATE.Value)+1)
-(DateDiff(DateInterval.WeekOfYear,Parameters!STARTDATE.Value,Parameters!ENDDATE.Value)*2)
-(iif(Weekday(Parameters!STARTDATE.Value) = 7,1,0)
-(iif(Weekday(Parameters!ENDDATE.Value) = 6,1,0))-1)
Ok after much perseverance I managed to get what I wanted in both TSQL and SSRS. My objective was to measure Agent productivity so I didn’t want to count the weekend and this would be unfair. If a date fell on a weekend then I wanted it to jump to a Monday. Likewise if adding number of days onto a date went over a weekend in the future then I needed the incremented date to reflect this. For the end user (In SSRS) I wanted a leading edge (Like an Upside down triangle) so that if the date + number working days was in the future then set to NULL, showing a zero would look like no productivity which is incorrect.
First TSQL - My base query started with the following SO thread but after trying many of the options I was finding when the date fell on a Saturday or Sunday the solution did not work for me (I was unable to create functions due to permissions). However tweaking the below got me there and I dealt with Sunday specifically
Add business days to date in SQL without loops
SELECT
,DATEADD(WEEKDAY, (/*Your Working Days*//5)*7+(/*Your Working Days*/ % 5) +
(CASE WHEN DATEPART(WEEKDAY,/*Your Date*/) <>7 AND DATEPART(WEEKDAY,/*Your Date*/) + (/*Your Working Days*/ % 5) >5 THEN 2
WHEN DATEPART(WEEKDAY,/*Your Date*/) = 7 AND DATEPART(WEEKDAY,/*Your Date*/) + (/*Your Working Days*/ % 5) >5 THEN 1 ELSE 0 END), /*Your Date*/) AS [IncrementedDate]
FROM /*YourTable*/
Then for SSRS - The 2 key points here is that TSQL will divide as an integer if the source number is an integer so this needs to be handled in SSRS and secondly you need to set the first week day to Monday as part of the expression.
I put this expression into a Matrix with Date Created being my Row Group and Contact Working Days being my Column Group.
=DATEADD("W",(INT(ReportItems!ContactWorkingDays.Value/5))*7+(ReportItems!ContactWorkingDays.Value MOD 5) + IIF(DATEPART("W",ReportItems!DateCreated.Value,FirstDayOfWeek.Monday) <> 7 AND (DATEPART("W",ReportItems!DateCreated.Value,FirstDayOfWeek.Monday) + (ReportItems!ContactWorkingDays.Value MOD 5) >5),2,IIF(DATEPART("W",ReportItems!DateCreated.Value,FirstDayOfWeek.Monday) = 7 AND (DATEPART("W",ReportItems!DateCreated.Value,FirstDayOfWeek.Monday) + (ReportItems!ContactWorkingDays.Value MOD 5) >5),1,0)),ReportItems!DateCreated.Value)
This does not include holidays - I'm not too bothered at this stage and that is for a rainy day! :)

Month to Date in SQL Server 2008

Hopefully this will be an easy one to answer.
I am working on a table that requires MTD data. One of our SQL guys told me to use
MONTH (#monthtodate)= 11
Where #monthtodate is set to GetDate() in the parameter list in SQL Server Management Studio. So in "theory", he says, it should select the month (11) and then get today and return all the requested data in between those two dates. But I'm thinking this isn't correct.
In looking at my data I'm starting to think that It's just returning data for the whole month of November instead of just MTD. I guess, technically, anything that has 0 won't be calculated. However that just means it's poorly written code correct?
In your opinions, would this be the better way to return MTD data:
production_date <= #today and Production_Date >= DATEADD(mm, DATEDIFF(mm, 0, #today), 0)
Thanks in advance everyone!
Here's how I do it. This should work on pretty much any version of SQL Server.
One important thing to note: at the outset, one should always establish a single value that represents 'now', the current moment in time. If you do not have a consistent value for now in your query, you will eventually get bit when your query is executed such that it crosses a date boundary whilst in-flight. Nothing like billing somebody for something they already paid for last month. Worst, edge-case bugs like that are difficult to catch, either by developers or by QA, since neither is likely to be working, say, at 11:59 on December 31.
The code:
declare
#dtNow datetime ,
#Today datetime ,
#dtFrom datetime ,
#dtThru datetime
---------------------------------------------------------------------------------------
-- set our effective notion of 'now'-ness.
--
-- We need have a consistent notion of now, lest we get bit in the a$$
-- by an edge case where we cross a day/month/year boundary in mid-execution.
--
-- NOTE: Mostly, we're interested in the *DATE* rather than the actual moment-in-time.
-- So, we carry around two flavors here.
---------------------------------------------------------------------------------------
set #dtNow = current_timestamp
set #Today = convert(datetime,convert(varchar,#dtNow,112),112)
---------------------------------------------------------------------------------------
-- compute the current date.
--
-- 1. get the current date sans timestamp (effectively start-of-day)
-- 2. add 1 day, then back off 3 millseconds to set it to the last tick of the current day
--
-- NOTE: Depending on the requirements of your particular application (and the nature
-- of your data), you might want to use the actual current date/time value as
-- your upper bound.
--
-- FURTHER NOTE: How far to back off is dependent on your date/time type:
--
-- * For DateTime, the resolution is milliseconds and the last tick of the day
-- is 997 milliseconds, so you need to back off 3ms from the start of the
-- next day.
--
-- * SmallDateTime has a 1 second resolution. The last tick of the day, natch,
-- is 59 seconds, so you need to back off 1 second from the start of the next day.
--
-- * For DateTime2, the user declares the precision in decimal fractions of a second,
-- though its resolution is 100ns ticks. You'll need (especially if you're working
-- with DateTime2 columns/variables of differing precision) experiment to figure out
-- what traps Microsoft has set for you inside DateTime2 and what you need to do to
-- make things work properly.
--
---------------------------------------------------------------------------------------
set #dtThru = dateadd(ms,-3,dateadd(day,1,#Today))
--set #dtThru = #dtNow -- if you need the actual current date/time value
---------------------------------------------------------------------------------------
-- compute start of month
--
-- We do this by subtracting the day number of 'today' from the date/time value #today.
-- That gives us the last day of the prior month. Then we add one day to get the first
-- day of the current month.
---------------------------------------------------------------------------------------
set #dtFrom = dateadd(day,1-day(#Today),#Today)
---------------------------------------------------------------------------------------
-- finally, make your query for 'current month to date'
---------------------------------------------------------------------------------------
select *
from dbo.foobar t
where t.recorded_date between #dtFrom and #dtThru
If you are asking which of these 2 queries is better from a performance standpoint:
DECLARE #now datetime = GETDATE()
SELECT *
FROM yourTable
WHERE Production_Date >= DATEADD(mm, DATEDIFF(mm, 0, #now), 0)
AND Production_Date < #now
SELECT *
FROM yourTable
WHERE YEAR(Production_Date) = YEAR(#now)
AND MONTH(Production_Date) = MONTH(#now)
AND Production_Date < #now
Then the first one would be, since it will use the index on Production_Date if there is one. However, they should both return the same results.

Business Days calculation

I have a query where I am calculating total days between two days including start and end date by the following SQL query. If the end date is not null, then end date is considered as current date.
This query does the job. But I do not want to count Sat and Sundays. Possible public UK Holidays.(I can do this one, if I can get the logic for Saturdays and Sundays)
SELECT DateDiff(day,DateADD(day,-1,StartDate),ISNULL(EndDate,getDate()))numberOfDays
FROM <mytable>
How do I count only weekdays between two dates?
Thank you
I would strongly recommend a calendar table for this, especially if you need to take specific holidays into account. Calculating Easter dynamically, for example, is going to be a royal pain.
http://web.archive.org/web/20070611150639/http://sqlserver2000.databases.aspfaq.com/why-should-i-consider-using-an-auxiliary-calendar-table.html
If you're going to use T-SQL alone, be careful about using functions that rely on regional/language settings for the output of things like DATENAME ...
Take a look at the DATEDIFF MSDN page.
At the bottom of the page, there is some user-generated content.
One user posted a function there which does exactly what you want, including holidays (headline: "UDF to return the number of business days, including a check to a bank holidays table").
try this
SELECT DateDiff(day,DateADD(day,-1,StartDate),ISNULL(EndDate,getDate())) -
( CASE WHEN DATENAME(dw, StartDate) = 'Sunday' OR
DATENAME(dw,ISNULL(EndDate,getDate())) = 'Sunday' THEN 1 ELSE 0 END)
- ( CASE WHEN DATENAME(dw, StartDate) = 'Saturday' OR
DATENAME(dw,ISNULL(EndDate,getDate())) = 'Saturday' THEN 1 ELSE 0 END)
numberOfDays
FROM <mytable>

SQL Stored Procedure: Business Hours

How can I create a stored procedure that accepts a start and end date.(e.g April 1 - April 30
1.) Get the business days including Saturdays x (a value). +
2.) Get Holidays x (a value)
and return the total.
I'm new to this, I guess it would be a tsql function. hmm.
any help would be appreciated.
Thanks
The simplest solution to this problem is to create a Calendar table that contains a value for every day you might want to consider. You could then add columns that indicate whether it is a business day or a holiday. With that, the problem becomes trivial:
Select ..
From Calendar
Where IsBusinessDay = 1
And Calendar.[Date] Between '2010-04-01' And '2010-04-30'
If you wanted the count of days, you could then do:
Select Sum( Case When IsBusinessDay = 1 Then 1 Else 0 End ) As BusinessDayCount
, Sum( Case When IsHoliday = 1 Then 1 Else 0 End ) As HolidayCount
From Calendar
Where Calendar.[Date] Between '2010-04-01' And '2010-04-30'
http://classicasp.aspfaq.com/date-time-routines-manipulation/how-do-i-count-the-number-of-business-days-between-two-dates.html
First, you will need to store all of the holidays into an independant table (Christmas, Easter, New Year Day, etc. with their respective dates (normally timed at midnight));
Second, you will have to generate, into a temporary table maybe, the dates of the office days, it then excludes the dates contained in the Holidays table.
Third, you may set the office hours to these dates depending on what day it is, if you have different working hours on different day.
That is the algorithm for you to find the appropriate code implementation.
Let me know if this helps!