SQL Server Function to return a range of dates

By:   |   Comments (27)   |   Related: > Dates


Problem

Have you ever been asked to create a report showing totals across a range of dates? Not too terribly difficult, is it? A simple GROUP clause and a SUM or other group function and you're done. But, what if you want to include those days that don't have any data? Then, it comes a bit more challenging.

A quick solution is a table function that will create a date range based on the parameters given. Then, you can just join to the results.

Solution

As with most problems in SQL Server, multiple answers exist. For instance, in one variation, the number of days needed was calculated. This number then determined how many cross joins would be used to populate a table variable. The table variable would then be sorted using the ROW_NUMBER function. The individual row number was then added to the starting date to create the date range. Sound complicated? It was.

The other examples that I've found were just as complicated or had some fundamental error that made them only usable under very narrow circumstances. I didn't like any of them, so I developed one of my own. The code and explanation is included below.

Function Definition

The first step to creating the function is to decide exactly what's going to be passed to it. For this function, I chose three parameters. The starting and ending dates were required, of course. After all, it's difficult to create a range if you don't have the range. I also decided to add an increment parameter. This parameter will determine if the range of dates is by day, month or year depending on the value that's passed in.

Since this function will need to return a range of values, it only makes sense to make it a table function. Similarly, it is a date range function, so the table that's returned will contain nothing but dates.

CREATE FUNCTION [dbo].[DateRange]
(     
      @Increment              CHAR(1),
      @StartDate              DATETIME,
      @EndDate                DATETIME
)
RETURNS  
@SelectedRange    TABLE 
(IndividualDate DATETIME)

Creating the Range

Once the parameters have been chosen, it's time to build the function. The code to create the list of dates is fairly simple. I wanted something that could be transported from database to database, so I didn't want to rely on any database object. Instead, I used a Recursive CTE.

The first value that the CTE returns is the start date. The subsequent values is the previous date added to the increment. Therefore, for the second value, the function takes the start date and adds either a day, a week or a month do it depending on the increment chosen. The next value after that adds another, and so forth.

The function knows that it's time to stop when it reaches the end date. Specifically, it checks to make sure the current value is less than or equal to the ending date minus the increment. It does this to ensure that the next increment won't take the date value past the end date.

The results of the CTE are put into the table that will be returned from the function. By default, CTEs have a maximum recursion of 100. So, for this function, only 100 days could be returned. I felt that wasn't sufficient. So, I used the OPTION keyword to reset the MAXRECURSION to 3660, i.e. just over 10 years if using a daily increment. If any more is needed, it'd be fairly simple to change that value.

;WITH cteRange (DateRange) AS (
    SELECT @StartDate
    UNION ALL
    SELECT 
        CASE
            WHEN @Increment = 'd' THEN DATEADD(dd, 1, DateRange)
            WHEN @Increment = 'w' THEN DATEADD(ww, 1, DateRange)
            WHEN @Increment = 'm' THEN DATEADD(mm, 1, DateRange)
        END
    FROM cteRange
    WHERE DateRange <= 
        CASE
            WHEN @Increment = 'd' THEN DATEADD(dd, -1, @EndDate)
            WHEN @Increment = 'w' THEN DATEADD(ww, -1, @EndDate)
            WHEN @Increment = 'm' THEN DATEADD(mm, -1, @EndDate)
        END)
INSERT INTO @SelectedRange (IndividualDate)
SELECT DateRange
FROM cteRange
OPTION (MAXRECURSION 3660);
 

Putting it Together

Now that each part has been built, here's the code for the full function.

CREATE FUNCTION [dbo].[DateRange]
(     
      @Increment              CHAR(1),
      @StartDate              DATETIME,
      @EndDate                DATETIME
)
RETURNS  
@SelectedRange    TABLE 
(IndividualDate DATETIME)
AS 
BEGIN
      ;WITH cteRange (DateRange) AS (
            SELECT @StartDate
            UNION ALL
            SELECT 
                  CASE
                        WHEN @Increment = 'd' THEN DATEADD(dd, 1, DateRange)
                        WHEN @Increment = 'w' THEN DATEADD(ww, 1, DateRange)
                        WHEN @Increment = 'm' THEN DATEADD(mm, 1, DateRange)
                  END
            FROM cteRange
            WHERE DateRange <= 
                  CASE
                        WHEN @Increment = 'd' THEN DATEADD(dd, -1, @EndDate)
                        WHEN @Increment = 'w' THEN DATEADD(ww, -1, @EndDate)
                        WHEN @Increment = 'm' THEN DATEADD(mm, -1, @EndDate)
                  END)
          
      INSERT INTO @SelectedRange (IndividualDate)
      SELECT DateRange
      FROM cteRange
      OPTION (MAXRECURSION 3660);
      RETURN
