Creating a date dimension or calendar table in SQL Server

By:   |   Updated: 2018-07-24   |   Comments (53)   |   Related: 1 | 2 | 3 | More > Dates


A calendar table can be immensely useful, particularly for reporting purposes, and for determining things like business days between two dates. I often see people struggling with manually populating a calendar or date dimension table; usually there are lots of loops and iterative code constructs being used. In this tip I will show you how to build and use a calendar table.


I build calendar tables all the time, for a variety of business applications, and have come up with a few ways to handle things. Sharing them here will hopefully prevent you from re-inventing any wheels when populating your own tables.

One of the biggest objections I hear to calendar tables is that people don't want to create a table. I can't stress enough how cheap a table can be in terms of size and memory usage, especially as storage continues to be larger and faster, compared to using all kinds of functions to determine date-related information on every single query. The table I create below probably has a lot more materialized columns than you would ever need, but it takes a whopping 1.29 MB on disk and in memory (that covers 20 years; 30 years would be 1.86 MB, and 50 years would be 3.08 MB). That will go up as you implement additional indexes, but still represents an extremely negligible impact in most systems.

I also always explicitly set things like DATEFORMAT, DATEFIRST, and LANGUAGE to avoid ambiguity, default to English for month and day names, and assume that quarters for the fiscal year align with the calendar year. You may need to change some of these things depending on your display language, your fiscal year, and other factors.

This is a one-time population, so I'm not worried about the costs of using intermediate storage like temp tables. I like to materialize all of the columns to disk, rather than rely on computed columns, since the table becomes read-only after initial population. So I'm going to do a lot of those calculations during the initial population of the #temp table:

DECLARE @StartDate DATE = '20000101', @NumberOfYears INT = 30;

-- prevent set or regional settings from interfering with 
-- interpretation of dates / literals


DECLARE @CutoffDate DATE = DATEADD(YEAR, @NumberOfYears, @StartDate);

-- this is just a holding table for intermediate calculations:

  [date]       DATE PRIMARY KEY, 
  [day]        AS DATEPART(DAY,      [date]),
  [month]      AS DATEPART(MONTH,    [date]),
  [MonthName]  AS DATENAME(MONTH,    [date]),
  [week]       AS DATEPART(WEEK,     [date]),
  [ISOweek]    AS DATEPART(ISO_WEEK, [date]),
  [DayOfWeek]  AS DATEPART(WEEKDAY,  [date]),
  [quarter]    AS DATEPART(QUARTER,  [date]),
  [year]       AS DATEPART(YEAR,     [date]),
  FirstOfYear  AS CONVERT(DATE, DATEADD(YEAR,  DATEDIFF(YEAR,  0, [date]), 0)),
  Style112     AS CONVERT(CHAR(8),   [date], 112),
  Style101     AS CONVERT(CHAR(10),  [date], 101)

-- use the catalog views to generate as many rows as we need

INSERT #dim([date]) 
  SELECT d = DATEADD(DAY, rn - 1, @StartDate)
    SELECT TOP (DATEDIFF(DAY, @StartDate, @CutoffDate)) 
      rn = ROW_NUMBER() OVER (ORDER BY s1.[object_id])
    FROM sys.all_objects AS s1
    CROSS JOIN sys.all_objects AS s2
    -- on my system this would support > 5 million days
    ORDER BY s1.[object_id]
  ) AS x
) AS y;

At this point, #dim looks like this, just showing the first 5 and last 5 dates:

Truncated contents of #dim temporary table

Now these pre-calculated values can help to derive all of the other materialized columns you might want in your calendar table. The following is just a sampling of the things I see most commonly; I am sure that you do not need all of these columns, and that there might be other columns you need. You should just use this as a starting point:

CREATE TABLE dbo.DateDimension
  --DateKey           INT         NOT NULL PRIMARY KEY,
  [Date]              DATE        NOT NULL,
  [Day]               TINYINT     NOT NULL,
  DaySuffix           CHAR(2)     NOT NULL,
  [Weekday]           TINYINT     NOT NULL,
  WeekDayName         VARCHAR(10) NOT NULL,
  IsWeekend           BIT         NOT NULL,
  IsHoliday           BIT         NOT NULL,
  HolidayText         VARCHAR(64) SPARSE,
  DOWInMonth          TINYINT     NOT NULL,
  [DayOfYear]         SMALLINT    NOT NULL,
  WeekOfMonth         TINYINT     NOT NULL,
  WeekOfYear          TINYINT     NOT NULL,
  ISOWeekOfYear       TINYINT     NOT NULL,
  [Month]             TINYINT     NOT NULL,
  [MonthName]         VARCHAR(10) NOT NULL,
  [Quarter]           TINYINT     NOT NULL,
  QuarterName         VARCHAR(6)  NOT NULL,
  [Year]              INT         NOT NULL,
  MMYYYY              CHAR(6)     NOT NULL,
  MonthYear           CHAR(7)     NOT NULL,
  FirstDayOfMonth     DATE        NOT NULL,
  LastDayOfMonth      DATE        NOT NULL,
  FirstDayOfQuarter   DATE        NOT NULL,
  LastDayOfQuarter    DATE        NOT NULL,
  FirstDayOfYear      DATE        NOT NULL,
  LastDayOfYear       DATE        NOT NULL,
  FirstDayOfNextMonth DATE        NOT NULL,
  FirstDayOfNextYear  DATE        NOT NULL

