Calendar Table and Date/Time Functions

I frequently see questions in the forums and newsgroups about how to best query date/time data and perform date manipulation.  Let me first say that a permanent calendar table that materializes commonly used DATEPART values along with time periods you frequently use is invaluable.  I’ve used such a table for over a decade with great success and strongly recommend you implement one on all of your database servers.  I’ve included a sample calendar table (and numbers table) later in this post and you can find other variations of such a table via an internet search.

Removing the Time Portion

A common requirement I have is to remove the time portion from a date/time value.  This is easy in SQL 2008 since you can simply “CAST(SomeDateTimeValue AS date)”.  But the date data type is not available in older SQL Server versions so you need an alternate method.  In SQL 2005 and earlier versions, I recommend the DATEADD…DATEDIFF method below with an arbitrary base date value specified in a format that is independent of the session DATAFORMAT setting:

SELECT CAST(GETDATE() AS date); --SQL 2008 and later

SELECT DATEADD(day, DATEDIFF(day, '19000101', GETDATE()), '19000101'); --SQL 2005 and earlier

 

I often see a variation of the DATEADD…DATEDIFF technique with the integer zero (no quotes) specified as the base date.  Although this may provide the expected results (I’ve done it myself), I caution against it because it relies on implicit conversion from the internal SQL Server integer date/time storage format.  If you want to be concise, a better approach is to specify an empty string for the base date value since the default value is ‘1900-01-01 00:00:00’.  In my opinion, an explicit data value is more intuitive, though.

SELECT DATEADD(day, DATEDIFF(day, '', GETDATE()), '');

 

I also sometimes see code that extracts the year, month and day date parts and concatenates with separators.  However, that method is dependent on session DATEFORMAT settings and slower than other methods.  See Tibor Karaszi’s The ultimate guide to the datetime datatypes article for details.

First and Last Day of Period

Another common task is to determine the first or last day of a given period.  The script below shows how to accomplish this of you don’t have a calendar table with the calculated values available.

DECLARE @Date date = GETDATE();

SELECT 'First day of year' [DateDescription], DATEADD(year, DATEDIFF(year,'19000101',@Date), '19000101') AS [CalendarDate]

UNION ALL

SELECT 'Last day of year', DATEADD(day,-1,DATEADD(year,0,DATEADD(year,DATEDIFF(year,'19000101',@Date)+1,'19000101')))

UNION ALL

SELECT 'First day of month', DATEADD(month, DATEDIFF(month,'19000101',@Date), '19000101')

UNION ALL

SELECT 'Last day of month', DATEADD(day,-1,DATEADD(month,0,DATEADD(month,DATEDIFF(month,'19000101',@Date)+1,'19000101')))

UNION ALL

SELECT 'First day week (based on DATEFIRST setting)', DATEADD(day,-(DATEPART(weekday ,@Date)-1),DATEDIFF(day,'19000101', @Date))

UNION ALL

SELECT 'Last day of week (based on DATEFIRST setting)', DATEADD(day,-(DATEPART(weekday ,@Date)-1)+6,DATEDIFF(day,'19000101', @Date));

 

With a calendar table like the one later in this post:

DECLARE @Date date = GETDATE();

SELECT 'First day of year' [DateDescription], (SELECT FirstDateOfYear FROM dbo.Calendar WHERE CalendarDate = @Date)

UNION ALL

SELECT 'Last day of year', (SELECT LastDateOfYear FROM dbo.Calendar WHERE CalendarDate = @Date)

UNION ALL

SELECT 'First day of month', (SELECT FirstDateOfMonth FROM dbo.Calendar WHERE CalendarDate = @Date)

UNION ALL

SELECT 'Last day of month', (SELECT LastDateOfMonth FROM dbo.Calendar WHERE CalendarDate = @Date)

UNION ALL

SELECT 'First day week (based on DATEFIRST setting)', (SELECT FirstDateOfWeek FROM dbo.Calendar WHERE CalendarDate = @Date)

UNION ALL

SELECT 'Last day of week (based on DATEFIRST setting)', (SELECT LastDateOfWeek FROM dbo.Calendar WHERE CalendarDate = @Date);

 

Calendar and Numbers Table

I think auxiliary calendar and number tables are a must-have on every database server.  These objects allow you to easily perform set-based processing in a number of scenarios.  In fact, the calendar table population script below uses a numbers table to populate the calendar table with several thousand rows in under a second.  This is much more efficient that a WHILE loop.

