Reputation: 3676
We have a set of records where each record (Job) has a cycle (or frequency) in days that determines when the job appears on somebody's work list. E.G
Job A is generated every 7 days Job B is generated every 14 days.
Each Job also has a start date.
I need to generate a table which contains all the possible job dates between a date range.
Here is my first part of code which generates the possible dates.
DECLARE @ForecastEarliestStartDate as DATETIME, @formStartDate as DATE,@formEndDate as DATE, @cycleInDays as BIGINT;
--set input param values
SET @ForecastEarliestStartDate = CAST('2017-07-03' AS DATETIME)
SET @formStartDate = getdate();
SET @formEndDate = getdate()+60;
SET @cycleInDays = 28;
WITH mycte AS
(
SELECT @ForecastEarliestStartDate DateValue
UNION ALL
SELECT DateValue + @cycleInDays
FROM mycte
WHERE DateValue + @cycleInDays < @formEndDate
)
SELECT *
FROM mycte
WHERE DateValue between @formStartDate and @formEndDate
OPTION (MAXRECURSION 0);
This works fine as for a single Job, but how can I run it against a whole table of Jobs where @ForecastEarliestStartDate, @formStartDate, @formEndDate, @cycleInDays
are fields in a table?
Upvotes: 2
Views: 1052
Reputation: 82020
I'll often use a TVF to create dynamic date/time ranges. Often faster than a recursive CTE and it is parameter driven. You supply the date/time range, datepart, and increment.
For Example
Select * From [dbo].[udf-Range-Date]('2017-07-03',getdate()+60,'DD',7)
Returns
RetSeq RetVal
1 2017-07-03
2 2017-07-10
3 2017-07-17
4 2017-07-24
5 2017-07-31
6 2017-08-07
7 2017-08-14
8 2017-08-21
9 2017-08-28
The UDF if Interested
CREATE FUNCTION [dbo].[udf-Range-Date] (@R1 datetime,@R2 datetime,@Part varchar(10),@Incr int)
Returns Table
Return (
with cte0(M) As (Select 1+Case @Part When 'YY' then DateDiff(YY,@R1,@R2)/@Incr When 'QQ' then DateDiff(QQ,@R1,@R2)/@Incr When 'MM' then DateDiff(MM,@R1,@R2)/@Incr When 'WK' then DateDiff(WK,@R1,@R2)/@Incr When 'DD' then DateDiff(DD,@R1,@R2)/@Incr When 'HH' then DateDiff(HH,@R1,@R2)/@Incr When 'MI' then DateDiff(MI,@R1,@R2)/@Incr When 'SS' then DateDiff(SS,@R1,@R2)/@Incr End),
cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (Select M from cte0) Row_Number() over (Order By (Select NULL)) From cte1 a, cte1 b, cte1 c, cte1 d, cte1 e, cte1 f, cte1 g, cte1 h ),
cte3(N,D) As (Select 0,@R1 Union All Select N,Case @Part When 'YY' then DateAdd(YY, N*@Incr, @R1) When 'QQ' then DateAdd(QQ, N*@Incr, @R1) When 'MM' then DateAdd(MM, N*@Incr, @R1) When 'WK' then DateAdd(WK, N*@Incr, @R1) When 'DD' then DateAdd(DD, N*@Incr, @R1) When 'HH' then DateAdd(HH, N*@Incr, @R1) When 'MI' then DateAdd(MI, N*@Incr, @R1) When 'SS' then DateAdd(SS, N*@Incr, @R1) End From cte2 )
Select RetSeq = N+1
,RetVal = D
From cte3,cte0
Where D<=@R2
)
/*
Max 100 million observations -- Date Parts YY QQ MM WK DD HH MI SS
Syntax:
Select * from [dbo].[udf-Range-Date]('2016-10-01','2020-10-01','YY',1)
Select * from [dbo].[udf-Range-Date]('2016-01-01','2017-01-01','MM',1)
*/
Edit - Working Example of CROSS APPLY
Declare @YourTable table (JobName varchar(50),StartDate date,Interval int)
Insert Into @YourTable values
('Job A','2017-07-03',7)
,('Job B','2017-07-03',14)
Select A.JobName
,JobDate = B.RetVal
From @YourTable A
Cross Apply [dbo].[udf-Range-Date](A.StartDate,getdate()+60,'DD',A.Interval) B
Returns
JobName JobDate
Job A 2017-07-03
Job A 2017-07-10
Job A 2017-07-17
Job A 2017-07-24
Job A 2017-07-31
Job A 2017-08-07
Job A 2017-08-14
Job A 2017-08-21
Job A 2017-08-28
Job B 2017-07-03 -- << Notice differant span for Job B
Job B 2017-07-17
Job B 2017-07-31
Job B 2017-08-14
Job B 2017-08-28
Upvotes: 3
Reputation: 69819
The first thing to note is that a recursive CTE is one of the worst ways to generate a set or series (the worst being with explicit loops). Before going any further I would recommend reading the following series of articles:
For example's sake, I will assume that you don't have a numbers table, and can't create one, so I will use the stacked CTE method. This query will get you a list of numbers from 0 to 99,999.
WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2)
SELECT N = ROW_NUMBER() OVER(ORDER BY N) - 1
FROM N3
If you need more numbers, you can add cross joins, if you need less you can remove them.
You can then just join these numbers to your table of jobs, each time adding (n * CycleInDays) to your start date:
WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2),
Numbers (Number) AS (SELECT N = ROW_NUMBER() OVER(ORDER BY N) - 1 FROM N3)
SELECT t.*,
Iteration = n.Number,
Date = DATEADD(DAY, n.Number * t.cycleInDays, t.ForecastEarliestStartDate)
FROM (VALUES
(1, CONVERT(DATE, '2017-07-03'), CONVERT(DATE, '2017-07-03'), CONVERT(DATE, '2017-09-03'), 28 ),
(2, CONVERT(DATE, '2017-07-03'), CONVERT(DATE, '2017-07-03'), CONVERT(DATE, '2017-09-03'), 19)
) t (JobID, ForecastEarliestStartDate, formStartDate, formEndDate, cycleInDays)
INNER JOIN Numbers AS N
ON t.formEndDate >= DATEADD(DAY, n.Number * t.cycleInDays, t.ForecastEarliestStartDate)
AND t.formStartDate <= DATEADD(DAY, n.Number * t.cycleInDays, t.ForecastEarliestStartDate)
ORDER BY t.JobID, n.Number;
This gives:
JobID ForecastEarliestStartDate formStartDate formEndDate cycleInDays Iteration Date
------------------------------------------------------------------------------------------------------------------
1 2017-07-03 2017-07-03 2017-09-03 28 0 2017-07-03
1 2017-07-03 2017-07-03 2017-09-03 28 1 2017-07-31
1 2017-07-03 2017-07-03 2017-09-03 28 2 2017-08-28
2 2017-07-03 2017-07-03 2017-09-03 19 0 2017-07-03
2 2017-07-03 2017-07-03 2017-09-03 19 1 2017-07-22
2 2017-07-03 2017-07-03 2017-09-03 19 2 2017-08-10
2 2017-07-03 2017-07-03 2017-09-03 19 3 2017-08-29
ADDENDUM
In response to the comments:
Great. So the general idea is that a loop is slower but if you genuinely don't know how man iterations there could be, what then? A loop?
No, as long as your numbers table is big enough to cover the maximum iterations then you don't need a loop. The example I have used with 100,000 rows is enough to cover 273 years of jobs that run every single day. I would have thought this would suffice.
BTW I do have a numbers table. Could you show me how to solve my problem without the need generating it please
Sure, simply remove the CTE that generates the numbers, and change the reference to the Numbers
CTE to whatever your numbers table is called:
SELECT t.*,
Iteration = n.Number,
Date = DATEADD(DAY, n.Number * t.cycleInDays, t.ForecastEarliestStartDate)
FROM (VALUES
(1, CONVERT(DATE, '2017-07-03'), CONVERT(DATE, '2017-07-03'), CONVERT(DATE, '2017-09-03'), 28 ),
(2, CONVERT(DATE, '2017-07-03'), CONVERT(DATE, '2017-07-03'), CONVERT(DATE, '2017-09-03'), 19)
) t (JobID, ForecastEarliestStartDate, formStartDate, formEndDate, cycleInDays)
INNER JOIN dbo.Numbers AS N ---- CHANGE TO WHATEVER YOUR NUMBERS TABLE IS CALLED
ON t.formEndDate >= DATEADD(DAY, n.Number * t.cycleInDays, t.ForecastEarliestStartDate)
AND t.formStartDate <= DATEADD(DAY, n.Number * t.cycleInDays, t.ForecastEarliestStartDate)
ORDER BY t.JobID, n.Number;
Just noticed you are hard coding the Jobs into the query. They exist in another table, so not sure how this solves my issue
I have hard coded the jobs into the query just to emulate the table that you have (I have had to guess a bit since there is not much information about this table in the question). Simply replace the table-value constructor I have used with your actual table. e.g.
SELECT t.*,
Iteration = n.Number,
Date = DATEADD(DAY, n.Number * t.cycleInDays, t.ForecastEarliestStartDate)
FROM dbo.[Your Job Table] AS t
INNER JOIN dbo.[Your Numbers Table] AS N
ON t.formEndDate >= DATEADD(DAY, n.Number * t.cycleInDays, t.ForecastEarliestStartDate)
AND t.formStartDate <= DATEADD(DAY, n.Number * t.cycleInDays, t.ForecastEarliestStartDate)
ORDER BY t.JobID, n.Number;
Upvotes: 2