-- create other useful index(es) here

A couple of notes:

  • DateKey is commented out. It is defined as an INT not because that is my preference (I would always store this as a DATE), but because that seems to be the preference of most data warehousing professionals. If you want your date key to be an int, please note the places in the code below where I comment out anything to do with DateKey.
  • DOWInMonth is the occurrence of that weekday within the current month - 1st Sunday, 3rd Monday, etc.

Now to populate this table from our #dim object, it is a relatively straightforward INSERT/SELECT; still, you'll see why I pre-calculated some of the values, since many of the expressions are used multiple times:

INSERT dbo.DateDimension WITH (TABLOCKX)
  --DateKey     = CONVERT(INT, Style112),
  [Date]        = [date],
  [Day]         = CONVERT(TINYINT, [day]),
  DaySuffix     = CONVERT(CHAR(2), CASE WHEN [day] / 10 = 1 THEN 'th' ELSE 
                  CASE RIGHT([day], 1) WHEN '1' THEN 'st' WHEN '2' THEN 'nd' 
	              WHEN '3' THEN 'rd' ELSE 'th' END END),
  [Weekday]     = CONVERT(TINYINT, [DayOfWeek]),
  [WeekDayName] = CONVERT(VARCHAR(10), DATENAME(WEEKDAY, [date])),
  [IsWeekend]   = CONVERT(BIT, CASE WHEN [DayOfWeek] IN (1,7) THEN 1 ELSE 0 END),
  [IsHoliday]   = CONVERT(BIT, 0),
  HolidayText   = CONVERT(VARCHAR(64), NULL),
                  (PARTITION BY FirstOfMonth, [DayOfWeek] ORDER BY [date])),
                  (PARTITION BY [year], [month] ORDER BY [week])),
  WeekOfYear    = CONVERT(TINYINT, [week]),
  [Month]       = CONVERT(TINYINT, [month]),
  [MonthName]   = CONVERT(VARCHAR(10), [MonthName]),
  [Quarter]     = CONVERT(TINYINT, [quarter]),
  QuarterName   = CONVERT(VARCHAR(6), CASE [quarter] WHEN 1 THEN 'First' 
                  WHEN 2 THEN 'Second' WHEN 3 THEN 'Third' WHEN 4 THEN 'Fourth' END), 
  [Year]        = [year],
  MMYYYY        = CONVERT(CHAR(6), LEFT(Style101, 2)    + LEFT(Style112, 4)),
  MonthYear     = CONVERT(CHAR(7), LEFT([MonthName], 3) + LEFT(Style112, 4)),
  FirstDayOfMonth     = FirstOfMonth,
  LastDayOfMonth      = MAX([date]) OVER (PARTITION BY [year], [month]),
  FirstDayOfQuarter   = MIN([date]) OVER (PARTITION BY [year], [quarter]),
  LastDayOfQuarter    = MAX([date]) OVER (PARTITION BY [year], [quarter]),
  FirstDayOfYear      = FirstOfYear,
  LastDayOfYear       = MAX([date]) OVER (PARTITION BY [year]),
  FirstDayOfNextMonth = DATEADD(MONTH, 1, FirstOfMonth),
  FirstDayOfNextYear  = DATEADD(YEAR,  1, FirstOfYear)
FROM #dim

We're not done yet; all of the IsHoliday values are still set to 0. Since I am in the United States, I'm going to deal with statutory holidays here; of course, if you live in another country, you'll need to use different logic here. You'll also need to add your own company's holidays manually, but hopefully if you have things that are deterministic, like bank holidays, Boxing Day, or the third Monday of July is your annual off-site arm-wrestling tournament, you should be able to do most of that without much work by following the same sort of pattern I use below. We can update most of the stat holidays with a single pass and rather simple criteria:

  SELECT /* DateKey, */ [Date], IsHoliday, HolidayText, FirstDayOfYear,
    DOWInMonth, [MonthName], [WeekDayName], [Day],
    LastDOWInMonth = ROW_NUMBER() OVER 
      PARTITION BY FirstDayOfMonth, [Weekday] 
      ORDER BY [Date] DESC
  FROM dbo.DateDimension
