[UPDATED: Thursday, January 08, 2009 at 11:04:25 AM]
UPDATE:I've revised this concept, so please see the new version: Dynamically generating a table of dates in Microsoft SQL (Part Deux!)
I'm working on some code where I need to show some trend date over various periods of time. One of the issues with working with SQL and aggregate values is that it'll only show you values where there was some data, so if you want to print the number of orders on each day and some days had no orders, they wouldn't show up in the results.
For what I was working on, I needed to ensure that even missing days would show up in my query results. This got me thinking about the best way to accomplish this task. After thinking about the problem for a while, I decided the ideal method would be if I could dynamically generate a table of dates that I could then use in a LEFT OUTER JOIN.
What I came up with is a UDF for MSSQL 2005 that will generate a resultset of dates based upon a start and end dates, an interval and the datepart to implement (see the generateDateTable UDF source code at the bottom of this article.) So, now I can use the following to generate a table containing all the days from 12-1-2008 to 12-31-2008:
select * from dbo.generateDateTable('12-01-2008', '12-31-2008', 'day', 1) /* Since the UDF uses a CTE to generate the data, if you're trying to output a resultset that produces more than 100 rows, you'll want to use the OPTION directive to set the maximum recursion number to use. This is only required if your output would generate more than 100 records. */ option (maxrecursion 32767)
This code would generate a table that looked like:
The nice thing about this UDF is that you can specify any datepart you want and any interval/step to use. So, if you wanted to create for hours in the day, you could do:
dbo.generateDateTable('12-01-2008 8:00:00', '12-01-2008 17:00:00', 'hour', 1)
This would generate a resultset like:
The syntax of the UDF is:
dbo.generateDateTable(start_date, end_date, datepart, step)
The datepart is any valid MSSQL datepart as a string (i.e. d, dd, day, etc.) The step is the incremental value to use and will stop once the date reaches the end_date.
Now, if I want to create an aggregate report that shows the number of orders on each day of the month, I can do:
select -- use the isnull() to make sure counts for dates with no orders is zero dates.date as OrderDate, isnull(orders.OrderCount, 0) as OrderCount from -- generate a table from 12-1-2008 to 12-31-2008 dbo.generateDateTable('12-01-2008', '12-31-2008', 'day', 1) as dates left outer join ( select -- convert the OrderDate to just the date (with the time being 00:00:00) cast(floor(cast(OrderDate as float)) as datetime) as OrderDate , count(TicketId) as OrderCount from Orders where -- grab orders for the range we're showing OrderDate between '12-01-2008' and '12-31-2008' group by -- group by the order date cast(floor(cast(OrderDate as float)) as datetime) ) as orders on dates.date = orders.OrderDate order by dates.date
This will generate a resultset which would look like this:
Finally, I've included the code for the UDF below. Just copy and paste the code and run it in your favorite query tool for MSSQL 2005. The UDF isn't really that complicated—the majority of the code comes from the massive CASE statement used to convert the datepart string into something SQL Server 2005 can actually use (since it uses a hardcoded constant, instead of a string that can be supplied dynamically.)
SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO -- ============================================= -- Author: Dan G. Switzer, II -- Create date: Jan 7, 2009 -- Description: Creates a table with a set of date ranges -- ============================================= CREATE function [dbo].[generateDateTable] ( -- Add the parameters for the function here @start_date datetime , @end_date datetime , @datepart varchar(20) = 'day' , @step int = 1 ) returns table as return ( with getdates (date) as ( select @start_date as date union all select case when @datepart in ('year', 'yy', 'yyyy') then dateadd(yy, @step, date) when @datepart in ('quarter', 'qq', 'q') then dateadd(qq, @step, date) when @datepart in ('month', 'mm', 'm') then dateadd(mm, @step, date) when @datepart in ('dayofyear', 'dy', 'y') then dateadd(dy, @step, date) when @datepart in ('day', 'dd', 'd') then dateadd(dd, @step, date) when @datepart in ('week', 'wk', 'ww') then dateadd(ww, @step, date) when @datepart in ('hour', 'hh') then dateadd(hh, @step, date) when @datepart in ('minute', 'mi', 'n') then dateadd(n, @step, date) when @datepart in ('second', 'ss', 's') then dateadd(s, @step, date) when @datepart in ('millisecond', 'ms') then dateadd(ms, @step, date) else dateadd(dd, @step, date) end as date from getdates where (case when @datepart in ('year', 'yy', 'yyyy') then dateadd(yy, @step, date) when @datepart in ('quarter', 'qq', 'q') then dateadd(qq, @step, date) when @datepart in ('month', 'mm', 'm') then dateadd(mm, @step, date) when @datepart in ('dayofyear', 'dy', 'y') then dateadd(dy, @step, date) when @datepart in ('day', 'dd', 'd') then dateadd(dd, @step, date) when @datepart in ('week', 'wk', 'ww') then dateadd(ww, @step, date) when @datepart in ('hour', 'hh') then dateadd(hh, @step, date) when @datepart in ('minute', 'mi', 'n') then dateadd(n, @step, date) when @datepart in ('second', 'ss', 's') then dateadd(s, @step, date) when @datepart in ('millisecond', 'ms') then dateadd(ms, @step, date) else dateadd(dd, @step, date) end) <= @end_date ) select date from getdates )
6 Comments
Comments for this entry have been disabled.