END
GO

Running a Sample

Now that the function is built, it's time to test. The query below is looking for every day between November 1st, 2012 and November 10th, 2012. I picked these dates at random.

SELECT IndividualDate FROM DateRange('d', '11/01/2012', '11/10/2012')

Here's the results of the query.

DateRange Results

Joining to Another Table

In the example below I am creating a temporary Orders table that has a few orders.  I can use the function to show a list of orders by date as well as any dates that have gaps as shown below.

CREATE TABLE #temp (orderDate DATETIME, orderInfo VARCHAR(50))
INSERT INTO #temp VALUES ('11/1/2012','2 copies of SQL Server 2008')
INSERT INTO #temp VALUES ('11/5/2012','6 copies of SQL Server 2008 R2')
INSERT INTO #temp VALUES ('11/5/2012','10 copies of SQL Server 2012')

SELECT a.IndividualDate FROM DateRange('d', '11/01/2012', '11/10/2012') as a LEFT JOIN #temp as b on a.IndividualDate = b.orderDate

date range join to table

Finding Dates With No Activity

Another report need may be to find dates where there was no activity as shown below.

SELECT *
FROM DateRange('d', '11/01/2012', '11/10/2012') as a 
LEFT JOIN #temp as b on a.IndividualDate = b.orderDate
WHERE b.orderDate IS NULL

date range no activity
Next Steps


sql server categories

sql server webinars

subscribe to mssqltips

sql server tutorials

sql server white papers

next tip



About the author
MSSQLTips author Albert Hetzel Albert Hetzel works as a SQL Server Developer for a Dallas technology company.

This author pledges the content of this article is based on professional experience and not AI generated.

View all my tips



Comments For This Article




Friday, July 14, 2017 - 8:09:08 AM - Greg Robidoux Back To Top (59341)

Hi Sumit,

It looks like you have Oracle code.  This code is for SQL Server.

-Greg


Friday, July 14, 2017 - 2:48:41 AM - sumit Back To Top (59329)

 Hi ,

 

I have daily wise view created having around 30 columns in my system, view name like 'CDR_Details_20170712'. I need to create customize view with required 5 columns.

So need to create proc which run daily basis and create view sysdate -1 means if today is 20170713 then it will create view from 20170701 to 20170712 same like for other days - ex - CDR_Details_20170701 .... CDR_Details_2017074 is sytem generated view , so once proc will rum its should customize view from 20170701 to 20170713, same like it will run daily and craete sysdate - 1 view. I Have issue to pass the date increment value . Urgent help really helpful here. Thanks in adavance.

 

CREATE OR REPLACE PROCEDURE test2_proc as

  str1 varchar(4000);
  v_year varchar2(10);
  v_month varchar2(5);
  v_day varchar2(5);
  cdr_date varchar2(5);
  x number := 0;
  y number := 2;

begin
  for v_all in (select to_char(trunc(sysdate-1),'YYYYMMDD') cdr_date from dual)
  loop
    select substr(v_all.cdr_date, 0, 4)
         , substr(v_all.cdr_date, 5, 2)
         , substr(v_all.cdr_date, 7, 2)
      into v_year
          , v_month
          , v_day
          from dual;
          loop
          x:=x+1;
          if x<=y then
         
    str1 := 'CREATE View '||'TZ'||'_CDR_DETAIL_'||v_all.cdr_date||' AS';
    str1 := str1||' SELECT /*+ full(sd) parallel(sd,4) use_nl(sd,sc) use_nl(sc,ss) */ CDR_FILE_NO,EVENT_START_DATE,EVENT_START_TIME,PRODUCT_GROUP,BILLED_PRODUCT,EVENT_DIRECTION ,ANUM,BNUM,INCOMING_PATH,OUTGOING_PATH,EVENT_DURATION,AMOUNT,CURRENCY ,BILLING_OPERATOR ';         
    str1 := str1||' FROM ' ||'  CDR_DETAIL_'||v_year||v_month||v_day||'_view ';
    str1 := str1||' WHERE'|| 'franchise = '|| 'AIRGB';
    Dbms_Output.put_line (str1||';');
    end if;
    exit when x =2;
    end loop;
  end loop;