UPDATE x SET IsHoliday = 1, HolidayText = CASE
  WHEN ([Date] = FirstDayOfYear) 
    THEN 'New Year''s Day'
  WHEN ([DOWInMonth] = 3 AND [MonthName] = 'January' AND [WeekDayName] = 'Monday')
    THEN 'Martin Luther King Day'    -- (3rd Monday in January)
  WHEN ([DOWInMonth] = 3 AND [MonthName] = 'February' AND [WeekDayName] = 'Monday')
    THEN 'President''s Day'          -- (3rd Monday in February)
  WHEN ([LastDOWInMonth] = 1 AND [MonthName] = 'May' AND [WeekDayName] = 'Monday')
    THEN 'Memorial Day'              -- (last Monday in May)
  WHEN ([MonthName] = 'July' AND [Day] = 4)
    THEN 'Independence Day'          -- (July 4th)
  WHEN ([DOWInMonth] = 1 AND [MonthName] = 'September' AND [WeekDayName] = 'Monday')
    THEN 'Labour Day'                -- (first Monday in September)
  WHEN ([DOWInMonth] = 2 AND [MonthName] = 'October' AND [WeekDayName] = 'Monday')
    THEN 'Columbus Day'              -- Columbus Day (second Monday in October)
  WHEN ([MonthName] = 'November' AND [Day] = 11)
    THEN 'Veterans'' Day'            -- Veterans' Day (November 11th)
  WHEN ([DOWInMonth] = 4 AND [MonthName] = 'November' AND [WeekDayName] = 'Thursday')
    THEN 'Thanksgiving Day'          -- Thanksgiving Day (fourth Thursday in November)
  WHEN ([MonthName] = 'December' AND [Day] = 25)
    THEN 'Christmas Day'
  ([Date] = FirstDayOfYear)
  OR ([DOWInMonth] = 3     AND [MonthName] = 'January'   AND [WeekDayName] = 'Monday')
  OR ([DOWInMonth] = 3     AND [MonthName] = 'February'  AND [WeekDayName] = 'Monday')
  OR ([LastDOWInMonth] = 1 AND [MonthName] = 'May'       AND [WeekDayName] = 'Monday')
  OR ([MonthName] = 'July' AND [Day] = 4)
  OR ([DOWInMonth] = 1     AND [MonthName] = 'September' AND [WeekDayName] = 'Monday')
  OR ([DOWInMonth] = 2     AND [MonthName] = 'October'   AND [WeekDayName] = 'Monday')
  OR ([MonthName] = 'November' AND [Day] = 11)
  OR ([DOWInMonth] = 4     AND [MonthName] = 'November' AND [WeekDayName] = 'Thursday')
  OR ([MonthName] = 'December' AND [Day] = 25);

(You may have to perform some manual modifications to some of those, in the case where they fall on a weekend - usually the following Monday is marked as the holiday instead.)

Black Friday is a little trickier, because it's the Friday after the fourth Thursday in November, and so it might be the fourth Friday, but several times a century it is actually the fifth Friday:

UPDATE d SET IsHoliday = 1, HolidayText = 'Black Friday'
FROM dbo.DateDimension AS d
  SELECT /* DateKey, */ [Date], [Year], [DayOfYear]
  FROM dbo.DateDimension 
  WHERE HolidayText = 'Thanksgiving Day'
) AS src 
ON d.[Year] = src.[Year] 
AND d.[DayOfYear] = src.[DayOfYear] + 1;

And then there's Easter. This has always been a complicated problem; the rules for calculating the exact date are so convoluted, I suspect most people can only mark those dates where they have physical calendars they can look at to confirm. If your company doesn't recognize Easter, you can skip ahead; if it does, you can use the following function, which will return the Easter holiday dates for any given year:

CREATE FUNCTION dbo.GetEasterHolidays(@year INT) 
  WITH x AS 
    SELECT [Date] = CONVERT(DATE, RTRIM(@year) + '0' + RTRIM([Month]) 
        + RIGHT('0' + RTRIM([Day]),2))
      FROM (SELECT [Month], [Day] = DaysToSunday + 28 - (31 * ([Month] / 4))
      FROM (SELECT [Month] = 3 + (DaysToSunday + 40) / 44, DaysToSunday
      FROM (SELECT DaysToSunday = paschal - ((@year + @year / 4 + paschal - 13) % 7)
      FROM (SELECT paschal = epact - (epact / 28)
      FROM (SELECT epact = (24 + 19 * (@year % 19)) % 30) 
        AS epact) AS paschal) AS dts) AS m) AS d
  SELECT [Date], HolidayName = 'Easter Sunday' FROM x
    UNION ALL SELECT DATEADD(DAY,-2,[Date]), 'Good Friday'   FROM x
    UNION ALL SELECT DATEADD(DAY, 1,[Date]), 'Easter Monday' FROM x

(You can adjust the function easily, depending on whether they recognize just Easter Sunday or also Good Friday and/or Easter Monday. There is also another tip here that will show you how to determine the date for Mardi Gras, given the date for Easter.)

