Finding all dates after a date for a variable number of days - sql

I have a list of dates in a table. For this examples the 1st day of each month. Let's call it table timeperiod with column endTime
endTime
1-1-2019
2-1-2019
3-1-2019
4-1-2019
I want to find all dates x number of days after each date in a list. Lets say x = 4. Then the list should be:
1-1-2019
1-2-2019
1-3-2019
1-4-2019
2-1-2019
2-2-2019
2-3-2019
2-4-2019
3-1-2019
3-2-2019
3-3-2019
3-4-2019
4-1-2019
4-2-2019
4-3-2019
4-4-2019
I have found solutions to find all dates between dates but I keep getting "Subquery returned more than 1 value" error when I try to use it with a list of dates.
Here is an example of something I tried but doesn't work
declare #days DECIMAL = 4
declare #StartDate date = (select convert(varchar, DATEADD(Day, +0, endTime),101) from timeperiod
declare #EndDate date = (select convert(varchar, DATEADD(Day, +#days, endTime),101) from timeperiod;
;WITH cte AS (
SELECT #StartDate AS myDate
UNION ALL
SELECT DATEADD(day,1,myDate) as myDate
FROM cte
WHERE DATEADD(day,1,myDate) <= #EndDate
)
SELECT myDate
FROM cte
OPTION (MAXRECURSION 0)

Here is a row generator that generates 5 rows, 0 to 4:
WITH rg AS (
SELECT 0 AS rn
UNION ALL
SELECT rg.rn + 1
FROM rg
WHERE rn < 4
)
Here we join it with your existing table that has firsts of the month and use DATEADD to add rn numbers of days (between 0 and 4) to the endPeriod. CROSS JOINing it caused the rows in timePeriod to repeat 5 times each:
SELECT
DATEADD(DAY, rg.rn, timePeriod.endTime) as fakeEndTime
FROM
rg CROSS JOIN timePeriod
I wasn't really clear when you say "days X days after the date, say x = 4" - to me if there is a day that is 1-Jan-2000, then the date 4 days after this is 5-Jan-2000
If you only want the 1,2,3 and 4 of Jan make the row generator < 3 instead of < 4

Already +1'd on Caius Jard's recursive cte.
Here is yet another option using an ad-hoc tally table in concert with a CROSS JOIN
Example
Declare #YourTable Table ([endTime] date)
Insert Into #YourTable Values
('1-1-2019')
,('2-1-2019')
,('3-1-2019')
,('4-1-2019')
Select NewDate = dateadd(DAY,N-1,EndTime)
From #YourTable A
Cross Join (
Select Top (4) N=row_number() over (order by (select null))
From master..spt_values N1
) B
Returns
NewDate
2019-01-01
2019-01-02
2019-01-03
2019-01-04
2019-02-01
2019-02-02
2019-02-03
2019-02-04
2019-03-01
2019-03-02
2019-03-03
2019-03-04
2019-04-01
2019-04-02
2019-04-03
2019-04-04

Related

Create a row for each date in a range, and add 1 for each day within a date range for a record in SQL

