Generate Dates recursively in SQL Server - sql

I have some dates I want to calculate which is currently done over several subqueries. Each subsequent subquery uses the result (a date) of the previous query in its calculation. E.g.
DECLARE #Date DATE = '20170101'
SELECT #foo1 = (SELECT TOP 1 dbo.DateFunction(DateField)
FROM [DateTable]
WHERE DateField <= #Date
ORDER BY DateField DESC)
SELECT #foo2 = (SELECT TOP 1 dbo.DateFunction(DateField)
FROM [DateTable]
WHERE DateField <= #foo1
ORDER BY DateField DESC)
....
SELECT #fooN = (SELECT TOP 1 dbo.DateFunction(DateField)
FROM [DateTable]
WHERE DateField <= #fooNMinus1
ORDER BY DateField DESC)
Is it possible (perhaps using CTE) to make a recursive query to achieve this for a specified number of times?

Weeks are almost always 7 days, so you can get the first one and then just add seven days. If so:
WITH dates as (
SELECT MAX(dbo.DateFunction(DateField)) as dte, 1 as counter
FROM [DateTable]
WHERE DateField <= #Date
UNION ALL
SELECT DATEADD(DAY, 7, dte), counter + 1
FROM dates
WHERE counter < #n
)
SELECT dte
FROM dates;

