There are times when you date reliant data that has gaps in it but you need a query that includes days even where data is missing. The data source may be a sales table where there may be days that no sales where made but you want to generate a calendar like cross-tab query for every day in a period. When I was asked by a colleague to help him with a similar problem it struck me that we could leverage a SQL 2005 CTE in a user defined function to act as a date range source for the query on which we could left join and fill in the missing days.
The solution is quite simple, all you need is a CTE that starts and the start date and recursively joins on to itself using DATEADD plus a day until it reaches the end date. Here is the function that I wrote:
The performance of the query is pretty good, but is almost identical to using a temp table/table variable and a loop to generate the same data. On my P4 2.8GHZ machine at home both approaches generate a month's worth of days in 16ms and a year's worth in 30ms. I have to say I was somewhat surprised to find this out since I expected it to be faster but I suppose the inserts into the table variable negate the possible speed up. Regardless it is an interesting approach that I thought I'd share with you.
The solution is quite simple, all you need is a CTE that starts and the start date and recursively joins on to itself using DATEADD plus a day until it reaches the end date. Here is the function that I wrote:
CREATE FUNCTION GetDateRange
(
@StartDate DateTime,
@EndDate DateTime
)
RETURNS @DateRangeList TABLE
(
Date DateTime NOT NULL
)
AS
BEGIN
IF @StartDate > @EndDate
BEGIN
--Unfortunately you cannot raise an error in a UDF so simply return no rows
RETURN
END;
WITH DateRange(Date) AS
(
SELECT
@StartDate Date
UNION ALL
SELECT
DATEADD(day, 1, Date) Date
FROM
DateRange
WHERE
Date < @EndDate
)
INSERT @DateRangeList
SELECT Date
FROM DateRange
--You could remove Maximum Recursion level constraint by specifying a MaxRecusion of zero
OPTION (MaxRecursion 10000);
RETURN
END;
(
@StartDate DateTime,
@EndDate DateTime
)
RETURNS @DateRangeList TABLE
(
Date DateTime NOT NULL
)
AS
BEGIN
IF @StartDate > @EndDate
BEGIN
--Unfortunately you cannot raise an error in a UDF so simply return no rows
RETURN
END;
WITH DateRange(Date) AS
(
SELECT
@StartDate Date
UNION ALL
SELECT
DATEADD(day, 1, Date) Date
FROM
DateRange
WHERE
Date < @EndDate
)
INSERT @DateRangeList
SELECT Date
FROM DateRange
--You could remove Maximum Recursion level constraint by specifying a MaxRecusion of zero
OPTION (MaxRecursion 10000);
RETURN
END;
The performance of the query is pretty good, but is almost identical to using a temp table/table variable and a loop to generate the same data. On my P4 2.8GHZ machine at home both approaches generate a month's worth of days in 16ms and a year's worth in 30ms. I have to say I was somewhat surprised to find this out since I expected it to be faster but I suppose the inserts into the table variable negate the possible speed up. Regardless it is an interesting approach that I thought I'd share with you.