This calendar table population script also updates the table with most US holidays and adjusts business/non-business days accordingly.  In addition to customizing the script for holidays as observed by your organization, you might add fiscal period start/end dates to facilitate querying based on those cycles.  Also consider creating user-defined functions or stored procedures to encapsulate frequently used code that uses the calendar table.  For example, here is a function that returns the date that is a specified number of business days from the date provided:

CREATE FUNCTION dbo.udf_AddBusinessDays

(@Date date, @BusinessDays int)

RETURNS date

AS

BEGIN

      RETURN (

            SELECT TOP (1) CalendarDate AS BusinessDate

            FROM (SELECT TOP (@BusinessDays) CalendarDate

                  FROM dbo.Calendar

                  WHERE

                        CalendarDate > @Date

                        AND BusinessDay = 1

                  ORDER BY CalendarDate) AS BusinessDays

            ORDER BY CalendarDate DESC

      )

END

GO

Script 1: Example calendar table utility function

--auxiliary number table

CREATE TABLE dbo.Numbers(

      Number int NOT NULL

            CONSTRAINT PK_Numbers PRIMARY KEY CLUSTERED

      );

     

--load Numbers table with 1,000,000 numbers

WITH t1 AS (SELECT 0 AS n UNION ALL SELECT 0 UNION ALL SELECT 0 UNION ALL SELECT 0)

      ,t2 AS (SELECT 0 AS n FROM t1 t1a, t1 t1b, t1 t1c, t1 t1d)

      ,t3 AS (SELECT 0 AS n FROM t2 t2a, t2 t2b, t2 t2c)

      ,numbers AS (SELECT ROW_NUMBER() OVER(ORDER BY n) - 1 AS number FROM t3)

      INSERT INTO dbo.Numbers WITH (TABLOCKX) (

            Number

            )

            SELECT number

            FROM numbers

                                                                                                                                                                                                                        WHERE number < 1000000;                                                                                    

Script 2: Create and populate numbers table

CREATE TABLE dbo.Calendar(

      CalendarDate date NOT NULL

            CONSTRAINT PK_Calendar PRIMARY KEY CLUSTERED

      ,CalendarYear int NOT NULL

      ,CalendarMonth int NOT NULL

      ,CalendarDay int NOT NULL

      ,DayOfWeekName varchar(10) NOT NULL

      ,FirstDateOfWeek date NOT NULL

      ,LastDateOfWeek date NOT NULL

      ,FirstDateOfMonth date NOT NULL

      ,LastDateOfMonth date NOT NULL

      ,FirstDateOfQuarter date NOT NULL

      ,LastDateOfQuarter date NOT NULL

      ,FirstDateOfYear date NOT NULL

      ,LastDateOfYear date NOT NULL

      ,BusinessDay bit NOT NULL

      ,NonBusinessDay bit NOT NULL

      ,Weekend bit NOT NULL

      ,Holiday bit NOT NULL

      ,Weekday bit NOT NULL

      ,CalendarDateDescription varchar(50) NULL

);

GO

 

--load dates 2000-01-01 through 2025-12-31

WITH t1 AS (SELECT 0 AS n UNION ALL SELECT 0 UNION ALL SELECT 0 UNION ALL SELECT 0)

      ,t2 AS (SELECT 0 AS n FROM t1 t1a, t1 t1b, t1 t1c, t1 t1d)

      ,t3 AS (SELECT 0 AS n FROM t2 t2a, t2 t2b)

      ,numbers AS (SELECT ROW_NUMBER() OVER(ORDER BY n) - 1 AS number FROM t3)

INSERT INTO dbo.Calendar WITH (TABLOCKX) (

      CalendarDate

      ,CalendarYear

      ,CalendarMonth

      ,CalendarDay

      ,DayOfWeekName

      ,FirstDateOfWeek

      ,LastDateOfWeek

      ,FirstDateOfMonth

      ,LastDateOfMonth

      ,FirstDateOfQuarter

      ,LastDateOfQuarter

      ,FirstDateOfYear

      ,LastDateOfYear

      ,BusinessDay

      ,NonBusinessDay

      ,Weekend

      ,Holiday

      ,Weekday

      ,CalendarDateDescription

      )

