September 28, 2005 at 1:08 am
Yeah Remi is right. The other one is very procedural - not very SQL friendly.
September 28, 2005 at 3:20 am
Gentlemen,
Thanks for the comments.
I don't think RGR'us' solution meets the needs despite being neat code, as usual.
The problem setting was:
"I need to pass a StartDate parameter and count of businessdays. I am trying to write a function to get the enddate."
The procedural code works for any start date and any number of days > 0 without needing [to populate] a calendar table.
As for performance, I tried the following:
------------------------ snip -----------------------------------------------------------------------
-- =============================================
-- Create scalar function (FN)
-- =============================================
IF EXISTS (SELECT *
FROM sysobjects
WHERE name = N'dbo.BusDays2')
DROP FUNCTION dbo.BusDays2
GO
CREATE FUNCTION dbo.BusDays2
(@StartDate DateTime,
@BusinessDays int)
RETURNS DateTime
AS
BEGIN
Declare @EndDate DateTime
DECLARE @Table table (Date smalldatetime not null primary key clustered, BD bit not null)
INSERT INTO @Table (Date, BD) VALUES ('1980-01-01', 1)
INSERT INTO @Table (Date, BD) VALUES ('1980-01-02', 1)
INSERT INTO @Table (Date, BD) VALUES ('1980-01-03', 1)
INSERT INTO @Table (Date, BD) VALUES ('1980-01-04', 1)
INSERT INTO @Table (Date, BD) VALUES ('1980-01-05', 0)
INSERT INTO @Table (Date, BD) VALUES ('1980-01-06', 0)
INSERT INTO @Table (Date, BD) VALUES ('1980-01-07', 1)
INSERT INTO @Table (Date, BD) VALUES ('1980-01-08', 1)
INSERT INTO @Table (Date, BD) VALUES ('1980-01-09', 1)
INSERT INTO @Table (Date, BD) VALUES ('1980-01-10', 1)
INSERT INTO @Table (Date, BD) VALUES ('1980-01-11', 1)
INSERT INTO @Table (Date, BD) VALUES ('1980-01-12', 0)
INSERT INTO @Table (Date, BD) VALUES ('1980-01-13', 0)
INSERT INTO @Table (Date, BD) VALUES ('1980-01-14', 1)
INSERT INTO @Table (Date, BD) VALUES ('1980-01-15', 1)
select @endDate = (
Select Top 1 T1.Date AS Row from @Table T1
INNER JOIN @Table T2 ON T2.Date <=T1.Date AND T1.BD = CAST(1 AS BIT) AND T2.BD = CAST(1 AS BIT)
GROUP BY T1.Date HAVING COUNT(*) <= @BusinessDays
ORDER BY T1.Date desc
)
return @endDate
END
GO
-- =============================================
-- Example to execute function
-- =============================================
declare @cnt int
set @cnt = 0
While @cnt < 1000
Begin
print Cast(dbo.BusDays2('1980/01/01', 10) as Varchar)
set @cnt = @cnt + 1
end
GO
------------------------ snip -----------------------------------------------------------------------
and
------------------------ snip -----------------------------------------------------------------------
-- =============================================
-- Create inline function (IF)
-- =============================================
IF EXISTS (SELECT *
FROM sysobjects
WHERE name = N'dbo.BusDays1')
DROP FUNCTION dbo.BusDays1
GO
CREATE FUNCTION dbo.BusDays1
(@StartDate DateTime,
@NumDays int)
RETURNS DateTime
AS
Begin
Declare @d datetime
Declare @businessDays int
Declare @cnt int
set @d = @StartDate -- start date
set @BusinessDays = @NumDays -- num days
set @cnt = 0
while @cnt < @BusinessDays
Begin
if Not DatePart(dw, @d) in (6, 7) -- careful with Set DateFirst
set @cnt = @cnt + 1
if @cnt < @BusinessDays
set @d = DateAdd(d, 1, @d)
End
return @d
End
GO
-- =============================================
-- Example to execute function
-- =============================================
declare @cnt int
set @cnt = 0
While @cnt < 1000
Begin
print Cast(dbo.BusDays1('1980/1/1', 10) as Varchar)
set @cnt = @cnt + 1
end
GO
------------------------ snip -----------------------------------------------------------------------
Results on my laptop were:
BusDays1 (Procedural) - 1000 lines Jan 14 1980 12:00AM elapsed 0:00:01 (no disk activity)
BusDays2 (SQL) - 1000 lines Jan 14 1980 12:00AM elapsed 0:00:05 (Disk activity)
Hardly time to make the coffee, in either case.
Quote: "Yeah Remi is right. The other one is very procedural - not very SQL friendly."
My point, exactly - Quote "No tables, no SQL - looks like unemployment [mine] looming large "
also no calendar table. I don't see the point of filling the database with dates (most of which are never used) if I can avoid it.
Enjoy your morning coffee
Regards
Otto
Otto Schreibke
The future is a foreign country, they do things differently there.
(Stolen from Arthur C Clarke)
September 28, 2005 at 9:12 am
First of all you are right; My solution didn't provide exactly what was needed (actually was shipping out too much). This opens the door for a real set based solution. And this is also the last part where you are right >>
You are using my code WHILE inserting in a calendar table. That will kill the point of the query to use an existing table and save time that way. I'm sure both solution can work at somewhat the same speed but 5 to 1 (slower) is just a ridiculous number.
Second of all, the calendar table has the advantage of flagging holidays which is also a big concern when talking about business days. While I'm only assuming that this was his intention in the first place, that just makes sens to do it using a perm table with all the dates already flagged so you don't repeat those operations everytime you check for some dates.
Lastly I corrected my version of the code to meet the correct requirements and reran one test. Find the next business day 10 YEARS after the some date. Guess which one is faster by 36 to 1???
--pre clean up
IF EXISTS (select * from dbo.sysobjects where id = object_id(N'[dbo].[BusDays1]') and xtype in (N'FN', N'IF', N'TF'))
drop function [dbo].[BusDays1]
GO
IF EXISTS (select * from dbo.sysobjects where id = object_id(N'[dbo].[fnvwHolidayList]') and xtype in (N'FN', N'IF', N'TF'))
drop function [dbo].[fnvwHolidayList]
GO
IF EXISTS (select * from dbo.sysobjects where id = object_id(N'[dbo].[fnFloatingDate]') and xtype in (N'FN', N'IF', N'TF'))
drop function [dbo].[fnFloatingDate]
GO
IF EXISTS (Select * from dbo.SysObjects where name = 'DemoCalendar' and XType = 'U')
DROP TABLE dbo.DemoCalendar
GO
IF EXISTS (Select * from dbo.SysObjects where name = 'Numbers' and XType = 'U')
DROP TABLE dbo.Numbers
GO
SET QUOTED_IDENTIFIER OFF
GO
SET ANSI_NULLS OFF
GO
CREATE FUNCTION dbo.fnFloatingDate(@Occur as int,@WeekDay as int,@Month as int,@Year as int)
RETURNS SmallDatetime
AS
BEGIN
DECLARE @Result as SmallDatetime
DECLARE @StartDate as SmallDatetime
DECLARE @DayOfWeek as int
SET @StartDate = DATEADD(M, @Month - 1,DATEADD(YYYY, @Year - 1900, 0))--generate the first day of the requested month and year
SET @DayOfWeek = DatePart(dw,@StartDate)
IF @DayOfWeek @weekDay
BEGIN
SET @StartDate = DateAdd(d,@weekDay - @DayOfWeek,@StartDate) -- Adjust to requested day of week
IF MONTH(@StartDate) @Month
BEGIN
SET @StartDate = DATEADD(d, 7, @StartDate)
END
END
SET @Result = DateAdd(wk,@Occur-1,@StartDate)--Adjust to the requested week of the month
RETURN @Result
END
GO
SET QUOTED_IDENTIFIER OFF
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER OFF
GO
SET ANSI_NULLS OFF
GO
CREATE FUNCTION [dbo].[fnvwHolidayList] (@nYear as int)
RETURNS @Holidays TABLE
(
Holiday_date SmallDatetime PRIMARY KEY,
Holiday_name Varchar(32),
CCQ_Only bit
)
AS
BEGIN
-- Calculate Easter Sunday
DECLARE @g as int
DECLARE @C as int
DECLARE @h as int
DECLARE @i as int
DECLARE @j-2 as int
DECLARE @l as int
DECLARE @Month as int
DECLARE @Day as int
DECLARE @Easter as SmallDatetime
DECLARE @WorkDT as SmallDatetime
DECLARE @FeteDollard as SmallDatetime
------------------------------------------------------------------------------------------------
-- Bizarre Algorithm to determine Easter Sunday
SET @g = @nYear % 19
SET @C = @nYear / 100
SET @h = ((@c - (@c / 4) - ((8 * @C + 13) / 25) + (19 * @g) + 15) % 30)
SET @i = @h - ((@h / 28) * (1 - (@h /28) * (29 / (@h + 1)) * ((21 - @g) / 11)))
SET @j-2 = ((@nYear + (@nYear / 4) + @i + 2 - @C + (@c / 4)) % 7)
SET @l = @i - @j-2
SET @Month = 3 + ((@l + 40) / 44)
SET @Day = @l + 28 - (31 * (@Month / 4))
SET @Easter = CAST(@Month AS VARCHAR(2)) + '/' + CAST(@Day As VARCHAR(2)) + '/' + CAST(@nYear AS VARCHAR(4))
-- Add Easter Sunday to holiday list, and get holidays based around Easter
INSERT INTO @Holidays (Holiday_name,Holiday_date, CCQ_Only) VALUES ('Lundi de Pâques',DateAdd(d,1,@Easter), 0)
INSERT INTO @Holidays (Holiday_name,Holiday_date, CCQ_Only) VALUES ('Vendredi Saint',DateAdd(d,-2,@Easter), 0)
-------------------------------------------------------------------------------------------------
-- Fixed date holidays are loaded next
INSERT INTO @Holidays (Holiday_name,Holiday_date, CCQ_Only) VALUES ('Nouvel An (1er)',CONVERT(SmallDateTime,'1/1/'+CAST(@nYear AS VARCHAR(4))), 0)
INSERT INTO @Holidays (Holiday_name,Holiday_date, CCQ_Only) VALUES ('Nouvel An (2)',CONVERT(SmallDateTime,'1/2/'+CAST(@nYear AS VARCHAR(4))), 0)
INSERT INTO @Holidays (Holiday_name,Holiday_date, CCQ_Only) VALUES ('Fête Nationale',CONVERT(SmallDateTime,'06/24/'+CAST(@nYear AS VARCHAR(4))), 0)
INSERT INTO @Holidays (Holiday_name,Holiday_date, CCQ_Only) VALUES ('Fête du Canada',CONVERT(SmallDateTime,'07/01/'+CAST(@nYear AS VARCHAR(4))), 0)
INSERT INTO @Holidays (Holiday_name,Holiday_date, CCQ_Only) VALUES ('Jour du Souvenir',CONVERT(SmallDateTime,'11/11/'+CAST(@nYear AS VARCHAR(4))), 1)
INSERT INTO @Holidays (Holiday_name,Holiday_date, CCQ_Only) VALUES ('Noël (25)',CONVERT(SmallDateTime,'12/25/'+CAST(@nYear AS VARCHAR(4))), 0)
INSERT INTO @Holidays (Holiday_name,Holiday_date, CCQ_Only) VALUES ('Noël (26)',CONVERT(SmallDateTime,'12/26/'+CAST(@nYear AS VARCHAR(4))), 0)
-- Holidays that fall on the same day of the week (based on the year they were officially established)
INSERT INTO @Holidays (Holiday_name,Holiday_date, CCQ_Only) VALUES ('Fête du travail',dbo.fnFloatingDate(1,1,9,@nYear), 0) -- 1er lundi de septembre
INSERT INTO @Holidays (Holiday_name,Holiday_date, CCQ_Only) VALUES ('Action de grâce',dbo.fnFloatingDate(2,1,10,@nYear), 0) -- 2èm lundi d'octobre
SET @FeteDollard = dbo.fnFloatingDate(4,1,5,@nYear)-- 4èm lundi de mai
--la fête de dollard est le premier lundi AVANT ( 25
BEGIN
SET @FeteDollard = DATEADD(d, -7, @FeteDollard)
END
INSERT INTO @Holidays (Holiday_name,Holiday_date, CCQ_Only) VALUES ('Fête de Dollard', @FeteDollard, 0)
RETURN --@Holidays
END
GO
SET QUOTED_IDENTIFIER OFF
GO
SET ANSI_NULLS ON
GO
CREATE FUNCTION dbo.BusDays1
(@StartDate DateTime,
@NumDays int)
RETURNS DateTime
AS
Begin
Declare @d datetime
Declare @businessDays int
Declare @cnt int
set @d = @StartDate -- start date
set @BusinessDays = @NumDays -- num days
set @cnt = 0
while @cnt < @BusinessDays
Begin
if Not DatePart(dw, @d) in (6, 7) -- careful with Set DateFirst
set @cnt = @cnt + 1
if @cnt < @BusinessDays
set @d = DateAdd(d, 1, @d)
End
return @d
End
GO
CREATE TABLE dbo.Numbers (PkNumber int identity(1,1) primary key clustered, dude bit null)
GO
INSERT INTO dbo.Numbers (dude)
SELECT NULL FROM (SELECT TOP 100 NULL AS A FROM master.dbo.spt_values) dt100 cross join (SELECT TOP 80 null AS A FROM master.dbo.spt_values) dt80
GO
ALTER TABLE dbo.Numbers
DROP COLUMN dude
GO
--recreate the calendar table
CREATE TABLE dbo.DemoCalendar (Date smalldatetime not null primary key clustered, BD bit not null)
GO
--fill the table with 24K Dates
INSERT INTO dbo.DemoCalendar (Date, BD)
SELECT DATEADD(D, PkNumber, '1979/12/31') AS Date, CASE WHEN DatePart(dw, DATEADD(D, PkNumber, '1979/12/31')) in (6, 7) THEN 0 ELSE 1 END AS BD FROM dbo.Numbers
UNION ALL
SELECT DATEADD(D, PkNumber, '2001/11/25') AS Date, CASE WHEN DatePart(dw, DATEADD(D, PkNumber, '2001/11/25')) in (6, 7) THEN 0 ELSE 1 END AS BD FROM dbo.Numbers
UNION ALL
SELECT DATEADD(D, PkNumber, '2023/10/21') AS Date, CASE WHEN DatePart(dw, DATEADD(D, PkNumber, '2001/11/25')) in (6, 7) THEN 0 ELSE 1 END AS BD FROM dbo.Numbers
GO
--Update all the holidays for the whole table
DECLARE @Years AS INT
SET @Years = 1980
WHILE @Years = @StartDate AND C.BD = CAST(1 AS BIT) ORDER BY C.Date
SET ROWCOUNT 0
Select @Date AS TheDay where @Count = @BusinessDays
GO
/*
this is where the speed testing can start
*/
declare @cnt int
set @cnt = 0
While @cnt < 100
Begin
print dbo.BusDays1('1980/1/1', 2480)
set @cnt = @cnt + 1
end
GO
--36 seconds
DECLARE @BusinessDays AS INT
DECLARE @Date AS DATETIME
DECLARE @StartDateAS DATETIME
DECLARE @CountAS INT
SET @BusinessDays = 2480 --10 years worth of business days
--SET @StartDate = DATEADD(D, 0, DATEDIFF(D, 0, GetDate()))
SET @StartDate = '1980/01/01'
declare @cnt int
set @cnt = 0
While @cnt = @StartDate AND C.BD = CAST(1 AS BIT) ORDER BY C.Date
SET ROWCOUNT 0
IF @Count = @BusinessDays
print @Date
set @cnt = @cnt + 1
end
--1 sec
/*
--clean up
DROP TABLE dbo.Numbers
DROP TABLE dbo.DemoCalendar
DROP FUNCTION dbo.fnvwHolidayList
DROP FUNCTION dbo.fnFloatingDate
DROP FUNCTION dbo.BusDays1
*/
September 28, 2005 at 9:32 am
Oh, I didn't turn up after. I got my function this way.
CREATE FUNCTION DBO.BUSINESS_DATEADD (@STARTDATE DATETIME, @DAYS BIGINT )
RETURNS DATETIME
AS
BEGIN
DECLARE @ENDDATE DATETIME
SET @ENDDATE = (SELECT DD FROM(SELECT T1.BDATE AS DD, COUNT(*) AS ROW FROM NAT_BUSINESSCALENDAR T1
INNER JOIN NAT_BUSINESSCALENDAR T2 ON T2.BDATE <=T1.BDATE AND T1.BUSINESS_DAY = 1 AND T2.BUSINESS_DAY = 1
WHERE T1.BDATE > CONVERT(VARCHAR(10),@STARTDATE,101) AND
T2.BDATE > CONVERT(VARCHAR(10),@STARTDATE,101)
GROUP BY T1.BDATE HAVING COUNT(*) <= @DAYS
)TT WHERE ROW = @DAYS)
RETURN @ENDDATE
END
September 28, 2005 at 9:41 am
Check my latest version, it'll perfom faster than the first one I did. Make sure understand the where clause in the final select... it's the validation part and it shouldn't be ignored .
September 29, 2005 at 8:27 am
Hi Remi,
is there any way that I can make this function work for negative intigers too?suppose if I say dbo.business_dateadd(getdate(), -3), then I should be able to get 09/26/05. Thanks for any help.
September 29, 2005 at 9:00 am
Ya, just make 2 different functions. One with the order by desc and <= @Date and the other one like I did. Cal the one depending on the number being negative. It could be done in a single statement but I would think that performance would suffer from this.
September 29, 2005 at 9:18 am
I am unable to use the SET ROWCOUNT with in a function Remi, getting this error.
Server: Msg 443, Level 16, State 2, Procedure BUSINESS_DATEADD1, Line 12
Invalid use of 'UNKNOWN TOKEN' within a function.
September 29, 2005 at 4:50 pm
Forgot that one... can't use set options in functions... any way you can make this a proc?
October 3, 2005 at 2:49 pm
I don't think that I can make it a procedure as I need to use this function to update a date field in a table. I tried to make it a procedure but it failed when I tried to execute this procedure for (getdate(), 10) .Also procedure doesn't take the column names like I can't do
select(exec proc1(datecolumn, 10) ) from tab1
October 3, 2005 at 2:58 pm
K, go back to my original solution or repost the actual needs you have so that you can update all those rows in a single pass.
October 3, 2005 at 3:20 pm
The nat_BusinessCalendar table looks like this as you know.
BDate BUSINESS_DAY
--------------------------------------- ------------ ----------- ---
1980-01-01 00:00:00.000 1
1980-01-02 00:00:00.000 1
1980-01-03 00:00:00.000 1
1980-01-04 00:00:00.000 1
1980-01-05 00:00:00.000 0
1980-01-06 00:00:00.000 0
1980-01-07 00:00:00.000 1
1980-01-08 00:00:00.000 1
1980-01-09 00:00:00.000 1
This is the function I created
CREATE FUNCTION DBO.BUSINESS_DATE_ADD (@STARTDATE DATETIME, @DAYS BIGINT )
RETURNS DATETIME
AS
BEGIN
DECLARE @ENDDATE DATETIME
SET @ENDDATE = (SELECT DD FROM(SELECT T1.BDATE AS DD, COUNT(*) AS ROW FROM NAT_BUSINESSCALENDAR T1
INNER JOIN NAT_BUSINESSCALENDAR T2 ON T2.BDATE <=T1.BDATE AND T1.BUSINESS_DAY = 1 AND T2.BUSINESS_DAY = 1
WHERE T1.BDATE > CONVERT(VARCHAR(10),@STARTDATE,101) AND
T2.BDATE > CONVERT(VARCHAR(10),@STARTDATE,101)
GROUP BY T1.BDATE HAVING COUNT(*) <= @DAYS
)TT WHERE ROW = @DAYS)
RETURN @ENDDATE
END
I am trying to run this query using this function.
update Tab_inventory set t_date = DBO.BUSINESS_DATE_ADD(getdate(),10),
set t_1st_letter = DBO.BUSINESS_DATE_ADD(t_1st_letter,10)
The above query returns around 3000 rows and takes almost 100 secs to update.
October 3, 2005 at 10:08 pm
if you run it like that,you can run the query once the fetch the value into a variable, then use the variable in the update. I thaught that this number was dynamic in some way...
Viewing 13 posts - 16 through 27 (of 27 total)
You must be logged in to reply to this topic. Login to reply