END;
/

 

 


Tuesday, January 10, 2017 - 11:25:00 PM - Ben Back To Top (45203)

Thanks for a very well written editorial. Do lots more.

I notice some people complaining about performance. You can find a similar function I create using a tally table, a table with one column which simply has a sequence of numbers, 0 thru 100,000.

So, I can do selects on the tally table,. thereby increasing performance because there is no looping required, or CTE or recursive anything. All that is done is to do a dateadd for N periods using the appropriate date data type for the increment.

If I want twelve months, I select from tally where number < 12. Here is an example

--Tally Table Definition populated with sequential numbers starting from zero

CREATE TABLE Tally
(  
    Number INT NOT NULL
   ,CONSTRAINT PK_Tally PRIMARY KEY CLUSTERED (Number)
)

DECLARE @StartDate DATETIME = '1/1/2017'
SELECT DATEADD(month, number, @StartDate) AS PeriodStart
            DATEADD(ms, -3, DATEADD(month, number + 1, @StartDate) as PeriodEnd
FROM    Tally
WHERE  Number < 12

This query returns the following output

 PeriodStart                     PeriodEnd
 1/1/2017   00:00:00:000  01/31/2017 23:59:59:997
 2/1/2017   00:00:00:000  02/28/2017 23:59:59:997
 3/1/2017   00:00:00:000  03/31/2017 23:59:59:997
 4/1/2017   00:00:00:000  04/31/2017 23:59:59:997
 5/1/2017   00:00:00:000  05/31/2017 23:59:59:997
 6/1/2017   00:00:00:000  06/30/2017 23:59:59:997
 7/1/2017   00:00:00:000  07/31/2017 23:59:59:997
 8/1/2017   00:00:00:000  08/31/2017 23:59:59:997
 9/1/2017   00:00:00:000  09/30/2017 23:59:59:997
10/1/2017   00:00:00:000 10/31/2017 23:59:59:997
11/1/2017   00:00:00:000 11/30/2017 23:59:59:997
12/1/2017   00:00:00:000 12/31/2017 23:59:59:997

If you make this into a table function it can be a query direct function, so fast as possible. Then, any fact with a date attribute will fall between the PeriodStart or PeriodEnd of one of the desired rows. This acts as a filter from the data being grouped because your function only returns the date ranges of interest.

Tie this together with a stored procedure, and you are golden.

If you're interested, you can see this in more detail along with other tips at www.sqlsageadvice.com

Cheers,

Ben


Thursday, June 30, 2016 - 3:50:01 AM - Rajesh Bargode Back To Top (41792)

helloo,

 i want last 4 week count in sql.

my op is-

30 May-03June 06 June-10 June 13 June-17 June 20 June-24 June
45 100 62 56

Monday, April 11, 2016 - 12:29:33 PM - Luggy Back To Top (41193)

Hi Albert,

Thanks for you above code. It's very educative.

I have this code which I need help with. The crux of the code is to print out all dates in a table and break after each week. The week starts from Monday. My sql code prints everything out but I'm not sure how to use loops to achieve the rest.

SELECT id, name, CONVERT(varchar(20), timestamp, 120) as timestamp, hrs, comment , DATEPART(wk,timestamp) as wk

FROM Tablename 

WHERE MONTH(timestamp) = '$month'

and YEAR(timestamp) = '$year'

order by wk

 

How do I break after each week?

Thanks for your help.


Thursday, March 10, 2016 - 11:04:55 AM - Stan Back To Top (40905)

Thanks Albert for taking the time for posting this. I'm currently working on a project where the user will enter the number of weeks, say 14, and a customers name. Starting with today's date, rounded down to the nearest Sunday, which is set up in our ERP (Enterprise Resource Planning) system as the first day of the week and counting out 14 weeks will display the current inventory, arrival date of parts on order, production date of parts produced in house, customer orders, and a running total of current inventory so that we will know if and when inventory will go short. This will be put in a pivot/crosstab tableso that the there will be 14 columns of dates beginning with the past Sunday's date and a Past Due column immediately prior to that. Being a former mainframe programmer my first instinct was a loop with a counter. However, I realized that there was probably an alternate and more efficient way to do this. So, thanks again! Hopefully in the near future I too can start making contributions on this site. 


Wednesday, February 25, 2015 - 8:28:03 AM - Sanjeeva Back To Top (36343)

 Its good approach.

Thanks,

Albert


Wednesday, September 24, 2014 - 11:39:20 AM - Aitch Back To Top (34698)

Albert,  thank you!!! I am in situation where I am disallowed to created a calendar table in the database and this function, which creates only as many days of year are needed at run-time, based on the supplied @start_date and @end_date.  I read through comments about performance (Jeff, Phil, and others), and would like to see implementable code examples posted.  Alternatives that were posted assume permanent calendar table object allowed on database.


Thursday, August 21, 2014 - 1:30:20 PM - Scott Back To Top (34225)

Why using the function DateRange as "parameter" to cteRange?

In the context of ";WITH cteRange (DateRange) AS (", DateRange is a column alias and not a function reference or parameter.  You'll notice the inner SELECT statements have unnamed columns, and they have to be named at some point.

It could also be written ";WITH cteRange AS ( SELECT @StartDate AS DateRange ".


Friday, February 7, 2014 - 4:20:02 AM - Kostas Tsiolis Back To Top (29365)

I would like to mention a bug .

The following query 

SELECT IndividualDate FROM DateRange('m', '29/01/2014', '28/02/2014')

returns only 

2014-01-29 00:00:00.000

Though, the following query 

SELECT IndividualDate FROM DateRange('m', '28/01/2014', '28/02/2014')

returns 

2014-01-28 00:00:00.000

2014-02-28 00:00:00.000

This happens because the CTE is based on difference of the Starting - Ending Date. 

In the first case  , the difference is LESS than a month so only the first month of the range is returned.

In the second case, the difference is MORE than a month so BOTH months are returned.

This issue is fixed by calibrating the Starting Date to the "Start" of the TimeSpan it refers.

Meaning .. if we want to get range for example of months ... then the actual start date is the first day of the month which refers to the start date. In code :

SELECT @StartDate=CASEWHEN @Increment = 'h' THEN CONVERT(VARCHAR(13),@StartDate,120)+':00'

WHEN @Increment = 'd' THEN CONVERT(VARCHAR(10),@StartDate,120)+' 00:00'

WHEN @Increment = 'm' THEN CONVERT(VARCHAR(7),@StartDate,120)+'-01 00:00'

END

The same issue as refered from the solution , exists also for Days and Hours.

(for example try SELECT IndividualDate FROM DateRange('h', '01/01/2014 15:59', '01/01/2014 16:00'))

For any questions , argues , please do not hesitate.

If I helped ..


Wednesday, November 13, 2013 - 11:18:59 AM - Mathieu Laforce Back To Top (27485)

Very interesting article, thanks to all who participated.

I'm new with CTE and i would like to understand something in the example provided by Albert.

Why using the function DateRange as "parameter" to cteRange?

Should'nt it be column names?

Is it implicitely referering to (IndividualDate), the only field in the table returned by the DateRange function?

I can read about WITH usage in different webpages, but i cannot find any explication about this.

 

Thanks in advance!


Wednesday, May 1, 2013 - 5:28:57 PM - David Sumlin Back To Top (23671)

If you have the ability/luxury of installing CLR on the server, there's a free SQL CLR library called SQL# at www.sqlsharp.com  With that library, there are two table valued functions called:  These are even more flexible and are super duper simple to use. 

  • Util_GenerateDateTimeRange
    • @StartDateTime datetime
    • @EndDateTime datetime
    • @Step int  (<> 0)
    • @StepType nvarchar(4000) (allowable values below)
      • year
      • month
      • day
      • hour
      • minute
      • second
      • millisecond
  • Util_GenerateDateTimes
    • @StartDateTime datetime
    • @EndDateTime datetime
    • @TotalDateTimes int  (>= 0)
    • @Step int (<> 0)
    • @StepType nvarchar(4000) (allowable values below)
      • year
      • month
      • day
      • hour
      • minute
      • second
      • millisecond

Sunday, March 10, 2013 - 12:45:42 AM - Sandeep Mittal Back To Top (22685)

Also, Refer this post

http://itdeveloperzone.blogspot.in/2012/11/generate-list-of-dates-in-sql-server.html


Monday, February 25, 2013 - 9:19:46 AM - Joe Celko Back To Top (22399)

Wouldn't it be easier to simply have a CALENDAR table and extract a subset? A century of dates is only 36,525 rows. The cal_date primary key will be in DESC order for speed and the data will be packed solid, no fill. A table done once versus the overhead of repeating a recursive query (really a loop and cursor in disguise) seems much better. 


Tuesday, November 6, 2012 - 7:48:24 PM - Albert Hetzel Back To Top (20236)

Thanks to all who commented.


Friday, November 2, 2012 - 6:42:32 PM - Peter Schott Back To Top (20199)

Interesting function (and discussion). I ended up creating a calendar table, but mostly because we're not just looking up dates, but also other information about those dates (1st day of month, last day of month, week of month, last week of month, number of the occurrence of this weekday in the month - e.g. 2nd Tueday or Last Friday).

I can see where any of these would be useful for a quick calendar control, though. For us, it was helpful to build the Calendar table with all of the other data attached and just pull the appropriate date range. It definitely helped some of our queries quite a bit, especially the ones that calculate dates for some really odd schedules. :)


