SQL – The Calendar Table for Date Dimensions

When it comes to data analysis, a calendar table can be a real game-changer. It may not seem necessary at first, since most data technologies (SQL, programming languages, visualization softwares, Excel, etc.) have a multitude of functions to help you add, subtract, and transform dates to aggregate by any time period imaginable. But the benefits of a calendar table are many:

  • Eliminate calculated fields – Calculated fields aren’t always human readable, which makes it more difficult to determine what you’re up to. Calendar tables can have field names that are easy to recognize. Do you prefer DATE_SUB(currentdate, INTERVAL DAYOFWEEK(currentdate)-1 DAY) or TheFirstOfWeek? I thought so.
  • Increase performance – In addition to their inherent calculation time, calculated fields must be joined in temporary tables (slow!). Instead, you can join and aggregate on a lovely, pre-populated and pre-indexed table to make your queries slippery quick.
  • Create consistency across technologies – Different technologies handle date formulas differently, with different arguments, and different formats. A calendar table gives you the consistency you need to get the job done faster.
  • Establish a full range of dates, even if no data exists for those dates – You can’t query what’s not there. A calendar table will return every date in the range you specify, not just the dates that have the data you’re looking for.
  • Allow for fiscal calendar craziness – Fiscal year start in April? Suffering the 4-4-5 blues? Set it and forget it with a calendar table.
  • Handle weekends and holidays – Weekends can be manageable without a calendar table, but it’ll make your life easier. And I don’t know of any data technology that handles holidays gracefully (because of their regionality), so set yourself up for success by populating the days that are special to you.

So without further ado, here is some MYSQL code to get us started. Just use the database of your choice, and copy/paste the entirety of this code.

DROP TABLE IF EXISTS date_dimension;
CREATE TABLE date_dimension (
    ID                          INTEGER PRIMARY KEY,
    TheDate                     DATE NOT NULL,
    TheDay                      INTEGER NOT NULL,
    TheDaySuffix                CHAR(2) NOT NULL,
    TheDayName                  VARCHAR(9) NOT NULL,
    TheDayOfWeek                INTEGER NOT NULL,
    TheDayOfWeekInMonth         INTEGER NOT NULL,
    TheDayOfYear                INTEGER NOT NULL,
    IsWeekend                   INTEGER NOT NULL,
    TheWeek                     INTEGER NOT NULL,
    TheISOWeek                  INTEGER NOT NULL,
    TheFirstOfWeek              DATE NOT NULL,
    TheLastOfWeek               DATE NOT NULL,
    TheWeekOfMonth              INTEGER NOT NULL,
    TheMonth                    INTEGER NOT NULL,
    TheMonthName                VARCHAR(9) NOT NULL,
    TheFirstOfMonth             DATE NOT NULL,
    TheLastOfMonth              DATE NOT NULL,
    TheFirstOfNextMonth         DATE NOT NULL,
    TheLastOfNextMonth          DATE NOT NULL,
    TheQuarter                  INTEGER NOT NULL,
    TheFirstOfQuarter           DATE NOT NULL,
    TheLastOfQuarter            DATE NOT NULL,
    TheYear                     INTEGER NOT NULL,
    TheISOYear                  INTEGER NOT NULL,
    TheFirstOfYear              DATE NOT NULL,
    TheLastOfYear	        DATE NOT NULL,
    IsLeapYear                  INTEGER NOT NULL,
    Has53Weeks                  INTEGER NOT NULL,
    Has53ISOWeeks               INTEGER NOT NULL,
    TheFiscalMonth_Apr          INTEGER NOT NULL,
    TheFiscalQuarter_Apr	INTEGER NOT NULL,
    TheFiscalYear_Apr           INTEGER NOT NULL,
    TheFirstOfFiscalYear_Apr    DATE NOT NULL,
    TheLastOfFiscalYear_Apr     DATE NOT NULL,
    TheFirstOfNextFiscal_Apr    DATE NOT NULL,
    TheFiscalMonth_Oct          INTEGER NOT NULL,
    TheFiscalQuarter_Oct        INTEGER NOT NULL,
    TheFiscalYear_Oct           INTEGER NOT NULL,
    TheFirstOfFiscalYear_Oct    DATE NOT NULL,
    TheLastOfFiscalYear_Oct     DATE NOT NULL,
    TheFirstOfNextFiscal_Oct    DATE NOT NULL,
        UNIQUE td_ymd_idx (TheYear,TheMonth,TheDay),
        UNIQUE td_dbdate_idx (TheDate)
) Engine=InnoDB;

