views:

278

answers:

5

I have a table of dates call [BadDates], it has just one column where every record is a date to exclude. I have a UDF as follows:

CREATE FUNCTION [dbo].[udf_GetDateInBusinessDays]
(
  @StartDate datetime,  --Start Date
  @NumberDays int           --Good days ahead
)
RETURNS datetime
AS
BEGIN
-- Declare the return variable here
DECLARE @ReturnDate datetime
SET @ReturnDate = @StartDate
DECLARE @Counter int
SET @Counter = 0
WHILE @Counter < @NumberDays
BEGIN
 SET @ReturnDate = DateAdd(d,1,@ReturnDate)
 IF ((SELECT COUNT(ID)
  FROM dbo.[BadDates]
  WHERE StartDate = @ReturnDate) = 0)
 BEGIN
  SET @Counter = @Counter + 1
 END
END
RETURN @ReturnDate
END

This UDF works great, but it is slow in processing. The stored procedure that uses this runs the UDF in every record. Is there other ways to provide this same functionality in a faster method.

Any help is greatly appreciated!

A: 

You may want to put an index on BadDates.StartDate, but there may be other, better solutions.

Mark Canlas
A: 

Ok why are you counting when you can use the EXISTS keyword? If its because you can have multiple dates of the same type in Badates this seems wrong. COUNT will probably looking through the whole table to count the instances of startdate when all you need is 1 to exclude.

Have you had a look at the query plan to see what is happening?

John Nolan
I tried replacing IF ((SELECT COUNT(ID) FROM dbo.[BadDates] WHERE StartDate = @ReturnDate) = 0)withIF (NOT EXISTS(SELECT ID FROM dbo.[BadDates] WHERE StartDate = @ReturnDate))But that did not help much, 2 seconds of improvment.
Dustin Laine
A: 

Looks like you are using this UDF to calculate the difference between two dates. If i am interpreting this correctly then I would recommend that you use the built-in datediff function.

joe.liedtke
No, not difference. Basically if @StartDate were to equal 1/1/2009 and @NumberDays = 3 then it would look if 1/2/2009 were in BadDates, if not then it would increment counter, otherwise it would move on to 1/3/2009. This would happen until 3 good dates were incremented.If 1/3/2009 were the only date in baddates then the output would 1/5/2009.
Dustin Laine
So, why not query for the number of records in a given date range in bad dates and then use dateadd to increment the date if necessary?
joe.liedtke
+2  A: 

I'm assuming that what you are trying to do is calculate the date that is x working days past a given date.e.g. what date is 10 working days from today. I am also assuming that your baddates table contains non-working days e.g. Weekends and bank holidays.

I have encountered similar requirements in the past and usually ended up with days table that contains all possible dates along with a flag that indicates whether a particular date is a working day or not.

I then use that table to calculate what date is x working days from the provided date by selecting the record that is x days after the starting date.

So something like this

 CREATE TABLE all_days (  
  dated DATETIME,  
  day_state CHAR(1)  
  )

Where day_state is value of
D - Working Day
W - Weekend
B - Bank Holiday

The SQL to find the date after x working days then becomes

SELECT MAX(dated)
FROM (
  SELECT TOP(@number_days) dated
  FROM all_days
  WHERE day_state = 'D'
  AND dated >= @start_date
  ORDER by dated ASC
)

This code is untested but should give you the general idea. You may not want to differentiate between weekends and public holidays in which case you could rename the day_state to working_day and make it a BIT field.

You should create a composite unique index on dated and day_state.

Steve Weet
This is it, and the old version works like mentioned. Unfortunetly the new requirement is to only house bad dates and not the good ones.
Dustin Laine
Maybe whoever wrote that design spec should check on SO - we'll award them 20 WTF points.
le dorfier
I think the requirement is the same as it always was, but someone wants to see a different implementation, presumably because of the pain in the arse of maintaining the full days table.
Steve Weet
+2  A: 

I haven't tested this but in theory it should work. I add up the number of days. Then I check if there were any baddates in that range. If there were I add the number of bad days and check if there were anymore baddates in the range I just added. Repeat until no bad dates.

CREATE FUNCTION [dbo].[udf_GetDateInBusinessDays]
(
  @StartDate datetime,  --Start Date
  @NumberDays int           --Good days ahead
)
RETURNS datetime
AS
BEGIN
-- Declare the return variable here
DECLARE @ReturnDate datetime
SET @ReturnDate = dateadd(d, @NumberDays, @StartDate);


DECLARE @d int;
SET @d = (select count(1) from baddates where startdate >= @StartDate and startdate <= @ReturnDate);

declare @t datetime;

WHILE   @d > 0
BEGIN
    set @t = @ReturnDate;
    set @ReturnDate = dateadd(d, @d, @ReturnDate);
    SET @d = (select count(1) from baddates where startdate > @t and startdate <= @ReturnDate);
END

RETURN @ReturnDate
END
dotjoe
+1 this was going to be my next approach
Steve Weet
This worked for me, still not super fast but my query went from over 6 minutes to 1 1/2 minutes in my worst possible scenario. Thanks much!!!!!
Dustin Laine
Your welcome, I'm gonna clean it up a bit. I always forget that an aggregate function will always return a row. So the isnull really isn't needed.
dotjoe