SELECT

      CalendarDate = DATEADD(day, number, '20000101')

      ,CalendarYear = DATEPART(year, DATEADD(day, number, '20000101'))

      ,CalendarMonth = DATEPART(month, DATEADD(day, number, '20000101'))

      ,CalendarDay = DATEPART(day, DATEADD(day, number, '20000101'))

      ,DayOfWeekName = DATENAME(weekday, DATEADD(day, number, '20000101'))

      ,FirstDateOfWeek = DATEADD(day,-(DATEPART(weekday ,DATEADD(day, number, '20000101'))-1),DATEADD(day, number, '20000101'))

      ,LastDateOfWeek = DATEADD(day,-(DATEPART(weekday ,DATEADD(day, number, '20000101'))-1)+6,DATEADD(day, number, '20000101'))

      ,FirstDateOfMonth = DATEADD(month, DATEDIFF(month,'20000101',DATEADD(day, number, '20000101')), '20000101')

      ,LastDateOfMonth = DATEADD(day,-1,DATEADD(month,0,DATEADD(month,DATEDIFF(month,'20000101',DATEADD(day, number, '20000101'))+1,'20000101')))

      ,FirstDateOfQuarter = DATEADD(quarter, DATEDIFF(quarter,'20000101',DATEADD(day, number, '20000101')), '20000101')

      ,LastDateOfQuarter = DATEADD(day, -1, DATEADD(quarter, DATEDIFF(quarter,'20000101',DATEADD(day, number, '20000101'))+1, '20000101'))

      ,FirstDateOfYear = DATEADD(year, DATEDIFF(year,'20000101',DATEADD(day, number, '20000101')), '20000101')

      ,LastDateOfYear = DATEADD(day,-1,DATEADD(year, DATEDIFF(year,'20000101',DATEADD(day, number, '20000101'))+1, '20000101'))

      --initially set all weekdays as business days

      ,BusinessDay = CASE WHEN DATENAME(weekday, DATEADD(day, number, '20000101')) IN('Monday','Tuesday','Wednesday','Thursday','Friday') THEN 1 ELSE 0 END

      --initially set only weekends as non-business days

      ,NonBusinessDay = CASE WHEN DATENAME(weekday, DATEADD(day, number, '20000101')) IN('Saturday','Sunday') THEN 1 ELSE 0 END

      ,Weekend = CASE WHEN DATENAME(weekday, DATEADD(day, number, '20000101')) IN('Saturday','Sunday') THEN 1 ELSE 0 END

      ,Holiday = 0 --initially no holidays

      ,Weekday = CASE WHEN DATENAME(weekday, DATEADD(day, number, '20000101')) IN('Monday','Tuesday','Wednesday','Thursday','Friday') THEN 1 ELSE 0 END

      ,CalendarDateDescription = NULL

FROM numbers

WHERE number < 9497;

 

--New Year's Day

UPDATE dbo.calendar

SET

      BusinessDay = 0

      ,NonBusinessDay = 1

      ,Holiday = 1

    ,CalendarDateDescription = 'New Year''s Day'

WHERE

    CalendarMonth = 1

    AND CalendarDay = 1;

 

--New Year's Day celebrated on Friday, December 31 when January 1 falls on Saturday

UPDATE dbo.Calendar

SET

      BusinessDay = 0

      ,NonBusinessDay = 1

    ,CalendarDateDescription = 'New Year''s Day Celebrated'

WHERE

    CalendarMonth = 12

    AND CalendarDay = 31

    AND DayOfWeekName = 'Friday';

   

--New Year's Day celebrated on Monday, January 2 when January 1 falls on Sunday

UPDATE dbo.Calendar

SET

      BusinessDay = 0

      ,NonBusinessDay = 1

    ,CalendarDateDescription = 'New Year''s Day Celebrated'

WHERE

    CalendarMonth = 1

    AND CalendarDay = 2

    AND DayOfWeekName = 'Monday';   

 

--Martin Luther King Day - 3rd Monday in January

UPDATE dbo.Calendar

SET

      BusinessDay = 0

      ,NonBusinessDay = 1

      ,Holiday = 1

    ,CalendarDateDescription = 'Martin Luther King Day'

WHERE

    CalendarMonth = 1

    AND DayOfWeekName = 'Monday'

    AND (SELECT COUNT(*)

            FROM dbo.Calendar c2

        WHERE

            c2.CalendarDate <= Calendar.CalendarDate

            AND c2.CalendarYear = Calendar.CalendarYear

            AND c2.CalendarMonth = Calendar.CalendarMonth

            AND c2.DayOfWeekName = 'Monday'

        ) = 3;

 

--President's Day - 3rd Monday in February

UPDATE dbo.Calendar