Now, to use that function to mark the Easter holidays in the calendar table:

  SELECT d.[Date], d.IsHoliday, d.HolidayText, h.HolidayName
    FROM dbo.DateDimension AS d
    CROSS APPLY dbo.GetEasterHolidays(d.[Year]) AS h
    WHERE d.[Date] = h.[Date]
UPDATE x SET IsHoliday = 1, HolidayText = HolidayName;

And now you have a functional calendar table you can use for all of your reporting or business needs.


Creating a dimension or calendar table for business dates and fiscal periods might seem intimidating at first, but once you have a solid methodology in line, it can be very worthwhile. There are many ways to do this; some will subscribe to the idea that many of these date-related facts can be derived at query time, or at least be non-persisted computed columns. You will have to decide if the values are calculated often enough to justify the additional space on disk and in the buffer pool.

If you are using Enterprise Edition on SQL Server 2014 or above, you could consider using In-Memory OLTP, and possibly even a non-durable table that you rebuild using a startup procedure. Or on any version or edition, you could put the calendar table into its own filegroup (or database), and mark it as read-only after initial population (this won't force the table to stay in memory all the time, but it will reduce other types of contention).

Next Steps

Last Updated: 2018-07-24

get scripts

next tip button

About the author
MSSQLTips author Aaron Bertrand Aaron Bertrand (@AaronBertrand) is a passionate technologist with industry experience dating back to Classic ASP and SQL Server 6.5. He is editor-in-chief of the performance-related blog,, and serves as a community moderator for the Database Administrators Stack Exchange.

View all my tips

Post a comment or let the author know this tip helped.

All comments are reviewed, so stay on subject or we may delete your comment. Note: your email address is not published. Required fields are marked with an asterisk (*).

Email me updates

Signup for our newsletter

I agree by submitting my data to receive communications, account updates and/or special offers about SQL Server from MSSQLTips and/or its Sponsors. I have read the privacy statement and understand I may unsubscribe at any time.

Sunday, December 29, 2019 - 11:05:43 AM - BoAnna Back To Top


This post actually resolved some of my dilemmas i have for a similar project :) 

I stil have a few decisions to make, but this helped figure out a few of them. This is a short description of what I got stuck on

Friday, December 20, 2019 - 1:04:18 PM - John-S Pretorius Back To Top

Good day,

Thank you for the great topic on calenders, I have a challenge defining a standard day from 03am prev day to current 02:59:59

Tuesday, November 26, 2019 - 1:01:28 PM - Chaitanya Back To Top

I am looking for a solution where I have table format at source e.g. NewTableName112019<MMYYYY>, So when I run my SSIS package it should dynamically select current month -1 (basically last month) automatically.

Help would be really appreciated

Wednesday, November 06, 2019 - 10:34:50 AM - Phil Barr Back To Top

This is wonderful.  Thank you so much for all the work you've put into this.  I'll let you know what enhancements I need as I begin to use it.

Friday, November 01, 2019 - 1:20:31 PM - Nicolas Back To Top

Thanks!!! That helped me a lot! You are a genious.

Tuesday, October 01, 2019 - 1:41:44 PM - Daniel Adeniji Back To Top

Aaron Bertrand,

As always thanks for your articles.

Daniel Adeniji

Monday, September 16, 2019 - 4:57:51 PM - Marc Church Back To Top


You have saved me so much time with this.  Thanks you very much!!!

Thursday, August 29, 2019 - 12:29:16 PM - Ben Back To Top

Hi Aaron,

I just wanted to say thank you for your date dimension/calendar table code.  This was really helpful in establishing some dynamic date ranges that I needed for a project.  I will definitely be following your blog for more tips and tricks!


Friday, March 01, 2019 - 10:53:19 AM - Elena Back To Top

Very helpful! Thank you so much!

I do have a question though... As I try to set the holidays, I receive the message that I cannot change a computed column. I read your post several times, but do not seem to get where exactly to include updates. Could you be so kind and help me?

Thursday, February 07, 2019 - 11:25:03 AM - klm Back To Top

 Excellent!!!!  This was very helpful.

Thursday, January 17, 2019 - 10:40:07 AM - Aaron Bertrand Back To Top

-- hugk,

/* after this section: */

-- use the catalog views to generate as many rows as we need

INSERT #dim([date])
) AS y;

/* add this: */

ALTER TABLE #dim ADD dimdate char(8);

;WITH calc AS 
  SELECT [date], dimdate,
    m = DATEADD(MONTH, CASE WHEN [day] > 15 THEN 1 ELSE 0 END, FirstOfMonth) 
  FROM #dim
  SET dimdate = CONVERT(char(3), DATENAME(MONTH,m))
    +' '+ CONVERT(char(4), m, 112);

/* once you add the dimdate column add dbo.DateDimension you should then just be able to add dimdate to the insert/select there. */

Thursday, January 17, 2019 - 9:47:58 AM - hugk Back To Top