Suppose I have a date range, #StartDate = 2022-01-01 and #EndDate = 2022-02-01, and this is a reporting period.
In addition, I also have customer records, where each customer has a LIVE Date and a ServiceEndDate (or ServiceEndDate = NULL as they are an ongoing customer)
Some customers may have their Live Date and Service end date range extend outside of the reporting period range. I would only want to report for days that they were a customer in the period.
Name
LiveDate
ServiceEndDate
Tom
2021-10-11
2022-01-13
Mark
2022-11-13
2022-02-15
Andy
2022-01-02
2022-02-10
Rob
2022-01-09
2022-01-14
I would like to create a table where column A is the Date (iterating between every date in the reporting period) and column B is a sum of the number of customers that were a customer on that date.
Something like this
Date
NumberOfCustomers
2022-01-01
2
2022-01-02
3
2022-01-03
3
2022-01-04
3
2022-01-05
3
2022-01-06
3
2022-01-07
3
2022-01-08
3
2022-01-09
4
2022-01-10
4
2022-01-11
4
2022-01-12
4
2022-01-13
4
2022-01-14
3
2022-01-15
3
And so on until the end the #EndDate
Any help would be much appreciated, thanks.
You can join your table to a calendar table containing all the dates you need:
with calendar as
(select cast('2022-01-01' as datetime) as d
union all select dateadd(day, 1, d)
from calendar
where d < '2022-02-01')
select d as "Date", count(*) as NumberOfCustomers
from calendar inner join table_name
on d between LiveDate and coalesce(ServiceEndDate, '9999-12-31')
group by d;
Fiddle
I would personally suggest using a Tally, rather than an rCTE, as a Tally is significantly more performant.
SELECT *
INTO dbo.YourTable
FROM (VALUES('Tom ',CONVERT(date,'2021-10-11 '),CONVERT(date,'2022-01-13')),
('Mark',CONVERT(date,' 2022-11-13'),CONVERT(date,' 2022-02-15')),
('Andy',CONVERT(date,' 2022-01-02'),CONVERT(date,' 2022-02-10')),
('Rob ',CONVERT(date,'2022-01-09 '),CONVERT(date,'2022-01-14')))V(Name,LiveDate,ServiceEndDate);
GO
SELECT *
FROM dbo.YourTable;
GO
DECLARE #StartDate date = '20220101',
#EndDate date = '20220201';
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT 0 AS I
UNION ALL
SELECT TOP (DATEDIFF(DAY, #StartDate, #EndDate))
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2, N N3), --up to 1,000 days
Dates AS(
SELECT DATEADD(DAY, T.I, #StartDate) AS Date
FROM Tally T)
SELECT D.Date,
COUNT(YT.[Name]) AS NumberOfCustomers
FROM Dates D
LEFT JOIN dbo.YourTable YT ON D.[Date] >= YT.LiveDate
AND (D.[Date] <= YT.ServiceEndDate
OR YT.ServiceEndDate IS NULL)
GROUP BY D.[Date]
ORDER BY D.[Date];
GO
DROP TABLE dbo.YourTable;
Note that then results don't reflect your expected results, I suspect your expected results are wrong. For example you have 2 people live on 2022-01-01, however, there is only 1 person who is live on that date: Tom.
This solution will also never have Mark as "live" (the rCTE method in the other answer won't either) as their end date is before their Live date. If someone can have their service end before it started, I would suggest you have a data quality issue, and you should be adding a CHECK CONSTRAINT to the table to ensure that value of ServiceEndDate is >= LiveDate.

SQL: Getting Missing Date Values and Copy Data to Those New Dates

So this seems somewhat weird, but this use case came up, and I have been somewhat struggling trying to figure out how to come about a solution. Let's say I have this data set:
date
value1
value2
2020-01-01
50
2
2020-01-04
23
5
2020-01-07
14
8
My goal is to try and fill in the gap between the two dates while copying whatever values were from the date before it. So for example, the data output I would want is:
date
value1
value2
2020-01-01
50
2
2020-01-02
50
2
2020-01-03
50
2
2020-01-04
23
5
2020-01-05
23
5
2020-01-06
23
5
2020-01-07
14
8
Not sure if this is something I can do with SQL but would definitely take any suggestions.
One approach is to use the window function lead() in concert with an ad-hoc tally table if you don't have a calendar table (highly suggested).
Example
;with cte as (
Select *
,nrows = datediff(day,[date],lead([date],1,[date]) over (order by [date]))
From YourTable A
)
Select date = dateadd(day,coalesce(N-1,0),[date])
,value1
,value2
From cte A
left Join (Select Top 1000 N=Row_Number() Over (Order By (Select NULL)) From master..spt_values n1 ) B
on N<=nRows
Results
date value1 value2
2020-01-01 50 2
2020-01-02 50 2
2020-01-03 50 2
2020-01-04 23 5
2020-01-05 23 5
2020-01-06 23 5
2020-01-07 14 8
EDIT: If you have a calendar table
Select Date = coalesce(B.Date,A.Date)
,value1
,value2
From (
Select Date
,value1
,value2
,Date2 = lead([date],1,[date]) over (order by [date])
From YourTable A
) A
left Join CalendarTable B on B.Date >=A.Date and B.Date< A.Date2
Another option is to use CROSS APPLY. I am not sure how you are determining what range you want from the table, but you can easily override my guess by explicitly defining #s and #e:
DECLARE #s date, #e date;
SELECT #s = MIN(date), #e = MAX(date) FROM dbo.TheTable;
;WITH d(d) AS
(
SELECT #s UNION ALL
SELECT DATEADD(DAY,1,d) FROM d
WHERE d < #e
)
SELECT d.d, x.value1, x.value2
FROM d CROSS APPLY
(
SELECT TOP (1) value1, value2
FROM dbo.TheTable
WHERE date <= d.d
AND value1 IS NOT NULL
ORDER BY date DESC
) AS x
-- OPTION (MAXRECURSION 32767) -- if date range can be > 100 days but < 89 years
-- OPTION (MAXRECURSION 0) -- if date range can be > 89 years
If you don't like the recursive CTE, you could easily use a calendar table (but presumably you'd still need a way to define the overall date range you're after as opposed to all of time).
Example db<>fiddle
In SQL Server you can make a cursor, which iterates over the dates. If it finds values for a given date, it takes those and stores them for later. in the next iteration it can then take the stored values, in case there are no values in the database

Split dates into quarters based on start and end date

I want to split quarters based on a given start and end date.
I have the following table:
table1
ID
start_date
end_date
No. of Quarters
1
01-01-2017
01-01-2018
4
2
01-04-2017
01-10-2018
7
So the result table should be have dates split based on number of quarters and end date.
The result table should look like:
table2
ID
Quarterly Start Date
1
01-01-2017
1
01-04-2017
1
01-07-2017
1
01-10-2017
2
01-04-2017
2
01-07-2017
2
01-10-2017
2
01-01-2018
2
01-04-2018
2
01-07-2018
2
01-10-2018
I found a solution on stackoverflow which states
declare #startDate datetime
declare #endDate datetime
select
#startDate= ET.start_date,
#endDate= ET.end_date
from
table1
;With cte
As
( Select #startDate date1
Union All
Select DateAdd(Month,3,date1) From cte where date1 < #endDate
) select cast(cast( Year(date1)*10000 + MONTH(date1)*100 + 1 as
varchar(255)) as date) quarterlyDates From cte
Since I am new to sql, I am having troubles customizing it to my problem.
Could anyone please recommend a way? Thanks!
If I understand correctly, the recursive CTE would look like:
with cte as (
select id, start_date, num_quarters
from t
union all
select id, dateadd(month, 3, start_date), num_quarters - 1
from cte
where num_quarters > 1
)
select *
from cte;
Here is a db<>fiddle.

Generate a list of dates between 2 dates for more than one record

I am trying to write SQL to generate the following data
Date Count
2018-09-24 2
2018-09-25 2
2018-09-26 2
2018-09-27 2
2018-09-28 2
2018-09-29 1
A sample of the base table I am using is
ID StartDate EndDate
187267 2018-09-24 2018-10-01
187270 2018-09-24 2018-09-30
So I'm trying to get a list of dates between 2 dates and then count how many base data records there are in each date.
I started using a temporary table and attempting to loop through the records to get the results but I'm not sure if this is the right approach.
I have this code so far
WITH ctedaterange
AS (SELECT [Dates] = (select ea.StartWork from EngagementAssignment ea where ea.EngagementAssignmentId IN(SELECT ea.EngagementAssignmentId
FROM EngagementLevel el INNER JOIN
EngagementAssignment ea ON el.EngagementLevelID = ea.EngagementLevelId
WHERE el.JobID = 15072 and ea.AssetId IS NOT NULL))
UNION ALL
SELECT [dates] + 1
FROM ctedaterange
WHERE [dates] + 1 < = (select ea.EndWork from EngagementAssignment ea where ea.EngagementAssignmentId IN(SELECT ea.EngagementAssignmentId
FROM EngagementLevel el INNER JOIN
EngagementAssignment ea ON el.EngagementLevelID = ea.EngagementLevelId
WHERE el.JobID = 15072 and ea.AssetId IS NOT NULL)))
SELECT [Dates], Count([Dates])
FROM ctedaterange
GROUP BY [Dates]
But I get this error
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
I get correct results when the job I use only generates one record in the subselect in the where clause, ie:
SELECT ea.EngagementAssignmentId
FROM EngagementLevel el INNER JOIN
EngagementAssignment ea ON el.EngagementLevelID = ea.EngagementLevelId
WHERE el.JobID = 15047 and ea.AssetId IS NOT NULL
generates one record.
The results look like this:
Dates (No column name)
2018-09-24 02:00:00.000 1
2018-09-25 02:00:00.000 1
2018-09-26 02:00:00.000 1
2018-09-27 02:00:00.000 1
2018-09-28 02:00:00.000 1
2018-09-29 02:00:00.000 1
2018-09-30 02:00:00.000 1
2018-10-01 02:00:00.000 1
you can generate according to your range by changing from and to date
DECLARE
#DateFrom DATETIME = GETDATE(),
#DateTo DATETIME = '2018-10-30';
WITH DateGenerate
AS (
SELECT #DateFrom as MyDate
UNION ALL
SELECT DATEADD(DAY, 1, MyDate)
FROM DateGenerate
WHERE MyDate < #DateTo
)
SELECT
MyDate
FROM
DateGenerate;
Well, if you only have a low date range, you can use a recursive CTE as demonstrated in the other answers. The problem with a recursive CTE is with large ranges, where it starts to be ineffective - So I wanted to show you a different approach, that builds the calendar CTE without using recursion.
First, Create and populate sample table (Please save us this step in your future questions):
DECLARE #T AS TABLE
(
ID int,
StartDate date,
EndDate date
)
INSERT INTO #T (ID, StartDate, EndDate) VALUES
(187267, '2018-09-24', '2018-10-01'),
(187270, '2018-09-24', '2018-09-30')
Then, get the first start date and the number of dates you need in the calendar cte:
DECLARE #DateDiff int, #StartDate Date
SELECT #DateDiff = DATEDIFF(DAY, MIN(StartDate), Max(EndDate)),
#StartDate = MIN(StartDate)
FROM #T
Now, construct the calendar cte based on row_number (that is, unless you already have a numbers (tally) table you can use):
;WITH Calendar(TheDate)
AS
(
SELECT TOP(#DateDiff + 1) DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY ##SPID)-1, #StartDate)
FROM sys.objects t0
-- unremark the next row if you don't get enough records...
-- CROSS JOIN sys.objects t1
)
Note that I'm using row_number() - 1 and therefor have to select top(#DateDiff + 1)
Finally - the query:
SELECT TheDate, COUNT(ID) As NumberOfRecords
FROM Calendar
JOIN #T AS T
ON Calendar.TheDate >= T.StartDate
AND Calendar.TheDate <= T.EndDate
GROUP BY TheDate
Results:
TheDate | NumberOfRecords
2018-09-24 | 2
2018-09-25 | 2
2018-09-26 | 2
2018-09-27 | 2
2018-09-28 | 2
2018-09-29 | 2
2018-09-30 | 2
2018-10-01 | 1
You can see a live demo on rextester.
Can you please try following SQL CTE query where I have used a SQL dates table function [dbo].[DatesTable] which produces a list of dates between min date and max date in the source table
;with boundaries as (
select
min(StartDate) minD, max(EndDate) maxD
from DateRanges
), dates as (
select
dates.[date]
from boundaries
cross apply [dbo].[DatesTable](minD, maxD) as dates
)
select dates.[date], count(*) as [count]
from dates
inner join DateRanges
on dates.date between DateRanges.StartDate and DateRanges.EndDate
group by dates.[date]
order by dates.[date]
The output is as expected
Try this: demo
WITH cte1
AS (SELECT id,sdate,edate from t
union all
select c.id,DATEADD(DAY, 1, c.sdate),c.edate from cte1 c where DATEADD(DAY, 1, c.sdate)<=c.edate
)
SELECT sdate,count(id) as total FROM cte1
group by sdate
OPTION (MAXRECURSION 0)
Output:
sdate total
2018-09-24 2
2018-09-25 2
2018-09-26 2
2018-09-27 2
2018-09-28 2
2018-09-29 2
2018-09-30 1

SQL - creating a list of custom dates between two dates

I am having trouble compiling a query than can do the following:
I have a table which has a startDate and endDate [tblPayments]
I have a column which stores a specific paymentDay [tblPayments]
Data
paymentID startDate endDate paymentDay
1 2016-01-01 2016-12-31 25
2 2015-06-01 2016-06-30 16
I am trying to generate a SELECT query which will split this specific table into separate lines based on the amount of months between these two dates, and set the paymentDay as the day for these queries
Example Output
paymentID expectedDate
1 2016-01-25
1 2016-02-25
1 2016-03-25
1 2016-04-25
1 2016-05-25
1 2016-06-25
1 2016-07-25
1 2016-08-25
1 2016-09-25
1 2016-10-25
1 2016-11-25
1 2016-12-25
2 2015-06-16
2 2015-07-16
2 2015-08-16
2 2015-09-16
2 2015-10-16
2 2015-11-16
2 2015-12-16
2 2016-01-16
2 2016-02-16
2 2016-03-16
2 2016-04-16
2 2016-05-16
I have found a query which will select the months between these dates but its adapting it to my table above, and multiple startDates and endDates I am struggling with
spliting the months
declare #start DATE = '2015-01-01'
declare #end DATE = '2015-12-31'
;with months (date)
AS
(
SELECT #start
UNION ALL
SELECT DATEADD(MM,1,date)
from months
where DATEADD(MM,1,date)<=#end
)
select Datename(MM,date) from months
This query is limited to just one startDate and endDate, so I haven't expanded it to change the DAY of the date.
Use a date table and a simple inner join
DECLARE #tblPayments table (paymentID int identity(1,1), startDate date, endDate date, paymentDay int)
INSERT #tblPayments VALUES
('2016-01-01', '2016-12-31', 25),
('2015-06-01', '2016-06-30', 16)
;WITH dates AS -- Build date within the range of startDate and endDate
(
SELECT MIN(startDate) AS Value, MAX(endDate) AS MaxDate FROM #tblPayments
UNION ALL
SELECT DATEADD(DAY, 1, Value), MaxDate
FROM dates WHERE DATEADD(DAY, 1, Value) <= MaxDate
)
SELECT pay.paymentID, dates.Value AS expectedDate
FROM
#tblPayments pay
INNER JOIN dates ON
dates.Value BETWEEN pay.startDate AND pay.endDate
AND DAY(dates.Value) = paymentDay
OPTION (maxrecursion 0)
I would create an in memory calendar table and then perform a simple query by joining to that:
-- Create a table with all the dates between the min and max dates in the
-- data table
DECLARE #Calendar TABLE
(
[CalendarDate] DATETIME
)
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SELECT #StartDate = MIN(startdate), #EndDate = MAX(enddate) FROM YourDataTable
WHILE #StartDate <= #EndDate
BEGIN
INSERT INTO #Calendar (CalendarDate)
SELECT #StartDate
SET #StartDate = DATEADD(dd, 1, #StartDate)
END
-- Join to return only dates between the start and end date that match the Payment Day
SELECT D.PaymentId, C.CalendarDate FROM YourDataTable D
INNER JOIN #Calendar C ON C.CalendarDate BETWEEN D.StartDate AND D.EndDate
AND DATEPART(day, C.CalendarDate) = D.PaymentDay