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

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

Related

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.

Generate Dates recursively in SQL Server

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

Get all dates between 2 dates in SQL Server stored procedure

I 'm using SQL Server 2014 and I have a table Attendance. This table has 2 columns AttdDate and Status. I want to create a stored procedure that returns list of dates between 2 dates, AttdDate and status. And if the AttdDate is in this list (dates list) status should be true else status should be false.
Any advice? Thanks
CREATE PROCEDURE sp_Attendance #Start DATETIME, #End DATETIME
AS
BEGIN
DECLARE #NumDays INT;
-- This will give you the number of days between your start date and end date.
SELECT #NumDays = DATEDIFF(DAY, #Start, #End) + 1;
WITH CTE AS (
SELECT TOP (#Numdays)
/*
ROW_NUMBER() OVER (ORDER BY a.object_id) will give you an integer from 1 to #NumDays becuase of TOP (#NumDays)
ROW_NUMBER() OVER (ORDER BY a.object_id) - 1 will give you an integer from 0 to #NumDays
DATEADD(DAY, ROW_NUMBER(), #Start) -- This will add the integer from the row number statement to your start date.
i.e.
#Start + 0
#Start + 1
#Start + 2
etc
etc
#Start + #NumDays
*/
DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY a.object_id) - 1, #Start) AS AttdDate
FROM
sys.all_columns a
CROSS JOIN
sys.all_columns b)
SELECT
c.AttdDate,
CASE WHEN a.AttdDate IS NOT NULL THEN 1 ELSE 0 END AS Status
FROM
CTE c
LEFT JOIN
Attendance a
ON c.AttdDate = a.AttdDate;
END;
You can use a recursive query for this:
WITH q(d) AS
(
SELECT #mindate
UNION ALL
SELECT DATEADD(day, 1 d)
FROM q
WHERE d < #maxdate
)
SELECT *
FROM q
OPTION (MAXRECURSION 0)
, however, its performance would be quite poor for longer ranges.
I usually just create a table with all possible dates up to year 2100 in my SQL Server databases:
SELECT *
FROM dates
WHERE d BETWEEN #mindate AND #maxdate

T-SQL to select every nth date from column of dates

I have a column of dates. They are all workdays. I would like to generate a list of dates that are 'n' days apart. For example, starting with the most recent date, I want to find the date n days before it, 2n days before it, 3n days before it, etc. I could use a while loop but I wanted to know if I could use SQL set operations instead. Can it be done?
Find the difference between the most_recent_date and dates in your table, then use the modulo function, where n is the interval.
SELECT date
FROM my_table
WHERE mod(most_recent_date - date, n) = 0
This is the perfect case for a CTE:
DECLARE #LastDate datetime;
DECLARE #N int;
DECLARE #NCoefficientMax;
SELECT #N = 1, #NCoefficientMax = 10;
SELECT #LastDate = MyDate
FROM MyTable
ORDER BY MyDate DESC
WITH mycte
AS
(
SELECT DATEADD(dd, #N, #LastDate) AS NextDate, #N AS NCoefficient
UNION ALL
SELECT DATEADD(dd, #N, NextDate), #N + NCoefficient AS NCoefficient
FROM mycte WHERE NCoefficient < #NCoefficientMax
)
SELECT NextDate FROM mycte
Where #NCoefficientMax is the max coefficient for N.
You can use the dateadd funcion
and make select with join to self table.
What that you need to do it -
Insert the result to temporary table,
with additional column then contain the row_number then order like the result
simple example:
declare #t1 table (d datetime, row int)
insert #t1
select d, row_number()over(order by d)
from T1
order by d
select T1A.*, datediff(day,T1A.d,T1B.d) as dif
from #t1 as T1A
left join #t1 as T1B on T1A.row = T1B.row-1
DECLARE #mostRecent datetime2
SELECT #mostRecent = MAX(dateColumn)
FROM table
SELECT columns
FROM table
WHERE (DATEDIFF(day, dateColumn, #mostRecent) % n) = 0

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'