Friday, November 2, 2012 - 5:50:17 PM - Jeff Moden Back To Top (20198)

First of all, let me say that this is a very well written article and it's a real pleasure to see such nicely formatted and easy to read code.  Well done, Albert.

That being said, the others are correct about the resursive CTE (rCTE for short) being a performance problem.  And, while it may be true that this code might not be executed often thereby supposedly justifying such a performance and resource problem with low rowcounts and low usage, the damage may be done when someone picks up on the method for something with much higher usage even if not used for the same purpose.  A "newbie" or someone desparate for the solution may believe that using rCTEs to do something with a "count" are ok to use simply because they don't contain a WHILE loop.

Nothing could be further from the truth.  Please see the article about this at the following URL.

http://www.sqlservercentral.com/articles/T-SQL/74118/

Not all rCTEs are bad... just the ones that count in a RBAR fashion are.


Friday, November 2, 2012 - 10:43:41 AM - Keith Back To Top (20194)

Thank You Albert

I'm sure Dave and Phil are technically correct but ... It doesn't seem to me that performance is an issue with this.  How many time periods is someone likely to request? 13 or 52 weeks? or 365 days? And I prefer to avoid creating unnessary tables.  Anyway.

I added one parameter @IncrementCount int = null and changed @EndDate to = null. Then I added following to the start of the function which lets you specify how many increments (periods) you want without having to determine the @EndDate to pass.  If the increment is negative you get dates going back x periods up to the supplied @StartDate.  The latter is very helpful in my world - think rolling 13 weeks. I'm sure someone out there can make this a lot prettier.