SET

      BusinessDay = 0

      ,NonBusinessDay = 1

      ,Holiday = 1

    ,CalendarDateDescription = 'President''s Day'

WHERE

    CalendarMonth = 2

    AND DayOfWeekName = 'Monday'

    AND (SELECT COUNT(*)

            FROM dbo.Calendar c2

        WHERE

            c2.CalendarDate <= Calendar.CalendarDate

            AND c2.CalendarYear = Calendar.CalendarYear

            AND c2.CalendarMonth = Calendar.CalendarMonth

            AND c2.DayOfWeekName = 'Monday'

        ) = 3;

       

--Easter - first Sunday after the full moon following the vernal (March 21) equinox

UPDATE dbo.Calendar

SET

      Holiday = 1

    ,CalendarDateDescription = 'Easter'

WHERE

    CalendarDate IN(

        '20000423'

        ,'20010415'

        ,'20020331'

        ,'20030420'

        ,'20040411'

        ,'20050327'

        ,'20060416'

        ,'20070408'

        ,'20080323'

        ,'20090412'

        ,'20100404'

        ,'20110424'

        ,'20120408'

        ,'20130331'

        ,'20140420'

        ,'20150405'

        ,'20160427'

        ,'20170416'

        ,'20180401'

        ,'20190421'

        ,'20200412'

        ,'20210404'

        ,'20220417'

        ,'20230409'

        ,'20240331'

        ,'20250420'

    );

 

--Good Friday - 2 days before Easter Sunday

UPDATE dbo.Calendar

SET

      BusinessDay = 0

      ,NonBusinessDay = 1

      ,Holiday = 1

    ,CalendarDateDescription = 'Good Friday'

WHERE

    CalendarDate IN(

        SELECT DATEADD(day, -2, c2.CalendarDate)

        FROM dbo.Calendar c2

        WHERE c2.CalendarDateDescription = 'Easter'

        );

 

--Memorial Day - last Monday in May

UPDATE dbo.Calendar

SET

      BusinessDay = 0

      ,NonBusinessDay = 1

      ,Holiday = 1

    ,CalendarDateDescription = 'Memorial Day'

WHERE

    CalendarMonth = 5

    AND DayOfWeekName = 'Monday'

    AND CalendarDate IN(

        SELECT MAX(c2.CalendarDate)

        FROM dbo.Calendar c2

        WHERE

            c2.CalendarYear = Calendar.CalendarYear

            AND c2.CalendarMonth = 5

            AND c2.DayOfWeekName = 'Monday'

        );

 

--Independence Day - July 4th

UPDATE dbo.Calendar

SET

      BusinessDay = 0

      ,NonBusinessDay = 1

      ,Holiday = 1

    ,CalendarDateDescription = 'Independence Day'

WHERE

    CalendarMonth = 7

    AND CalendarDay = 4;

 

--Independence Day celebrated on Friday, July 3 when July 4 falls on a Saturday

UPDATE dbo.Calendar

SET

      BusinessDay = 0

      ,NonBusinessDay = 1

    ,CalendarDateDescription = 'Independence Day Celebrated'

WHERE

    CalendarMonth = 7

    AND CalendarDay = 3

    AND DayOfWeekName = 'Friday';

 

--Independence Day celebrated on Friday, July 3 when July 4 falls on a Saturday

UPDATE dbo.Calendar

SET

      BusinessDay = 0

      ,NonBusinessDay = 1

    ,CalendarDateDescription = 'Independence Day Celebrated'

WHERE

    CalendarMonth = 7

    AND CalendarDay = 5

    AND DayOfWeekName = 'Monday';

       

--Labor Day - first Monday in September

UPDATE dbo.Calendar

SET

      BusinessDay = 0

      ,NonBusinessDay = 1

      ,Holiday = 1

    ,CalendarDateDescription = 'Labor Day'

WHERE

    CalendarMonth = 9

    AND DayOfWeekName = 'Monday'

    AND CalendarDate IN(

        SELECT MIN(c2.CalendarDate)

        FROM dbo.Calendar c2

        WHERE

            c2.CalendarYear = calendar.CalendarYear

            AND c2.CalendarMonth = 9

            AND c2.DayOfWeekName = 'Monday'

        );

 

--Columbus Day - second Monday in October

UPDATE dbo.Calendar

SET

      Holiday = 1

    ,CalendarDateDescription = 'Columbus Day'

