1

I have a list of Member IDs and they have a START_DATE and END_DATE as is below.

MEMBER_ID START_DATE END_DATE
1 2023-01-01 2023-01-31
1 2023-01-01 2023-02-28
1 2023-01-01 2023-03-31

But I need a table that lists each month between the minimum start date and maximum end date like this - THIS IS MY DESIRED OUTPUT:

MEMBER_ID START_DATE
1 2023-01-01
1 2023-02-01
1 2023-03-01

I have tried the below code:

 WITH TEST AS
   (SELECT MEMBER_ID
   ,MIN(START_DATE) AS MIN_START_DATE
   ,MAX(END_DATE) AS MAX_END_DATE
    FROM < INSERT TABLE NAME >
    GROUP BY MEMBER_ID,
         START_DATE,
         END_DATE),

CTE AS (
  select MEMBER_ID,
         dateadd(day, 1 - day(MIN_START_DATE), MIN_START_DATE) as som,
         eomonth(MAX_END_DATE) as eom
   FROM TEST
   GROUP BY MEMBER_ID,
   dateadd(day, 1 - day(MIN_START_DATE), MIN_START_DATE),
   eomonth(MAX_END_DATE)
  union all
  select MEMBER_ID, dateadd(month, 1, som), eomonth(dateadd(month, 1, som))
  from CTE
  where dateadd(month, 1, som) < eom)

 select DISTINCT MEMBER_ID, som
 from CTE

But the result I get is as below:

MEMBER_ID som
1 2023-01-01
1 2023-02-01

It's not returning the last month that should be 2023-03-01.

I'm probably doing something wrong in the recursive CTE. Any thoughts are appreciated.

  • Are you looking for the first of each `END DATE`? Not sure what you are trying to accomplish. – Jatin Morar May 22 '23 at 20:56
  • I need the first of each month between the minimum START DATE (1/1/2023 in this example) and the maximum END_DATE (3/31/2023 in this example). The second table shows my desired output. I understand that the first of each end date would work in this example, but it will not for most situations. – Samantha Thompson May 22 '23 at 20:59
  • The second table is my desired output. I will make it clearer. – Samantha Thompson May 22 '23 at 21:00

3 Answers3

3

Here is an option using an ad-hoc tally/numbers table.

You may notice the TOP 500, you certainly can pick a more reasonable value, but be sure to leave a little wiggle room

Example

Select Distinct 
       A.MEMBER_ID
      ,NewValue = dateadd(month,n,start_date)     
 From YourTable A
 Join (
        Select Top 500 N=-1+Row_Number() Over (Order By (Select NULL)) From master..spt_values n1, master..spt_values n2
      ) B
    on N<=datediff(month,Start_Date,End_Date)

Results

enter image description here

John Cappelletti
  • 79,615
  • 7
  • 44
  • 66
3

This is relatively simple if you have a calendar table in your environment.

Even a simple one that is a list of dates would be beneficial. An example:

CREATE TABLE CAL (cal_date date);
with dates ([Date]) as (
    Select convert(date,'2020-01-01') as [Date] 
    union all 
    Select dateadd(day, 1, [Date])
    from dates
    where [Date] <= '2025-12-31' 
)
INSERT INTO CAL
select [Date]
from dates
option (maxrecursion 32767)

With this you have a table to which you can join to get all the first_of_month's between your two dates:

SELECT DISTINCT MEMBER_ID,
  cal.first_of_month as START_DATE
FROM TEST
   INNER JOIN (SELECT cal_date as first_of_month FROM CAL WHERE DAY(cal_date) = 1) as cal
      ON cal.first_of_month BETWEEN TEST.START_DATE AND TEST.END_DATE

DBFiddle example

A good article with a great write up on creating a calendar table

Also, some good answers on stackoverflow for creating a date dimension or calendar table in your sql server

JNevill
  • 46,980
  • 4
  • 38
  • 63
  • 1
    Do be careful using a recursive cte for this kind of thing. It can become a challenge from a performance perspective because it is basically using a loop. https://www.sqlservercentral.com/articles/hidden-rbar-counting-with-recursive-ctes – Sean Lange May 22 '23 at 21:39
  • I was able to make this work - thank you very much. The data I'm working with only applies to the first 3 months of 2023, so I only needed the "calendar" for that part of the year, which I'm sure helps with performance. – Samantha Thompson May 22 '23 at 21:57
  • To Sean Lange's point, the recursive CTE in this example should only be a one-time thing to create and populate a proper calendar/date-dimension table. I feel like every database should have a calendar table built on day-one since date dimension attributes are so useful. If instead you are generating these dates every time you want to run this then @SeanLange's approach with generate series is superior. – JNevill May 23 '23 at 15:24
  • +100 for having a calendar table and/or a numbers table in every database. – Sean Lange May 23 '23 at 15:54
1

Just for fun I put this together using generate_series which is only available in Azure or 2022. It is lightning fast if you are on a new enough version.

with DateRange as
(
    select a.MEMBER_ID
        , MonthCount = datediff(month, min(start_date), max(end_date))
        , StartDate = min(start_date)
    from Members a
    group by a.MEMBER_ID
)
select MEMBER_ID
    , dateadd(month, x.value - 1, StartDate)
from DateRange
cross apply generate_series(1, MonthCount + 1) x
Sean Lange
  • 33,028
  • 3
  • 25
  • 40