DROP PROCEDURE IF EXISTS fill_date_dimension;
DELIMITER //
CREATE PROCEDURE fill_date_dimension(IN startdate DATE,IN stopdate DATE)
BEGIN
    DECLARE currentdate DATE;
    SET currentdate = startdate;
    WHILE currentdate < stopdate DO
        INSERT INTO date_dimension VALUES (
            YEAR(currentdate)*10000+MONTH(currentdate)*100 + DAY(currentdate),
            currentdate,
            DAY(currentdate),
            SUBSTRING(DATE_FORMAT(currentdate,'%D'),-2,2),
            DATE_FORMAT(currentdate,'%W'),
            DAYOFWEEK(currentdate),
            FLOOR(DAY(currentdate)/7)+1,
            DATE_FORMAT(currentdate,'%j'),
            CASE DAYOFWEEK(currentdate) WHEN 1 THEN 0 WHEN 7 THEN 0 ELSE 1 END,
            WEEK(currentdate,6),
            WEEKOFYEAR(currentdate),
            DATE_SUB(currentdate, INTERVAL DAYOFWEEK(currentdate)-1 DAY),
            DATE_ADD(currentdate, INTERVAL 7-DAYOFWEEK(currentdate) DAY),
            WEEK(currentdate)-WEEK(DATE_ADD(MAKEDATE(YEAR(currentdate), 1), INTERVAL (MONTH(currentdate))-1 MONTH))+1,
            MONTH(currentdate),
            DATE_FORMAT(currentdate,'%M'),
            DATE_ADD(MAKEDATE(YEAR(currentdate), 1), INTERVAL (MONTH(currentdate))-1 MONTH),
            DATE_ADD(DATE_ADD(MAKEDATE(YEAR(currentdate), 1), INTERVAL (MONTH(currentdate)) MONTH), INTERVAL -1 DAY),
            DATE_ADD(MAKEDATE(YEAR(currentdate), 1), INTERVAL (MONTH(currentdate)) MONTH),
            DATE_ADD(DATE_ADD(MAKEDATE(YEAR(currentdate), 1), INTERVAL (MONTH(currentdate))+1 MONTH), INTERVAL -1 DAY),
            QUARTER(currentdate),
            DATE_ADD(MAKEDATE(YEAR(currentdate),1), INTERVAL (QUARTER(currentdate)-1)*3 MONTH),
            DATE_ADD(DATE_ADD(MAKEDATE(YEAR(currentdate),1), INTERVAL (QUARTER(currentdate))*3 MONTH), INTERVAL -1 DAY),
            YEAR(currentdate),
            YEAR(currentdate) - CASE WHEN MONTH(currentdate) = 1 AND WEEKOFYEAR(currentdate) > 51 THEN 1 WHEN MONTH(currentdate) = 12 AND WEEKOFYEAR(currentdate) = 1  THEN -1 ELSE 0 END,
            MAKEDATE(YEAR(currentdate), 1),
            DATE_ADD(MAKEDATE(YEAR(currentdate)+1, 1), INTERVAL -1 DAY),
            CASE WHEN (YEAR(currentdate) % 400 = 0) OR (YEAR(currentdate) % 4 = 0 AND YEAR(currentdate) % 100 != 0)  THEN 1 ELSE 0 END,
            WEEK(DATE_ADD(MAKEDATE(YEAR(currentdate)+1, 1), INTERVAL -1 DAY),6),
            WEEKOFYEAR(DATE_ADD(MAKEDATE(YEAR(currentdate)+1, 1), INTERVAL -1 DAY)),
            CASE WHEN MONTH(currentdate) <= 3 THEN MONTH(currentdate) + 9 ELSE MONTH(currentdate) - 3 END,
            CASE WHEN MONTH(currentdate) <= 3 THEN 4 ELSE QUARTER(currentdate)-1 END,
            CASE WHEN MONTH(currentdate) <= 3 THEN YEAR(currentdate) ELSE YEAR(currentdate)+1 END,
            CASE WHEN MONTH(currentdate) <= 3 THEN DATE_ADD(MAKEDATE(YEAR(currentdate)-1, 1), INTERVAL 3 MONTH) ELSE DATE_ADD(MAKEDATE(YEAR(currentdate), 1) , INTERVAL 3 MONTH) END,
            CASE WHEN MONTH(currentdate) <= 3 THEN DATE_ADD(DATE_ADD(MAKEDATE(YEAR(currentdate), 1), INTERVAL 3 MONTH), INTERVAL -1 DAY) ELSE DATE_ADD(DATE_ADD(MAKEDATE(YEAR(currentdate)+1, 1), INTERVAL 3 MONTH), INTERVAL -1 DAY) END,
            CASE WHEN MONTH(currentdate) <= 3 THEN DATE_ADD(MAKEDATE(YEAR(currentdate), 1), INTERVAL 3 MONTH) ELSE DATE_ADD(MAKEDATE(YEAR(currentdate)+1, 1), INTERVAL 3 MONTH) END,
            CASE WHEN MONTH(currentdate) >= 10 THEN MONTH(currentdate) - 9 ELSE MONTH(currentdate) + 3 END,
            CASE WHEN MONTH(currentdate) >= 10 THEN 1 ELSE QUARTER(currentdate)+1 END,
            CASE WHEN MONTH(currentdate) >= 10 THEN YEAR(currentdate)+1 ELSE YEAR(currentdate) END,
            CASE WHEN MONTH(currentdate) >= 10 THEN DATE_ADD(MAKEDATE(YEAR(currentdate), 1), INTERVAL 9 MONTH) ELSE DATE_ADD(MAKEDATE(YEAR(currentdate)-1, 1), INTERVAL 9 MONTH) END,
            CASE WHEN MONTH(currentdate) >= 10 THEN DATE_ADD(DATE_ADD(MAKEDATE(YEAR(currentdate)+1, 1), INTERVAL 9 MONTH), INTERVAL -1 DAY) ELSE DATE_ADD(DATE_ADD(MAKEDATE(YEAR(currentdate), 1), INTERVAL 9 MONTH), INTERVAL -1 DAY) END,
            CASE WHEN MONTH(currentdate) >= 10 THEN DATE_ADD(MAKEDATE(YEAR(currentdate)+1, 1), INTERVAL 9 MONTH) ELSE DATE_ADD(MAKEDATE(YEAR(currentdate), 1), INTERVAL 9 MONTH) END
        );
        SET currentdate = DATE_ADD(currentdate,INTERVAL 1 DAY);
    END WHILE;