Thank you for this.

I would like to have a dimdate with the follow requirement.

2018-12-16 to 2019-01-15 = Jan 2019

2019 -01-16 to 2019-02-15 = Feb 2019

2019-02-16 to 2019-03-15 = Mar 2019



2019-11-16 to 2019-12-15 = Dec 2019

2019-12-16 to 2020-01-15 = Jan 2020

Please assist with any hint


Monday, December 10, 2018 - 9:23:52 AM - Bobby Back To Top

Aaron, this is incredible. Thank you so much. I am struggling with 3 columns that I was asked to add and was hoping you can assist

Total Work Days in a Year Less Holiays?

Total Work Days till end of the Month, Less Holidays?

What current working day is today?

I have searched around with little help :( and was wonding if something like this could be achieved more simply?

Monday, December 03, 2018 - 9:33:25 PM - greg.locke Back To Top

easy to follow. Thanks.

it was easy to add fiscal periods for off calendar annual cycles to the create TABLE #dim.  

 [FiscalY] AS CASE DATEPART(QUARTER,     [date]) WHEN 1 THEN DATEPART(YEAR,     [date]) WHEN 2 THEN DATEPART(YEAR,     [date]) WHEN 3 THEN DATEPART(YEAR,     [date]) + 1 WHEN 4 THEN  DATEPART(YEAR,     [date]) + 1 END,


  [FiscalM] AS CASE DATEPART(MONTH,  [date]) WHEN 7 THEN 1 WHEN 8 THEN 2 when 9 then 3 when 10 then 4 when 11 then 5 when 12 then 6 

Thursday, November 29, 2018 - 10:45:14 AM - Leon Back To Top

Very helpful post, thank you I learnt a lot looking through the SQL script, i'm just starting with Analysis Services and this is a big help!!!

Sunday, October 21, 2018 - 6:03:27 AM - Mike Bailey Back To Top

 Hi Aaron,


Thanks for the great article. Would it be possible for you to add an update to include setting Fiscal information for businesses that run on a Fiscal calendar - FiscalYear, FiscalMonth, FiscalWeek, FiscalDay?



Mike Bailey


Wednesday, September 19, 2018 - 11:24:58 AM - Joe Celko Back To Top

Easter is one of my favorite holidays, not because I'm religious, but because I am a mathematician. It was originally created so that Passover and Easter would not conflict with each other. You could tell the Christians from the Jews. However, we want with two kinds of Christians and two kinds of Easter – Catholic and Orthodox. Decades ago I was trying to set up a calendar of the company was working at and we got Easter is a holiday. I had two junior programmers who were doing the work on this and one of them had found an algorithm for computing Easter. I told him don't bother with that rather tricky calculation; you are religious and you go to church every Sunday. So why don't you just ask your priest to hand you a list of all of the Good Fridays and Easters for the next 10 or 15 years? I thought this was a good example of the "don't reinvent the wheel" principle of programming. Unfortunately, I didn't know he was Greek Orthodox and that we were on the Catholic version of Easter. Sometimes they match, and sometimes they don't. 

Wednesday, September 19, 2018 - 8:22:17 AM - Wise Old Man Back To Top


Very nice.

One thing that is missing is a YOY date for those who "day align" their data like the hospitality industry.  An example of what that means is comparing the first Monday in January this year to the first Monday in January last year.  It's easy enough to just subtract 52 weeks, which always gives you the same day of the week; however the issue is that 365 (or 366) is not divisible by 7, so the week actually moves every 3-4 years.  It's very similar to the timeshare week problem of having weeks 1-53 every third or fourth year.  It would be interesting to see if there is an industry standard for adding that to the calendar table.

Thursday, August 30, 2018 - 7:23:20 AM - Aaron Bertrand Back To Top

Paul Hunter, yes, Easter is not a holiday for all of us in the US, but I included it because there is a significant audience outside of the US *and* because it's an interesting problem that's a little harder to solve than typical holidays.

Anyway, your comments about Good Friday, that's why the Easter function has this:

    UNION ALL SELECT DATEADD(DAY,-2,[Date]), 'Good Friday'   FROM x

Thursday, August 30, 2018 - 7:17:15 AM - Aaron Bertrand Back To Top

Joshua, yeah, I've heard that argument before. But when I ask for an actual demonstration, or even a recipe for me to build my own demonstration, they always mysteriously move on to other things and never get back to me.

I feel like on modern computers this is like using tweezers to take bugs off your bumper to increase your gas mileage, but would love for you to show me an actual scenario where this does make a tangible difference. But not here. Build a real example, run real performance tests, blog about it. Without that, I'm going to be perfectly honest, the rest of us in the data community just see it as hand-wavy.

And still, even if there is a tangible difference in one part of the process, I'll probably still take that hit over the hits I know I'll take elsewhere - loss of validation, implicit conversions causing scans, and lots of sloppy compensatory explicit conversions riddling the code.

Wednesday, August 29, 2018 - 6:47:45 PM - Joshua Perry Back To Top

As one of those DW guys, I'll tell you why we want the date ID as an integer and not just a date...when you're dealing with those millions of rows, and you've properly optimized your DW to be CPU bound, you want to use a datatype that is native to the CPUfor filtering and sorting.  It makes a huge difference, and combined with different indexing and partitioning schemes, we can actually slice those millions of rows on the fly, without the need to build traditional multidimensional cubes, and that's the holy grail for BI self-service, because there is no need to have an analyst spend months building a model before the data can be consumed.  It goes back to what some of us learned back when CPUs were extremely slow...value types versus reference types, and the reason Visual Basic stores dates as integers with an epoch.  Integers as value types are processed by the CPU much faster than a SQL datetime.  We still use the datetime for certain functions as well, however, using the integer for inital sorting and filtering is much faster.

Thursday, August 02, 2018 - 12:37:36 PM - kiran Back To Top

 Very Nice

Tuesday, July 17, 2018 - 9:50:58 AM - Aaron Bertrand Back To Top

Bob, I agree with you wholeheartedly, and actually have had that fight at work, and won.

But that is one DW person in a sea of people who will happily die on the sword of Kimball.

If you feel strongly about using a date, use date. It doesn't really change anything of substance here. I was just (uncharacteristically) being a conformist. 

Tuesday, July 17, 2018 - 8:45:10 AM - Bob Paris Back To Top


I think it is a very good article. One point though, you said:

A couple of notes:

DateKey is an INT not because that is my preference (I would always store this as a DATE), but because that seems to be the preference of most data warehousing professionals.

I see this a LOT. I do not understand it because its a DATE and because dwguys do this then it has to be converted. A DATE is a DATE.

In SQL Server, each column, local variable, expression, and parameter has a related data type. A data type is an attribute that specifies the type of data that the object can hold: integer data, character data, monetary data, date and time data, binary strings, and so on. (From: )

I do not believe that a bad practice should be encouraged because many dwguys do it.

Friday, June 01, 2018 - 5:00:54 PM - Julie Back To Top

This was a huge help. Thank you so much. I'm working with school data so I updated the holiday based on another table. The whole thing worked like a charm and will save me hours of work with future queries.


Friday, March 23, 2018 - 4:16:09 PM - Paul Hunter Back To Top

Thanks for the excellent tips.  Easter isn't normally a US holiday... unless your in the finicial trading sector, at which point the quesiton is "when is good friday".   If you can calculate Easter you can find good friday by backing up two days.  The other issue is "floaters"... any holiday that occurs on a specific day (i.e. New Years, US Independence, Veterans Day and Christmas) will float to the the preceeding/following week day when it falls during a weekend.  If the holiday falls on a Saturday it floats to Friady, if it falls on Sunday it floats to Monday.  I assume there are similar rules for non-US holidays such as Boxing Day.


Tuesday, December 05, 2017 - 2:43:07 PM - Devang Mistry Back To Top



Thanks for the great post, saves a lot of time. In my case i set the Start date as 20101001 as fiscal year starts from 1st october. What would be the change if i want to see 1 in the month column instead of 10. I need to make sure that october is the first month of the fiscal year not 10th. 

Any help will be greatly appreciated. Thanks.

Monday, November 27, 2017 - 5:05:45 PM - Aaron Bertrand Back To Top


(1) if you think you will need to support more than one holiday on the same day, and you didn't want to put "Father's Day and Pokemon Day" in the HolidayText column, then sure, you could use another table for that. It isn't a requirement that I've come across in my experience to date, and there are probably a lot of other less common requirements my solution doesn't cover, either.

(2) LastDOWInMonth is certainly defined at runtime in the CTE (just search this page for that term). It's not meant to be in the main dimension table, but again, if this is a requirement for you, obviously it's easy to add it to the table.

Monday, November 27, 2017 - 3:52:45 PM - Kirk Back To Top

A question and a slight hicup in the code that I see.

First, my question is what would be the downside of putting holidays in a seperate table (outside a join). As a developer I like the idea that the difference between instances would be isolated to a seperate table. Also, it could allow multiple holidays on the the same date if desired.

 My observation is that you use LastDOWInMonth to define Memorial Day. Which is correct, but the column isn't in your table. I easily added it setting the default to 0 then updating with the following:

UPDATE DateDimension

SET LastDOWInMonth = 1



    SELECT dd.Date

    FROM DateDimension dd

    WHERE DATEPART(month, DATEADD(day, 7, dd.Date)) <> DATEPART(month, dd.Date)


Friday, November 10, 2017 - 2:45:32 AM - Anne-Maarit Back To Top

 Thank you so much, this saved me days of time. It worked amazingly.



Tuesday, November 07, 2017 - 5:20:05 PM - alan Back To Top

 This was awesome, a real time saver for me and the processor. 




Friday, November 03, 2017 - 5:11:07 AM - Bajke Back To Top

Please use this link!week=53&year=2014

Then Expand for more options and set the First day of week to Sunday.

Observe which days are in the w53 2014 and wich days belong to w1 2015

Do you notice the difference I mentioned when compared to results from your code?

Thursday, November 02, 2017 - 4:44:22 PM - Greg Robidoux Back To Top

The first 4 days for that week 1,2,3,4 were the last 4 days of 2014.  It is based on the calendar.  So since Jan 1, 2015 started on a Thursday and the weeks begin on a Sunday there were only 3 days in the first calendar week in 2015.

Thursday, November 02, 2017 - 4:25:53 PM - Bajke Back To Top

Exactly! Every week must contain exactley 7 days. :)

