SQL how to convert row with date range to many rows with each date
Asked Answered
T

3

19

If I have a table that looks like this

begin date      end date        data
 2013-01-01     2013-01-04       7
 2013-01-05     2013-01-06       9

How can I make it be returned like this...

    date         data
 2013-01-01       7
 2013-01-02       7
 2013-01-03       7
 2013-01-04       7
 2013-01-05       9
 2013-01-06       9

One thing I was thinking of doing is to have another table that just has all the dates and then join the table with just dates to the above table using date>=begin date and date<=end date but that seems a little clunky to have to maintain that extra table with nothing but repetitive dates.

In some instances I don't have a data range but just an as of date which basically looks like my first example but with no end date. The end date is implied by the next row's 'as of' date (ie end date should be the next row's as of -1). I had a "solution" for this that uses the row_number() function to get the next value but I suspect that methodology, which the way I'm doing it has a bunch of nested self joins, contributes to very long query times.

Touter answered 3/5, 2013 at 12:29 Comment(3)
You're reinterpreting data, for which you shouldn't use SQL. On the business logic end this is 2~3 lines of code extra without burdening the database with something it was never meant to be able to do easily.Vituline
@Niels Which part are you saying shouldn't happen on the SQL server? Are you saying I should store my data in the format that I want it to be displayed or that I should use another process besides the SQL server to generate the output in the format that I want?Touter
No, you should store data in the DB in a normalized correct indexable fashion, as you are doing now. Presentation is not a DB's task, so do that in the invoking code.Vituline
I
18

Using some sample data...

create table data (begindate datetime, enddate datetime, data int);
insert data select 
 '20130101', '20130104', 7 union all select
 '20130105', '20130106', 9;

The Query: (Note: if you already have a numbers/tally table - use it)

select dateadd(d,v.number,d.begindate) adate, data
  from data d
  join master..spt_values v on v.type='P'
       and v.number between 0 and datediff(d, begindate, enddate)
order by adate;

Results:

|                       COLUMN_0 | DATA |
-----------------------------------------
| January, 01 2013 00:00:00+0000 |    7 |
| January, 02 2013 00:00:00+0000 |    7 |
| January, 03 2013 00:00:00+0000 |    7 |
| January, 04 2013 00:00:00+0000 |    7 |
| January, 05 2013 00:00:00+0000 |    9 |
| January, 06 2013 00:00:00+0000 |    9 |

Alternatively you can generate a number table on the fly (0-99) or as many numbers as you need

;WITH Numbers(number) AS (
  select top(100) row_number() over (order by (select 0))-1
  from sys.columns a
  cross join sys.columns b
  cross join sys.columns c
  cross join sys.columns d
  )
select dateadd(d,v.number,d.begindate) adate, data
  from data d
  join Numbers v on v.number between 0 and datediff(d, begindate, enddate)
order by adate;

SQL Fiddle Demo

Importunacy answered 3/5, 2013 at 12:32 Comment(5)
Despite admittedly being a rather brilliant solution to the problem, the spt_values table is an undocumented feature of SQL Server, and as such not guaranted to keep working, or never change. Ref this topicVituline
I don't believe the table will ever go away, but I have added an alternative. Thanks for the feedbackImportunacy
So glad I found this question & answer. This was part of a test in a job interview. The goal made sense to me, but the solution is tricky. Glad I found your simple and perfect solution!Bonham
The fiddle is failing, does this still work in 2019?Theology
@Notflip sqlfiddle had no working sql2014 hosts, switched link to use sql2017Importunacy
C
3

You can use recursive CTE to get all the dates between two dates. Another CTE is to get ROW_NUMBERs to help you with those missing EndDates.

DECLARE @startDate DATE
DECLARE @endDate DATE

SELECT @startDate = MIN(begindate) FROM Table1
SELECT @endDate = MAX(enddate) FROM Table1

;WITH CTE_Dates AS 
(
    SELECT @startDate AS DT
    UNION ALL
    SELECT DATEADD(DD, 1, DT)
    FROM CTE_Dates
    WHERE DATEADD(DD, 1, DT) <= @endDate
)
,CTE_Data AS 
(
    SELECT *, ROW_NUMBER() OVER (ORDER BY BeginDate) AS RN FROM Table1

)
SELECT DT, t1.data FROM CTE_Dates d
LEFT JOIN CTE_Data t1 on d.DT 
BETWEEN t1.[BeginDate] AND COALESCE(t1.EndDate, 
        (SELECT DATEADD(DD,-1,t2.BeginDate) FROM CTE_Data t2 WHERE t1.RN + 1 = t2.RN))

SQLFiddle DEMO

Conjectural answered 3/5, 2013 at 13:1 Comment(0)
L
0
DECLARE @tblDateRange TABLE (
    Id INT,
    FromDateTime DATETIME,
    ToDateTime DATETIME
)
INSERT INTO @tblDateRange (Id, FromDateTime, ToDateTime)
VALUES
(1, '2023-01-01 10:00:00.000', '2023-01-01 10:00:00.000'),      --starts and ends in same
(2, '2023-02-02 00:00:00.000', '2023-02-04 23:59:59.000'),      --starts and ends in standered
(3, '2023-03-05 10:00:00.000', '2023-03-06 23:59:59.000'),      --starts in middle of another date
(4, '2023-04-07 00:00:00.000', '2023-04-08 21:00:00.000'),      --ends in middle of another date
(5, '2023-05-09 11:00:00.000', '2023-05-11 11:00:00.000'),      --starts and ends in middle of another date
(6, '2023-06-01 10:00:00.000', '2023-06-01 22:00:00.000');      --starts and ends in middle of same date


WITH DateRanges(Id, FromDateTime, ToDateTime, LevelNo, [Date], DateWiseStartDateTime, DateWiseEndDateTime)
AS
(
    SELECT 
        p.*,
        1,
        CAST(p.FromDateTime AS DATE),
        p.FromDateTime,
        IIF(DATEADD(SECOND, -1, DATEADD(DAY, DATEDIFF(DAY, 0, p.FromDateTime) + 1, 0)) < p.ToDateTime, DATEADD(SECOND, -1, DATEADD(DAY, DATEDIFF(DAY, 0, p.FromDateTime) + 1, 0)),  p.ToDateTime)   
    FROM @tblDateRange p
    UNION ALL
    SELECT 
        c.*,
        p.LevelNo + 1,
        CAST(DATEADD(DAY, DATEDIFF(DAY, 0, DATEADD(DAY, 1, p.DateWiseStartDateTime)), 0) AS DATE),
        DATEADD(DAY, DATEDIFF(DAY, 0, DATEADD(DAY, 1, p.DateWiseStartDateTime)), 0),
        IIF(c.ToDateTime < DATEADD(DAY, 1, p.DateWiseEndDateTime), c.ToDateTime, DATEADD(DAY, 1, p.DateWiseEndDateTime))
    FROM @tblDateRange c
    JOIN DateRanges p ON c.Id = p.Id
    WHERE DATEADD(DAY, DATEDIFF(DAY, 0, DATEADD(DAY, 1, p.DateWiseStartDateTime)), 0) < c.ToDateTime    --date wise start datetime < range to datetime
)
SELECT *
FROM DateRanges
ORDER BY Id, LevelNo
OPTION (MAXRECURSION 30000) --max 32767, no limit 0

enter image description here

Likker answered 31/3, 2023 at 20:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.