Search code examples
sqlsql-servert-sql

How to generate a range of dates in SQL Server


The title doesn't quite capture what I mean, and this may be a duplicate.

Here's the long version: given a guest's name, their registration date, and their checkout date, how do I generate one row for each day that they were a guest?

Ex: Bob checks in 7/14 and leaves 7/17. I want

('Bob', 7/14), ('Bob', 7/15), ('Bob', 7/16), ('Bob', 7/17) 

as my result.

Thanks!


Solution

  • I would argue that for this specific purpose the below query is about as efficient as using a dedicated lookup table.

    DECLARE @start DATE, @end DATE;
    SELECT @start = '20110714', @end = '20110717';
    
    ;WITH n AS 
    (
      SELECT TOP (DATEDIFF(DAY, @start, @end) + 1) 
        n = ROW_NUMBER() OVER (ORDER BY [object_id])
      FROM sys.all_objects
    )
    SELECT 'Bob', DATEADD(DAY, n-1, @start)
    FROM n;
    

    Results:

    Bob     2011-07-14
    Bob     2011-07-15
    Bob     2011-07-16
    Bob     2011-07-17
    

    Presumably you'll need this as a set, not for a single member, so here is a way to adapt this technique:

    DECLARE @t TABLE
    (
        Member NVARCHAR(32), 
        RegistrationDate DATE, 
        CheckoutDate DATE
    );
    
    INSERT @t SELECT N'Bob', '20110714', '20110717'
    UNION ALL SELECT N'Sam', '20110712', '20110715'
    UNION ALL SELECT N'Jim', '20110716', '20110719';
    
    ;WITH [range](d,s) AS 
    (
      SELECT DATEDIFF(DAY, MIN(RegistrationDate), MAX(CheckoutDate))+1,
        MIN(RegistrationDate)
        FROM @t -- WHERE ?
    ),
    n(d) AS
    (
      SELECT DATEADD(DAY, n-1, (SELECT MIN(s) FROM [range]))
      FROM (SELECT ROW_NUMBER() OVER (ORDER BY [object_id])
      FROM sys.all_objects) AS s(n)
      WHERE n <= (SELECT MAX(d) FROM [range])
    )
    SELECT t.Member, n.d
    FROM n CROSS JOIN @t AS t
    WHERE n.d BETWEEN t.RegistrationDate AND t.CheckoutDate;
    ----------^^^^^^^ not many cases where I'd advocate between!
    

    Results:

    Member    d
    --------  ----------
    Bob       2011-07-14
    Bob       2011-07-15
    Bob       2011-07-16
    Bob       2011-07-17
    Sam       2011-07-12
    Sam       2011-07-13
    Sam       2011-07-14
    Sam       2011-07-15
    Jim       2011-07-16
    Jim       2011-07-17
    Jim       2011-07-18
    Jim       2011-07-19
    

    As @Dems pointed out, this could be simplified to:

    ;WITH natural AS 
    (
      SELECT ROW_NUMBER() OVER (ORDER BY [object_id]) - 1 AS val 
      FROM sys.all_objects
    ) 
    SELECT t.Member, d = DATEADD(DAY, natural.val, t.RegistrationDate) 
      FROM @t AS t INNER JOIN natural 
      ON natural.val <= DATEDIFF(DAY, t.RegistrationDate, t.CheckoutDate);