Call it like this select * from dbo.DateRange('d', '10/1/2012',null,-4) for a quick look.

if isnull(@IncrementCount,0) > 0
    begin

    set @EndDate = CASE
                        WHEN @Increment = 'd' THEN DATEADD(dd, @IncrementCount - 1, @StartDate)
                        WHEN @Increment = 'w' THEN DATEADD(ww, @IncrementCount - 1, @StartDate)
                        WHEN @Increment = 'm' THEN DATEADD(mm, @IncrementCount - 1 , @StartDate)
                    END
    end

if isnull(@IncrementCount,0) < 0
    begin
    set @EndDate = @StartDate
    set @StartDate = CASE
                        WHEN @Increment = 'd' THEN DATEADD(dd, @IncrementCount + 1, @EndDate)
                        WHEN @Increment = 'w' THEN DATEADD(ww, @IncrementCount + 1, @EndDate)
                        WHEN @Increment = 'm' THEN DATEADD(mm, @IncrementCount + 1, @EndDate)
                     END
    end

 

 


Friday, November 2, 2012 - 10:28:13 AM - John Goodwin Back To Top (20192)

###############################
Your method
###############################
-------------------------------
'd', '2012-03-01', '2012-04-01'
Table '#07020F21'. Scan count 0, logical reads 32, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 2, logical reads 193, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