So, what happent to 4 more days in 1st week of 2015?

Thursday, November 02, 2017 - 3:50:14 PM - Greg Robidoux Back To Top

Hi Bajke,

I just ran this code and it only shows three days for the first week of 2015. 

The days of the week are the 1st = 5 (Thursday), the 2nd = 6 (Friday) and 3rd = 7 (Saturday).

Let us know if you see something different.


Thursday, November 02, 2017 - 1:44:24 PM - Bajke Back To Top

I thing that this solution has a flaw. For example, the 1st week in 2015 does not have 7 days.

Best Regards,


Saturday, September 09, 2017 - 11:10:16 PM - stephen E Okala Back To Top

 I am buliding one with multiple calendars - four fiscal-year calendars. How do I handle that. Please help.



Wednesday, March 22, 2017 - 2:01:53 PM - Joe Celko Back To Top

 Easter is worse than your implying here. Many decades ago, I wanted to put future Easter dates into a calendar, and I gave the job to two junior programmers. I assumed they would look up the algorithm, but one of them said, "this is silly! I'll just go down and ask my priest for a calendar and I'll know it is right!"; I said we really should check this out a couple of sources just to be sure, etc. so another junior programmer volunteered to verify the other guys work (yes, we used to do code verification and walk-through, back in those days)


