March 3, 2015 at 8:08 pm
Awesome. Thanks for the feedback, JeeTee. It's not often that a decade old thread gets feedback like this.
--Jeff Moden
Change is inevitable... Change for the better is not.
January 3, 2017 at 1:07 pm
Just a small update.
--===== If the function already exists, drop it
IF OBJECT_ID(N'[dbo].[ifn_WorkDays]') IS NOT NULL
DROP FUNCTION [dbo].[ifn_WorkDays]
GO
GO
CREATE FUNCTION dbo.ifn_WorkDays
/***************************************************************************************
Purpose:
1. Given any valid start date and end date, this function will calculate and return
the number of workdays (Mon - Fri).
2. Given only a valid start date (end date has DEFAULT in it), this function will
return a 1 if the start date is a weekday and a 0 if not a weekday.
Notes:
1. Holidays are NOT considered.
2. Because of the way SQL Server calculates weeks and named days of the week, no
special consideration for the value of DATEFIRST is given. In other words, it
doesn't matter what DATEFIRST is set to for this function.
3. If the input dates are in the incorrect order, they will be reversed prior to any
calculations.
4. Only whole days are considered. Times are NOT used.
5. The number of workdays INCLUDES both dates
6. Inputs may be literal representations of dates, datetime datatypes, numbers that
represent the number of days since 1/1/1900 00:00:00.000, or anything else that can
be implicitly converted to or already is a datetime datatype.
7. Undocumented: The DATEPART(dw,date) does not actually count weeks... It counts the
transition to a Sunday regardless of the DATEFIRST setting. In essence, it counts
only whole weekends in any given date range.
8. This UDF does NOT create a tally table or sequence table to operate. Not only is
it set based, it is truly "tableless".
Revisions:
Rev 00 - 12/12/2004 - Jeff Moden - Initial creation and test.
Rev 01 - 12/12/2004 - Jeff Moden - Load test, cleanup, document, release.
Rev 02 - 12/26/2004 - Jeff Moden - Return NULL if @StartDate is NULL or DEFAULT and
modify to be insensitive to DATEFIRST settings.
Rev 03 - 01/03/2017 - Luis Cazares - Change the function into an iTVF. Keep the functionality
*/
(
@StartDate datetime,
@EndDate datetime
)
RETURNS TABLE
AS
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) AS WorkDays
FROM (SELECT DATEADD(dd,DATEDIFF(dd,0,MIN(adate)),0) AS StartDate, DATEADD(dd,DATEDIFF(dd,0,MAX(adate)),0) AS EndDate
FROM (VALUES(@StartDate),(@EndDate))x(adate)
WHERE @StartDate IS NOT NULL)y
GO
January 3, 2017 at 7:29 pm
Spot on, Luis! Thanks for the update.
--Jeff Moden
Change is inevitable... Change for the better is not.
June 8, 2018 at 10:26 am
I stumbled across this while looking to create my own and would like to propose a couple changes:
First, I have noticed lately moving filters from a WHERE clause to a WHEN clause often results in nice performance gains. Also, this:SELECT * FROM dbo.ifn_WorkDays(getdate(),NULL)
Returns a 1. To fix that I changed WHERE @StartDate IS NOT NULL in the subquery to WHEN DATEDIFF(dd, @startDate, @endDate) IS NOT NULL in my CASE statement.... and some schemabinding. Here's the proposed updated version:--===== If the function already exists, drop it
IF OBJECT_ID(N'dbo.ifn_WorkDays') IS NOT NULL
DROP FUNCTION dbo.ifn_WorkDays
GO
CREATE FUNCTION dbo.ifn_WorkDays
/***************************************************************************************
Purpose:
1. Given any valid start date and end date, this function will calculate and return
the number of workdays (Mon - Fri).
2. Given only a valid start date (end date has DEFAULT in it), this function will
return a 1 if the start date is a weekday and a 0 if not a weekday.
Notes:
1. Holidays are NOT considered.
2. Because of the way SQL Server calculates weeks and named days of the week, no
special consideration for the value of DATEFIRST is given. In other words, it
doesn't matter what DATEFIRST is set to for this function.
3. If the input dates are in the incorrect order, they will be reversed prior to any
calculations.
4. Only whole days are considered. Times are NOT used.
5. The number of workdays INCLUDES both dates
6. Inputs may be literal representations of dates, datetime datatypes, numbers that
represent the number of days since 1/1/1900 00:00:00.000, or anything else that can
be implicitly converted to or already is a datetime datatype.
7. Undocumented: The DATEPART(dw,date) does not actually count weeks... It counts the
transition to a Sunday regardless of the DATEFIRST setting. In essence, it counts
only whole weekends in any given date range.
8. This UDF does NOT create a tally table or sequence table to operate. Not only is
it set based, it is truly "tableless".
Revisions:
Rev 00 - 12/12/2004 - Jeff Moden - Initial creation and test.
Rev 01 - 12/12/2004 - Jeff Moden - Load test, cleanup, document, release.
Rev 02 - 12/26/2004 - Jeff Moden - Return NULL if @StartDate is NULL or DEFAULT and
modify to be insensitive to DATEFIRST settings.
Rev 03 - 01/03/2017 - Luis Cazares - Change the function into an iTVF. Keep the functionality
Rev 04 - 06/08/2018 - Alan Burstein - 1. Moved NULL parameter filering from subquery "x" to the
WHERE clause to remove a filter from the execution plan.
2. Updated function to return a NULL if either @startDate
OR @enddate are NULL.
3. Added SCHEMABINDING
*/
(
@startDate datetime,
@endDate datetime
)
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT workDays =
-- If @startDate or @endDate are NULL then rerturn a NULL
CASE WHEN DATEDIFF(dd, @startDate, @endDate) IS NOT NULL THEN
(DATEDIFF(dd, startDate, endDate) + 1) --Start with total days including weekends
-(DATEDIFF(wk, startDate, endDate) * 2) --Subtact 2 days for each full weekend
-- Subtract 1 when startDate is Sunday and Substract 1 when endDate is Sunday:
-(CASE WHEN DATENAME(dw, startDate) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, endDate) = 'Saturday' THEN 1 ELSE 0 END)
END
FROM
( -- if @endDate is earlier that @startDate then flip them
SELECT StartDate = DATEADD(dd, DATEDIFF(dd,0,MIN(adate)), 0),
EndDate = DATEADD(dd, DATEDIFF(dd,0,MAX(adate)), 0)
FROM (VALUES(@startDate),(@endDate)) x(adate))y;
GO
Moving the filter from the WHERE clause to the CASE statement changes the execution plan like so:
Here's a quick performance test I cooked up (dbo.ifn_WorkDays is the original, dbo.ifn_WorkDaysV2 is the updated version):-- 1 Million Row Test Harness
SET NOCOUNT ON;
IF OBJECT_ID('tempdb..#date') IS NOT NULL DROP TABLE #date
SELECT TOP (1000000)
d1 = DATEADD(dd, checksum(newid())%2000-2000, getdate()),
d2 = DATEADD(dd, checksum(newid())%2000-2000, getdate())
INTO #date
FROM sys.all_columns a, sys.all_columns b;
GO
-- Start Fresh
DBCC FREEPROCCACHE with no_infomsgs;
DBCC DROPCLEANBUFFERS with no_infomsgs;
IF OBJECT_ID('tempdb..#stats') IS NOT NULL DROP TABLE #stats;
CREATE TABLE #stats (v tinyint, tm int);
GO
PRINT char(13)+char(10)+'Print dbo.ifn_WorkDays'+char(13)+char(10);
GO
DECLARE @st datetime = getdate(), @x int;
SELECT @x = w.WorkDays
FROM #date d
CROSS APPLY dbo.ifn_WorkDays(d.d1, d.d2) w
OPTION (MAXDOP 1)
INSERT #stats VALUES (1, DATEDIFF(MS, @st, getdate()));
GO 5
PRINT char(13)+char(10)+'Print dbo.ifn_WorkDaysV2'+char(13)+char(10);
GO
DECLARE @st datetime = getdate(), @x int;
SELECT @x = w.WorkDays
FROM #date d
CROSS APPLY dbo.ifn_WorkDaysV2(d.d1, d.d2) w
OPTION (MAXDOP 1)
INSERT #stats VALUES (2, DATEDIFF(MS, @st, getdate()));
GO 5
-- raw stats:
SELECT
[version] = CASE v WHEN 1 THEN 'old' ELSE 'new' END,
[time] = tm
FROM #stats;
-- total average:
SELECT
[version] = CASE v WHEN 1 THEN 'old' ELSE 'new' END,
average = AVG(i.tm)
FROM #stats i
GROUP BY i.v;
Note that I tested this on my laptop and used MAXDOP 1 because I was getting a parallel plan that was not improving the performance any.
Results:
version time
------- -----------
old 1080
old 1070
old 1076
old 1074
old 1113
new 940
new 900
new 943
new 950
new 940version average
------- -----------
old 1082
new 934
That's ~15% performance improvement.
-- Itzik Ben-Gan 2001
June 13, 2018 at 9:21 am
Couldn't help it as when I read through the article and the whole discussion, I saw some obvious opportunities for improvement.
😎
Two pointers, an implicit conversion to nvarchar as the datename function returns nvarchar and datediff to 0 is more than five times more expensive than comparing the input values in a case statement.
Improved function
A test harness, configured for 1,000,000 rows
Results
T_TXT DURATION
DRY RUN 138479
ITVFN_CALC_WORKING_DAYS 585864
ifn_WorkDays Alan 1077415
ifn_WorkDays_Old 1328022
June 13, 2018 at 8:09 pm
Heh... this is a very old article... I don't believe I even knew what an implicit conversion was back then. 😀
Anyway, thanks for the science, Eirikur. Being a bit of a speed phreak myself, I always enjoy seeing performance improvments.
--Jeff Moden
Change is inevitable... Change for the better is not.
June 14, 2018 at 1:55 am
Jeff Moden - Wednesday, June 13, 2018 8:09 PMHeh... this is a very old article... I don't believe I even knew what an implicit conversion was back then. 😀Anyway, thanks for the science, Eirikur. Being a bit of a speed phreak myself, I always enjoy seeing performance improvments.
You know how it is, seeing an opportunity like this, one just can't help it.
😎
Viewing 7 posts - 151 through 156 (of 156 total)
You must be logged in to reply to this topic. Login to reply