END
//
DELIMITER ;

TRUNCATE TABLE date_dimension;

CALL fill_date_dimension('2006-01-01','2041-01-01');
OPTIMIZE TABLE date_dimension;

Here is an example row from the date_dimension table that is created:

                      ID: 20220502
                 TheDate: 2022-05-02
                  TheDay: 2
            TheDaySuffix: nd
              TheDayName: Monday
            TheDayOfWeek: 2
     TheDayOfWeekInMonth: 1
            TheDayOfYear: 122
               IsWeekend: 1
                 TheWeek: 18
              TheISOWeek: 18
          TheFirstOfWeek: 2022-05-01
           TheLastOfWeek: 2022-05-07
          TheWeekOfMonth: 1
                TheMonth: 5
            TheMonthName: May
         TheFirstOfMonth: 2022-05-01
          TheLastOfMonth: 2022-05-31
     TheFirstOfNextMonth: 2022-06-01
      TheLastOfNextMonth: 2022-06-30
              TheQuarter: 2
       TheFirstOfQuarter: 2022-04-01
        TheLastOfQuarter: 2022-06-30
                 TheYear: 2022
              TheISOYear: 2022
          TheFirstOfYear: 2022-01-01
           TheLastOfYear: 2022-12-31
              IsLeapYear: 0
              Has53Weeks: 52
           Has53ISOWeeks: 52
      TheFiscalMonth_Apr: 2
    TheFiscalQuarter_Apr: 1
       TheFiscalYear_Apr: 2023
TheFirstOfFiscalYear_Apr: 2022-04-01
 TheLastOfFiscalYear_Apr: 2023-03-31
TheFirstOfNextFiscal_Apr: 2023-04-01
      TheFiscalMonth_Oct: 8
    TheFiscalQuarter_Oct: 3
       TheFiscalYear_Oct: 2022
TheFirstOfFiscalYear_Oct: 2021-10-01
 TheLastOfFiscalYear_Oct: 2022-09-30
TheFirstOfNextFiscal_Oct: 2022-10-01

Leave a Comment