Unfortunately, one of them was Greek Orthodox and the other was Russian Orthodox.


Wednesday, March 22, 2017 - 6:35:50 AM - Maz Back To Top


Using you tip/code Aaron as the base I've created a DateDimension table that includes bank holidays for England Wales.

I hope someone finds this of use.




Friday, March 17, 2017 - 12:22:19 PM - KeviM Back To Top

Great article.  Thanks for sharing. 

How would I add the dates for 'FirstOfWeek and 'Lastofweek'?  (Where Monday is the first day of the week)


Tuesday, February 14, 2017 - 12:27:36 PM - Davide Moraschi Back To Top

It is an extemely useful script.


Monday, February 13, 2017 - 9:30:10 AM - Joe Back To Top

 This is awesome. Will save LOTS of time programmatically too (transactions processing) as well as doing data analysis.


Monday, January 23, 2017 - 2:33:54 PM - Robert Back To Top

Thank you for this write up. I am kind of surprised this table doesn't have a bit more of the oddities. I have included them below.



[IsLeapYear]  = CONVERT(BIT, CASE datepart(mm, dateadd(dd, 1, cast((cast([year] as varchar(4)) + '0228') as datetime))) when 2 then 1 else 0 END),


[Has53Weeks]  = CONVERT(BIT, CASE WHEN (5 * [YEAR] + 12 - 4 * (FLOOR([YEAR]/100) - FLOOR([YEAR]/400)) + FLOOR(([YEAR] - 100)/400) - FLOOR(([YEAR] - 102)/400)  + FLOOR(([YEAR] - 200)/400) - FLOOR(([YEAR] - 199)/400)) % 28 < 5 THEN 1 ELSE 0 END),


ISOYear = CONVERT(INT, (SELECT [dbo].[ISOyear]([date]))),


--where ISOyear is


returns SMALLINT




         WHEN Datepart(isowk, @date)=1

             AND Month(@date)=12 THEN Year(@date)+1

         WHEN Datepart(isowk, @date)=53

             AND Month(@date)=1 THEN Year(@date)-1

         WHEN Datepart(isowk, @date)=52

             AND Month(@date)=1 THEN Year(@date)-1             

         ELSE Year(@date)


     RETURN @isoyear;







Saturday, October 22, 2016 - 2:17:27 AM - Brian Back To Top

Fantastic write-up - extremely simple to follow. I'm relatively new to implementing dimension tables, so it's much appreciated.

Can you elaborate a bit on your differences in choosing the date as the pk as opposed using an int? Date would intuitively seem an easier type to manage across tables, to me. What is the advantage in using an int?