(32 row(s) affected)

(32 row(s) affected)
Table '#07020F21'. Scan count 1, logical reads 1, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
-------------------------------
'd', '2002-03-01', '2012-03-08'
Table '#145C0A3F'. Scan count 0, logical reads 3668, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 2, logical reads 21968, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

(3661 row(s) affected)

(3661 row(s) affected)
Table '#145C0A3F'. Scan count 1, logical reads 8, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

###############################
My method
###############################
-------------------------------
'd', '2012-03-01', '2012-04-01'
(32 row(s) affected)
Table 'NumberValues'. Scan count 1, logical reads 15, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
-------------------------------
'd', '2002-03-01', '2012-03-08'
(3661 row(s) affected)
Table 'NumberValues'. Scan count 1, logical reads 15, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Here's how my method works:

/*
Setup the data
*/
CREATE TABLE [dbo].[NumberValues] ([NumberVal] [int] IDENTITY(1, 1) PRIMARY KEY CLUSTERED) ON [PRIMARY]

SET IDENTITY_INSERT dbo.[NumberValues] ON

DECLARE @StopVal INT

SET @StopVal = 3661;

WITH TEST
AS (
 SELECT 1 VAL
 
 UNION ALL
 
 SELECT T.VAL + 1
 FROM TEST T
 WHERE T.VAL + 1  )
INSERT INTO NumberValues (NumberVal)
SELECT VAL
FROM TEST T
WHERE T.VALOPTION (MAXRECURSION 3660);

SET IDENTITY_INSERT dbo.[NumberValues] OFF

/*
Run the test
*/
SET STATISTICS IO ON

DECLARE @SelectedRange TABLE (IndividualDate DATETIME)
DECLARE @StartDate DATETIME
 , @EndDate DATETIME
 , @Increment CHAR(1)
SET @Increment = 'd'
SET @StartDate = '2002-03-01'
SET @EndDate = '2012-03-08';

WITH cteRange(DateRange) AS (
SELECT
 CASE
 WHEN @Increment = 'd'
  THEN DATEADD(dd, NumberVal-1, @StartDate)
 WHEN @Increment = 'w'
  THEN DATEADD(ww, NumberVal-1, @StartDate)
 WHEN @Increment = 'm'
  THEN DATEADD(mm, NumberVal-1, @StartDate)
 END AS IndividualDate
FROM NumberValues
)

SELECT *
FROM cteRange
WHERE DateRange BETWEEN @StartDate AND @EndDate
SET STATISTICS IO OFF

In short, you join against a table of numbers, and use the numbers instead of trying to loop.

Also note that the spt_values being a similar idea doesn't have a controllable number of rows.

John


Friday, November 2, 2012 - 9:33:57 AM - Paul Back To Top (20190)

Thanks Albert,

For readers that would prefer the output to be resolved to the begining of whatever time bracket is provided (d, m, q, whatever), they can add this block prior to the CTE block to zero out the seed date. 

    -- ---------------------------------------------------
    -- Resolve starting date to zero hour of the first increment
    -- identified by the parameter
    -- ---------------------------------------------------
    set @StartDate = case   
        WHEN @Increment = 'd' THEN DATEADD(day, DATEDIFF(day, 0, @StartDate), 0)
        WHEN @Increment = 'w' THEN DATEADD(WEEK, DATEDIFF(WEEK, 0, @StartDate), 0)
        WHEN @Increment = 'm' THEN DATEADD(MONTH, DATEDIFF(MONTH, 0, @StartDate), 0)
        WHEN @Increment = 'q' THEN DATEADD(QUARTER, DATEDIFF(QUARTER, 0, @StartDate), 0)
        WHEN @Increment = 'y' THEN DATEADD(year, DATEDIFF(year, 0, @StartDate), 0)
        END

... Continue with the rest of the function.


Friday, November 2, 2012 - 5:29:21 AM - Phil Factor Back To Top (20184)