You can use small tally table as below
Declare #d1 date = '2017-01-01'
Declare #d2 date = '2017-12-31'
select top (datediff(day, #d1, #d2)+1) dt = DateAdd(day, Row_Number() over (order by (Select NULL))-1, #d1)
from master..spt_values s1, master..spt_values s2
Or custom tally tables
;with num as
( select * from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) v(n) )
, n1 as (select n1.* from num n1, num n2, num n3, num n4) --numbers generation
select top (datediff(day, #d1, #d2)+1) dt = DateAdd(day, Row_Number() over (order by (Select NULL))-1, #d1)
from n1

Yes, you can use a recursive query. Since top and aggregates are not allowed in the recursive part, you can use the row_number() function instead.
Declare #date date = cast(getdate() as date), #n int = 10
declare #DateTable table (DateField date)
insert into #DateTable values ('2017-05-01'),('2017-05-02'),('2017-05-03'),('2017-05-04'),('2017-05-05'),('2017-05-06'),('2017-05-07'),('2017-05-08'),('2017-05-09'),('2017-05-10'),
('2017-05-11'),('2017-05-12'),('2017-05-13'),('2017-05-14'),('2017-05-15'),('2017-05-16'),('2017-05-17'),('2017-05-18'),('2017-05-19'),('2017-05-20')
;with date_rte as (
select top 1 dbo.DateFunction(DateField) datefield, 0 recursions, cast(1 as bigint) rn
from #dateTable
where datefield <= #date
order by datefield desc
union all
select dbo.DateFunction(DateField), recursions+1, ROW_NUMBER() over (order by d.datefield desc)
from #datetable d
join date_rte r on d.DateField <= r.datefield
where recursions < #n and rn = 1
)
select datefield
from date_rte
where rn=1 and recursions = #n

Related

SQL Server - Breakdown date period

I want to create a query that breakdowns a date period into 10 days sub-periods
So a period of 2022-04-15 to 2022-05-01 should be broken into
2022-04-15 2022-04-24
2022-04-25 2022-05-01
The period could be one day (2022-04-15 to 2022-04-15) or even years
Any help appreciated
Thank you
A Tally would be a much more performant approach:
DECLARE #Start date = '20220415',
#End date = '20220501',
#Days int = 10;
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,#Start,#End)/#Days)
ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM N N1, N N2, N N3, N N4) --Up to 1,000 rows. Add more cross joins for more rows
SELECT DATEADD(DAY, T.I*#Days,#Start),
CASE WHEN DATEADD(DAY, ((T.I+1)*#Days)-1,#Start) > #End THEN #END ELSE DATEADD(DAY, ((T.I+1)*#Days)-1,#Start) END
FROM Tally T;
You can use a recursive cte. A rough outline is as follows:
create table #test (
id int identity primary key,
date1 date,
date2 date
);
insert into #test (date1, date2) values
('2022-04-15', '2022-05-01'),
('2022-04-15', '2022-04-15');
with rcte as (
select id, date1 as date1_, dateadd(day, 10, date1) as date2_, date2 as enddate
from #test
union all
select id, date2_, dateadd(day, 10, date2_), enddate
from rcte
where date2_ <= enddate
)
select id, date1_, dateadd(day, -1, date2_)
from rcte
order by 1, 2
DB<>Fiddle
Does this help?
declare #fromdate date=cast('2022-04-15' as date);
declare #todate date=cast('2022-05-01' as date);
WITH cte_dates(tendays)
AS (
SELECT
#fromdate
UNION ALL
SELECT
case when dateadd(d,10,tendays) > #todate then #todate else dateadd(d,10,tendays) end
FROM
cte_dates
WHERE tendays < dateadd(d,-9,#todate)
)
SELECT
tendays,case when dateadd(d,9,tendays) > #todate then #todate else dateadd(d,9,tendays) end
FROM
cte_dates;
DB<>Fiddle

Adding dates in a range

I need to add dates from 01-01-2011 to 31-12-2014 in format: dd-mm-yyyy, how can I do this? I mean something like this:
SET #Date = '01/01/2011';
WHILE #Date <'31/12/2014'
BEGIN
INSERT INTO Calendar(DataKal) VALUES (#Date);
SET #Date = #Date + 1;
END
I am using SQL Server 2014.
If what you are saying is you want to INSERT a row for each date between 2 dates then the best, and by far fastest, method to do this is with a Tally:
DECLARE #StartDate date = '20110101',
#EndDate date = '20141230'; --Seems odd you want to omit 31 December 2014
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP (DATEDIFF(DAY, #StartDate, #EndDate) + 1)
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 AS I
FROM N N1, N N2, N N3, N N4) --10,000 rows
INSERT INTO dbo.Calendar (DataKal)
SELECT DATEADD(DAY, T.I, #StartDate)
FROM Tally T;
You can use a recursive CTE to define the dates and load them:
with cte as (
select convert(date, '2011-01-01') as dte
union all
select dateadd(day, 1, dte)
from cte
where dte < '2014-12-31'
)
insert into calendar (datakal)
select dte
from cte
option (maxrecursion 0);
Note that this inserts the date using the internal format. If you want to see it in a particular format -- such as dd-mm-yyyy -- then you can add a second column. I would suggest adding the column as a computed column:
alter table calendar add dd_mm_yyyy as (convert(varchar(10), datakal, 105));
Here is a db<>fiddle.

Is that possible to write the below query using CTE recursion?

create table #temp (date date)
declare #X date
set #X = '2016-7-01'
declare #Y date
set #Y = cast (getdate() as date)
while(#X<=#Y)
begin
if (datename(WEEKDAY,#X) = 'Sunday')
insert into #temp values (#X)
set #X = cast(((cast(#X as datetime))+1)as date)
continue
end
select * from #temp
drop table #temp
Is that possible to write the above query using CTE recursion?
You can use a CTE to create a numbers table. You can then use the numbers table to get your dates like so:
Declare #Startdate Datetime = '2016-07-01'
Declare #EndDate Datetime = '2016-08-29'
;with
N0 as (SELECT 1 as n UNION ALL SELECT 1)
,N1 as (SELECT 1 as n FROM N0 t1, N0 t2)
,N2 as (SELECT 1 as n FROM N1 t1, N1 t2)
,N3 as (SELECT 1 as n FROM N2 t1, N2 t2)
,N4 as (SELECT 1 as n FROM N3 t1, N3 t2)
,nums as (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 1)) as num FROM N4)
SELECT DATEADD(day,num-1,#startdate) as thedate
FROM nums
WHERE num <= DATEDIFF(day,#startdate,#enddate) + 1
and datename(WEEKDAY, DATEADD(day,num-1,#startdate)) = 'Sunday'
Each table (N0 to nums) effectively multiplies the number of rows in the previous 'table', so you end up with 65,536 rows of numbers in nums (you can do less or more by adding or removing table NX as required). Then, use the numbers table to add days to your start date(SELECT DATEADD(day,num-1,#startdate) as thedate) , where the dates returned are in your date range, and the weekday is Sunday.
Also, because the numbers in nums start at 1, we use nums-1 in our select, so as to avoid skipping over the first date in our series, effectively giving us DATEADD(day, 0, #startdate) in our first row.
You can try something like explained here: http://blog.sqlauthority.com/2009/12/29/sql-server-get-date-of-all-weekdays-or-weekends-of-the-year/
DECLARE #StartDate DATETIME
DECALRE #EndDate DATETIME
SET #StartDate = '2016-07-01'
SET #EndDate = GETDATE()
;WITH cte AS (
SELECT
1 AS DayID,
#StartDate AS FromDate,
DATENAME(dw, #StartDate) AS Dayname
UNION ALL
SELECT
cte.DayID + 1 AS DayID,
DATEADD(d, 1, cte.FromDate),
DATENAME(dw, DATEADD(d, 1, cte.FromDate)) AS Dayname
FROM cte
WHERE DATEADD(d, 1, cte.FromDate) < #EndDate
)
SELECT FromDate AS Date, Dayname
FROM cte
WHERE Dayname IN ('Sunday')
I'm thinking there's be a more efficient way to do this by using a numbers table and a factor of 7 (depending on how many dates you need to get, possible pre-calc the first sunday from your start date, then join all numbers that are factors of 7 from a numbers table), but the above works well enough also.
;with cte
as
(
select getdate() as datee
union all
select dateadd(day,1,datee)
from cte
where datediff(day,getdate(),datee)<100
)
select * from cte
where datename(WEEKDAY,datee) = 'Sunday'
You will hit max recursion limit as well ,to avoid..that use something like below..
option ( MaxRecursion 0 )
I would solve this using numbers table if i am not constrained by the need to use recursive cte ,which is also way faster than Recursive cte..
select
dateadd(day,n,getdate()) as datee
from numbers
where n<100 and datename(weekday,dateadd(day,n,getdate()))='sunday'
To learn why you need numbers table,check this link..https://dba.stackexchange.com/questions/11506/why-are-numbers-tables-invaluable

7 day average in SQL Server 2014

I need to modify the following T-SQL statement to include a rolling 7 day average of the revenue.
What do I need to include in the following code to achieve that?
SELECT
CAST(create_dtg AS DATE) DATE,
SUM([agent_rev] + [anchor_rev] + [corp_rev] + [offsite_rev]) AS RevenueTotals,
SUM([media_est_cost] + [other_cost]) AS COSTTOTALS
FROM
[dbo].[dw_rpt_traffic]
WHERE
[create_dtg] >= ( Getdate() - 90 )
--GROUP BY CREATE_DTG
--ORDER BY CREATE_DTG ASC
I also tried using Parttion by, however, this returns the same value as the Revenuetotals.
Select a.dte, a.revenuetotals, a.COSTTOTALS, AVG(A.RevenueTotals) OVER (PARTITION BY a.dte ORDER BY a.dte ROWS 7 PRECEDING) as Day7Avg
from
(
select CAST(CREATE_DTG AS DATE) as dte,
SUM([AGENT_REV]+[ANCHOR_REV]+[CORP_REV]+[OFFSITE_REV]) as RevenueTotals,
SUM([MEDIA_EST_COST]+[OTHER_COST]) as COSTTOTALS
FROM [dbo].[dw_rpt_traffic]
where [CREATE_DTG] >= (GetDate() - 90)
GROUP BY CREATE_DTG
) as A
Group by a.dte, a.revenuetotals, a.COSTTOTALS
order by a.dte
Thanks, Karen
For rolling aggregates I typically use an OVER clause with ROWS [...] PRECEDING [...].
WITH cte
AS ( SELECT x.Date
,x.Revenue
,AVG(x.Revenue) OVER ( ORDER BY x.Date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS [MA7]
FROM ( SELECT CAST(t.Date AS DATE) AS [Date]
,SUM(t.Revenue) AS [Revenue]
FROM #tmp t
WHERE CAST(t.Date AS DATE) > CAST(GETDATE() - 96 AS DATE)
GROUP BY CAST(t.Date AS DATE)
) x
)
SELECT c.Date
,c.Revenue
,c.MA7
FROM cte c
WHERE c.Date > CAST(GETDATE() - 90 AS DATE)
ORDER BY c.Date;
The table above was generated with the following:
IF ( OBJECT_ID('tempdb..#tmp') IS NOT NULL )
DROP TABLE #tmp;
CREATE TABLE #tmp
(
[Date] DATETIME
,[Revenue] DECIMAL(18, 2)
);
--
DECLARE #first INT = 0
,#last INT = 200;
WHILE #first < #last
BEGIN
INSERT INTO #tmp
( Date, Revenue )
VALUES ( GETDATE() - #first * 0.5, RAND() * 100000 );
SET #first = #first + 1;
END;
Probably the easiest way uses outer apply:
with rt as (
select CAST(CREATE_DTG AS DATE) as dte,
SUM([AGENT_REV]+[ANCHOR_REV]+[CORP_REV]+[OFFSITE_REV]) as RevenueTotals,
SUM([MEDIA_EST_COST]+[OTHER_COST]) as COSTTOTALS
from [dbo].[dw_rpt_traffic]
where [CREATE_DTG] >= (GetDate() - 90)
)
select rt.*, rolling.avgrt
from rt outer apply
(select avg(rt2.RevenueTotals) as avgrt
from rt rt2
where rt2.dte >= dateadd(day, -6, rt.dte) and
rt2.dte <= rt.dte
) rolling
order by dte;

Query to return +- 30 days from a specific date

I'm trying to figure out how to write a query that will return a table of 61 record that will list a date for each record from the current date.
This is a useful function I use, taken from here:
Explode Dates Between Dates, check and adjust parameter
Just send it Date-30 and Date+30
CREATE FUNCTION [dbo].[ExplodeDates] (#startdate DATETIME, #enddate DATETIME)
RETURNS TABLE
AS
RETURN (
WITH
N0 AS (SELECT 1 AS n UNION ALL SELECT 1)
,N1 AS (SELECT 1 AS n FROM N0 t1, N0 t2)
,N2 AS (SELECT 1 AS n FROM N1 t1, N1 t2)
,N3 AS (SELECT 1 AS n FROM N2 t1, N2 t2)
,N4 AS (SELECT 1 AS n FROM N3 t1, N3 t2)
,N5 AS (SELECT 1 AS n FROM N4 t1, N4 t2)
,N6 AS (SELECT 1 AS n FROM N5 t1, N5 t2)
,nums AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS num FROM N6)
SELECT DATEADD(day, num-1, #startdate) AS thedate
FROM nums
WHERE num <= DATEDIFF(day, #startdate, #enddate) + 1
);
GO
If you don't want the function, you can also simply use it as a query, declaring
#startdate = #myDate - 30 and
#enddate = #myDate + 30
The simplest, and probably most efficient way in SQL-Server to get a list of 61 dates is to use the system table Master.dbo.spt_values:
SELECT [Date] = DATEADD(DAY, number - 30, CAST(CURRENT_TIMESTAMP AS DATE))
FROM Master..spt_values
WHERE Type = 'P'
AND Number <= 60;
Example on SQL Fiddle
EDIT
If you are concerned about using undocumented system tables then this will do the same thing (again with no looping)
WITH T AS
( SELECT Number = ROW_NUMBER() OVER(ORDER BY Object_ID)
FROM sys.all_objects
)
SELECT [Date] = DATEADD(DAY, number - 30, CAST(CURRENT_TIMESTAMP AS DATE))
FROM T
WHERE Number <= 60;
Example on SQL Fiddle
Extensive testing has been done here on the merits of various methods of generating sequences of numbers. My preferred option would always be your own table (e.g. dbo.numbers, or in this case a calendar table).
Try this
;with DateList As
(
select GETDATE() as DateCol
union all
select datecol + 1 from datelist
where DateDiff(d, getdate(),datecol+1) < 31 and DateCol + 1 > GETDATE()
union all
select datecol - 1 from datelist
where DateDiff(d, datecol-1, getdate()) < 31 and DateCol - 1 < GETDATE()
)
select CONVERT(varchar(15), DateCol, 101) DateCol from DateList
order by 1
OPTION (MAXRECURSION 0)
If you want to join other table
declare #t table (code varchar(10));
insert into #t
values ('a'), ('b')
;with DateList As
(
select GETDATE() as DateCol
union all
select datecol + 1 from datelist
where DateDiff(d, getdate(),datecol+1) < 31 and DateCol + 1 > GETDATE()
union all
select datecol - 1 from datelist
where DateDiff(d, datecol-1, getdate()) < 31 and DateCol - 1 < GETDATE()
)
select * from DateList, #t
OPTION (MAXRECURSION 0)
In my opinion, the best way to approach this is not to use recursive ctes, temp tables, or system tables, but rather to create and reuse a date lookup table. Create the lookup table once, and then you can use it as needed.
From there, it's really easy to generate a list of dates:
select *
from datelookup
where datefull >= dateadd(day,-30,convert(varchar(10), getDate(), 120))
and datefull <= dateadd(day,30,convert(varchar(10), getDate(), 120));
SQL Fiddle Demo (includes sample code to create such a table)
This T-SQL code will generate your table:
DECLARE #dates TABLE (date_item DATE)
DECLARE #day DATE = DATEADD(DAY, -30, N'2013-05-02')
WHILE #day <= DATEADD(DAY, 30, N'2013-05-02')
BEGIN
INSERT INTO #dates (date_item) SELECT #day
SET #day = DATEADD(DAY, 1, #day)
END
The result is in #dates. Obviously you will need to set the desired value for the center date in place of N'2013-05-02'