IntroductionThose of you that have worked extensively with dates in SQL Server (or any other relational database management system (RDBMS)) will know how finicky and complicated it can be to use DATE functions, DATEPART, DATENAME, DATEADD, GETDATE(), CURRENT_TIMESTAMP etc. Personally, I find it very useful to have a calendar table that stocks all of the necessary, pre-calculated fields in one place. Whether you’re doing BI or web, it can be very helpful to have a fixed ID for a date in order to really optimize your data analysis and processing.
There are two points that you need to grasp in order to understand the following script:
- Computed column values
- Recursive CTE (Common Table Expressions) queries.
In SQL Server it is possible to create columns in a table that are merely calculations based on another column. These are called computed columns and they can come in very handy.
Take this query for example:
CREATE TABLE #idTest (ID INT NOT NULL IDENTITY(1,1), ID0 AS (ID -1), VALUE VARCHAR(32))
INSERT INTO #idTest(VALUE)
Here we are creating e a table with an auto-incremented ID that starts at 1 and another ID that effectively starts at 0 by subtracting 1 from the value of ID.
It is worth noting also that these columns do not occupy disk space as they are recalculated with every query execution. However, if need be, you can tell the database engine to stock the data by adding the word PERSISTED to each column in the CREATE script. You can see an example of this in the code used below.
Recursive queries in SQL Server
This one is a bit more difficult to get your head around and don’t worry if you can’t understand it completely, the importance is that you understand the utility of it. I will try to explain it in a simple way. Basically, we create a temporary table and execute a select on a value in that temporary table. This creates a loop that we take advantage of. But don’t worry, SQL Server has a recursive limit of 100 by default. This means you won’t knock out the instance by executing an infinite loop unless you really want to. It is often useful to use recursive CTE to calculate hierarchies in a single query. You can check out more details on CTEs and recursive queries here.
Once you have come to grips with these two concepts you will have the knowledge necessary to dynamically build your own calendar table. I have provided a basic example below which will create a date table with dates, years, semesters, trimesters, months, weeks and days. If you decide to implement such a table you can optimize storage by simply referencing a date ID in any table instead of stocking a full date. It will also allow you to select parts of dates instead of calculating them on the fly if you join to the calendar table.
Table and data creation
In this DDL script you see the creation of one ID column, one date column and the rest being calculations based on the date “DATE” using computed column syntax. The computed columns are then stored on the disk using the PERSISTED key word.
CREATE TABLE [dbo].CALENDAR(
[ID] INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
[DATE] DATE NOT NULL,
[YEAR] AS (DATEPART(YEAR,[DATE])) PERSISTED,
[SEMESTER] AS (CASE WHEN DATEPART(MONTH,[DATE]) < (7) THEN '1' ELSE '2' END) PERSISTED,
[TRIMESTER] AS (CASE WHEN DATEPART(MONTH,[DATE]) < (4) THEN '1' ELSE CASE WHEN DATEPART(MONTH,[DATE]) < (7) THEN '2' ELSE CASE WHEN DATEPART(MONTH,[DATE]) < (10) THEN '3' ELSE '4' END END END) PERSISTED,
[MONTH] AS (CASE WHEN LEN(CONVERT(VARCHAR(2),DATEPART(MONTH,[DATE])))=(1) THEN '0'+ CONVERT(VARCHAR(2),DATEPART(MONTH,[DATE])) ELSE CONVERT(VARCHAR(2),DATEPART(MONTH,[DATE])) END) PERSISTED,
[WEEK] AS (CASE WHEN LEN(CONVERT(VARCHAR(2),DATEPART(WEEK,[DATE])))=(1) THEN '0'+ CONVERT(VARCHAR(2),DATEPART(WEEK,[DATE])) ELSE CONVERT(VARCHAR(2),DATEPART(WEEK,[DATE])) END),
[DAY] AS (CASE WHEN LEN(CONVERT(VARCHAR(2),DATEPART(DAY,[DATE])))=(1) THEN '0'+ CONVERT(VARCHAR(2),DATEPART(DAY,[DATE])) ELSE CONVERT(VARCHAR(2),DATEPART(DAY,[DATE])) END) PERSISTED,
You can go ahead and use the column examples to make whatever other columns you need. For example it may be interesting to stock actual names of days like “Monday”, “Tuesday” etc. This can be done with the DATENAME(DAY, [DATE]) function.
Now you are ready to fill the calendar with dates. To do this we execute our recursive CTE query to fill only the FULLDATE column. The rest will calculate itself thanks to computed columns and, thanks to the PERSISTED clause, will be stocked on the disk and no longer need to be calculated at every execution:
DECLARE @START_DATE DATETIME
DECLARE @ENDDATE DATETIME
SET @START_DATE = '20100101'
SET @ENDDATE = '20151231'
WITH CTE_DATES AS
@START_DATE DateValue UNION ALL SELECT
DateValue + 1
WHERE DateValue + 1 < @ENDDATE)
INSERT INTO CALENDAR ([DATE])
CAST(DateValue AS date)
OPTION (MAXRECURSION 0)
This script inserts every day since the 1st of January 2010 until the last day of 2015. If you need more dates you can of course go ahead and select a bigger date span if need be. Note that we deactivate the maximum recursion of 100 but this is a conscious decision and will not cause infinite recursion because we give the query limits in terms of the max date (WHERE DateValue + 1 < @ENDDATE)
So there you have it. A lovely calendar table to use in JOINs to make your life a lot simpler when you need to search by semester or trimester. The easiest thing to do would be to create a foreign key column in your other tables that relates to the ID of the CALENDAR table.
Date tables of this type are a must in SQL Server Business Intelligence they are usually called “Date Dimensions”. Luckily for those of use that don’t know how to fabricate our own ones they are mostly auto-generated. But sometimes it is useful to create customized version as you can choose whatever calculations you want on the computed columns. Bear in mind also that computed columns and recursive CTE queries are not limited to creating calendar tables.
Here is an extract of the results you will get:
View all posts by Evan Barke