Friday, October 14, 2016 - 12:18:19 PM - Erin G Back To Top

 Thank you.  I think this will be quite helpful.  I am a relatively new DBA, < 3 years.  I have been asked to develope a database that can be used to search and then schedule resources.  Kind of like booking a hotel room.  I have saved the page so that I can come back to it when I figure out the rest of the project. 


Tuesday, September 13, 2016 - 6:22:03 AM - Guss Davey Back To Top


Some helper TSQL (thanks for the rest)

--Whenever a public holiday falls on a Sunday, the Monday following it will be a public holiday

UPDATE nextMonday SET nextMonday.IsHoliday = 1, 

HolidayText = CASE WHEN ISNULL(nextMonday.HolidayText,'')='' THEN 'Monday after ' + holidayOnSunday.HolidayText 

ELSE nextMonday.HolidayText END

FROM dbo.DateDimension nextMonday INNER JOIN dbo.DateDimension holidayOnSunday

ON nextMonday.[Date] = DATEADD(day,1,holidayOnSunday.[Date])

WHERE holidayOnSunday.IsHoliday = 1  AND holidayOnSunday.WeekdayName = 'Sunday'



--In Western Christianity, using the Gregorian calendar, 

--Easter always falls on a Sunday between 22 March and 25 April inclusive

UPDATE dbo.DateDimension SET IsHoliday = 1, HolidayText = 'Easter Sunday' 

WHERE WeekdayName = 'Sunday' AND MonthName='April' AND [Day] BETWEEN 20 AND 26



--The Monday following Easter SundayFamily Day

UPDATE nextMonday SET nextMonday.IsHoliday = 1, nextMonday.HolidayText = 'Family Day'

FROM dbo.DateDimension nextMonday INNER JOIN dbo.DateDimension easter

ON nextMonday.[Date] = DATEADD(day,1,easter.[Date])

WHERE easter.HolidayText = 'Easter Sunday'



--The Friday before Easter Sunday = Good Friday

UPDATE previousFriday SET previousFriday.IsHoliday = 1, previousFriday.HolidayText = 'Good Friday'

FROM dbo.DateDimension previousFriday INNER JOIN dbo.DateDimension easter

ON previousFriday.[Date] = DATEADD(day,-2,easter.[Date])

WHERE easter.HolidayText = 'Easter Sunday'


Wednesday, September 07, 2016 - 10:03:43 AM - Erik Back To Top

I'm surprised it doesn't throw an error when you get the language to US_ENGLISH and then pass in 'Labour Day'.

Friday, September 02, 2016 - 3:05:42 AM - Gavin Kelman Back To Top

 Hi i need a normal calendar as you have prepared but i also need a fiscal calendar starting 01 October. Each fiscal period ends on the last Friday of each month. I would appreciate a complete script if possible. Thanks in advance


Gavin Kelman


Tuesday, August 02, 2016 - 3:59:36 PM - vishnu Back To Top


could you show me how to modified the code when the holiday falls on a sunday and the holiday is observed on monday.  Very Much Appreciated. Thanks.  


Tuesday, March 29, 2016 - 12:33:14 AM - Gustavo Schneider Back To Top

Very helpful. Thanks for sharing, my original search was to understand half of things covered on your post, but after seeing it all it lead to answers things i haven't thought yet.  


Tuesday, January 12, 2016 - 5:30:53 PM - Lee Everest Back To Top

Nice, Aaron. Thanks! 


Tuesday, October 20, 2015 - 7:30:03 PM - Henry Stinson Back To Top

I like the article and find the parts about holidays, etc especially valuable, and especially the way you generated the date column.

I thought of using a CTE using the fact that adding 1 to a DateTime data type would add a day, which may not be as fast as your method since it causes a RBAR conversion from DATETIME to DATE data type.



        @EndDate DATE;


SET @EndDate = '01/01/2016';






( SELECT @Today AS d


  SELECT d + 1 AS d

  FROM cte

  WHERE d+1 < @EndDate


INSERT INTO #dim([Date])


FROM cte;


SELECT 100 *

FROM #dim;


In the calculations for first of month and first of year, I didn't like the multiple nested function calls and was thinking of other ways to do that, but not sure I could come up with anything better without spending too much time to do it.  I thought of pulliing out datapart month and year and concatenating those with day '01', but I decided not to even experiement, because I doubt my method would be much faster.

Then I thought of creating a separate table with just first-of-month dates and joining with that to populate the FirstOfMonth dates and FirstOfYear dates in the temp table.  I'm not sure exactly what that join would look like without experimenting, but I'll just throw the thought out there.

But in the end, trying to optimize loading of the table may not be needed, especially given the power of modern hardware and SQL Server.

Tuesday, October 20, 2015 - 3:36:32 PM - Renan Back To Top

Nice tip.

Very easy to use and customize.




get free sql tips

I agree by submitting my data to receive communications, account updates and/or special offers about SQL Server from MSSQLTips and/or its Sponsors. I have read the privacy statement and understand I may unsubscribe at any time.

Learn more about SQL Server tools