WHERE

    CalendarMonth = 10

    AND DayOfWeekName = 'Monday'

    AND (SELECT COUNT(*)

            FROM dbo.Calendar c2

        WHERE

            c2.CalendarDate <= Calendar.CalendarDate

            AND c2.CalendarYear = Calendar.CalendarYear

            AND c2.CalendarMonth = Calendar.CalendarMonth

            AND c2.DayOfWeekName = 'Monday'

        ) = 2;

 

--Veteran's Day - November 11

UPDATE dbo.Calendar

SET

      Holiday = 1

    ,CalendarDateDescription = 'Veteran''s Day'

WHERE

    CalendarMonth = 11

    AND CalendarDay = 11;

 

--Thanksgiving - fourth Thursday in November

UPDATE dbo.Calendar

SET

      BusinessDay = 0

      ,NonBusinessDay = 1

      ,Holiday = 1

    ,CalendarDateDescription = 'Thanksgiving'

WHERE

    CalendarMonth = 11

    AND DayOfWeekName = 'Thursday'

 

    AND (SELECT COUNT(*) FROM

            dbo.Calendar c2

        WHERE

            c2.CalendarDate <= Calendar.CalendarDate

            AND c2.CalendarYear = Calendar.CalendarYear

            AND c2.CalendarMonth = Calendar.CalendarMonth

            AND c2.DayOfWeekName = 'Thursday'

        ) = 4;

       

--Day after Thanksgiving - fourth Friday in November

UPDATE dbo.Calendar

SET

      BusinessDay = 0

      ,NonBusinessDay = 1

      ,Holiday = 1

    ,CalendarDateDescription = 'Day after Thanksgiving'

WHERE

    CalendarMonth = 11

    AND DayOfWeekName = 'Friday'

    AND (SELECT COUNT(*)

            FROM dbo.Calendar c2

        WHERE

            c2.CalendarDate <= Calendar.CalendarDate

            AND c2.CalendarYear = Calendar.CalendarYear

            AND c2.CalendarMonth = Calendar.CalendarMonth

            AND c2.DayOfWeekName = 'Friday'

        ) = 4;       

       

--Christmas Day - December 25th

UPDATE dbo.Calendar

SET

      BusinessDay = 0

      ,NonBusinessDay = 1

      ,Holiday = 1

    ,CalendarDateDescription = 'Christmas Day'

WHERE

    CalendarMonth = 12

    AND CalendarDay = 25;

 

--Christmas day celebrated on Friday, December 24 when December 25 falls on a Saturday

UPDATE dbo.Calendar

SET

      BusinessDay = 0

      ,NonBusinessDay = 1

    ,CalendarDateDescription = 'Christmas Day Celebrated'

WHERE

    CalendarMonth = 12

    AND CalendarDay = 24

    AND DayOfWeekName = 'Friday';

 

--Christmas day celebrated on Monday, December 24 when December 25 falls on a Sunday

UPDATE dbo.Calendar

SET

      BusinessDay = 0

      ,NonBusinessDay = 1

    ,CalendarDateDescription = 'Christmas Day Celebrated'

WHERE

    CalendarMonth = 12

    AND CalendarDay = 26

    AND DayOfWeekName = 'Monday';

Script 3: Create and populate calendar table and update with holidays

 

posted @ Monday, July 19, 2010 7:58 PM

Print

Comments on this entry:

# re: Calendar Table and Date/Time Functions

Left by MrDee at 7/20/2010 4:15 PM
Gravatar
Brilliant - Thank you!
The 'Removing the Time Portion' solution is sheer elegance.

# re: Calendar Table and Date/Time Functions

Left by Chirag Shah at 9/9/2010 8:19 AM
Gravatar
Dan, this is an excellent post on the subject.

If you could elaborate various use of numbers table to solve some T-SQL problems (may be on a next blog post)

Thanks!

# re: Calendar Table and Date/Time Functions

Left by Ramjet at 6/30/2012 8:23 AM
Gravatar
Good Morning Mr. Guzman,
I've reference your calendar script before (It's a favorite) but it used to have 5-10 (don't remember exactly) common functions listed as well. Am I remembering incorrectly?

JB

# re: Calendar Table and Date/Time Functions

Left by guzmanda at 6/30/2012 12:24 PM
Gravatar
I included only a few sample queries in my post so you might be thinking of another site or article. If you need help developing other ones you need, let me know your requirements and I'll try to help.

# re: Calendar Table and Date/Time Functions

Left by jamin at 8/24/2012 2:25 AM
Gravatar
great stuff - very helpful!
Comments have been closed on this topic.