I wouldn't want to assume that a recursive CTE solution will out-perform a properly written WHILE loop. It is dangerous even to assume that the WHILE loop is always bad.

My advice to anyone is to run performance tests on this sort of routine, comparing with other solutions. Whenever I try to guess how a routine will perform, I usually get it wrong.

Dave is absolutely right in his comment. His solution is always likely to be the most effective, and having a number or calendar table is no bar to portability. The key to its good performance is having accurate rowcount estimation for the query optimiser.


Friday, November 2, 2012 - 4:34:23 AM - Dave Ballantyne Back To Top (20182)

Hi Albert,

 

In my experience Recursive CTEs are rarely the most optimal solution.

Even a 'naive'  tally table will outperform this soln

select dateadd(dd,number,'20120101') as Date
 from master.dbo.spt_values
where type='P'
and number between 0 and 364

Taking that one step further and pushing to a calendar table

Create Table CalendarTab
(
  IndividualDate date primary key
)
go
insert into CalendarTab(IndividualDate)
select dateadd(dd,number,'20120101') as Date
 from master.dbo.spt_values
where type='P'
and number between 0 and 364
go

select * from CalendarTab where IndividualDate between '20120101' and '20121230'

Things get even better.

Not only are we faster, but we have statistics so the estimated row counts are spot on.

Would like to hear your thoughts


Thursday, November 1, 2012 - 3:04:10 PM - Paul Dennis Back To Top (20178)

Ok, thanks Albert for the great explanation, much appreciated!


Thursday, November 1, 2012 - 11:19:56 AM - Albert Hetzel Back To Top (20175)

A couple of reasons:

  1. I avoid loops whenever I can. Although I doubt there'd be much of a performance difference in this case, it's not a good practice to create loops if they can be avoided.
  2. When I first created that code, it was part of an on-the-fly query. I needed a single SQL Statement that could be written quickly. It was only later that I split out that code into a function.

Albert


Thursday, November 1, 2012 - 10:22:57 AM - Paul Dennis Back To Top (20173)

Hey there,

Thanks for the great info.  I've accomplished something similar by using a WHILE loop, and I'm wondering if there's a reason you decided to use a recursive CTE instead.  Is there a performance advantage?

Thanks!

Paul


Friday, October 26, 2012 - 5:36:28 AM - Paul Back To Top (20098)

This is excellent, very useful - thank you


Thursday, October 25, 2012 - 10:06:46 AM - Tom Back To Top (20087)

Thanks Albert - good idea.  I had been creating temp tables but I think this makes better sense.

-- Added increments for hourly and quarterly

CREATE FUNCTION [dbo].[DateRange]
(    
      @Increment              CHAR(1),
      @StartDate              DATETIME,
      @EndDate                DATETIME
)
RETURNS 
@SelectedRange    TABLE
(IndividualDate DATETIME)
AS
BEGIN
      ;WITH cteRange (DateRange) AS (
            SELECT @StartDate
            UNION ALL
            SELECT
                  CASE
      WHEN @Increment = 'h' THEN DATEADD(hh, 1, DateRange)
                        WHEN @Increment = 'd' THEN DATEADD(dd, 1, DateRange)
                        WHEN @Increment = 'w' THEN DATEADD(ww, 1, DateRange)
                        WHEN @Increment = 'm' THEN DATEADD(mm, 1, DateRange)
                        WHEN @Increment = 'q' THEN DATEADD(qq, 1, DateRange)
                  END
            FROM cteRange
            WHERE DateRange <=
                  CASE
      WHEN @Increment = 'h' THEN DATEADD(hh, -1, @EndDate)
                        WHEN @Increment = 'd' THEN DATEADD(dd, -1, @EndDate)
                        WHEN @Increment = 'w' THEN DATEADD(ww, -1, @EndDate)
                        WHEN @Increment = 'm' THEN DATEADD(mm, -1, @EndDate)
                        WHEN @Increment = 'q' THEN DATEADD(qq, -1, @EndDate)
                  END)
         
      INSERT INTO @SelectedRange (IndividualDate)
      SELECT DateRange
      FROM cteRange
      OPTION (MAXRECURSION 3660);
      RETURN
END
GO















get free sql tips
agree to terms