OPTION(MAXRECURSION 0) in SQL Server - sql

I am used this function to execute stored procedure,
FUNCTION [dbo].[Test]
(#d1 DATE,
#d2 DATE,
#nd VARCHAR(MAX))
RETURNS TABLE
AS
RETURN
(WITH AllDates AS
(
SELECT #d1 AS DateOf
UNION ALL
SELECT DATEADD (DAY, 1, DateOf­)
FROM AllDates
WHERE DateOf < #d2
)
SELECT COUNT() SumOfDays
FROM AllDates
WHERE EXISTS (SELECT 1
FROM STRING_SPLIT(#nd,' ')
WHERE DATENAME(weekday,dat­eof) = value)
OPTION (MAXRECURSION 0)
)
The result is supposed to know how many of 'Monday Tuesday Saturday' are between Date1 And Date 2
n.b i need to add OPTION(MAXRECURSION 0) in the function but it's not working ,
But I get this error:
Incorrect syntax near the keyword 'OPTION'.

OPTION clause can be used only at the statement level. So you cannot use it within a query expression inside view definitions or inline TVFs etc. The only way to use it in your case is to create the TVF without the OPTION clause and specify it in the query that uses the TVF. We have a bug that tracks request for allowing use of OPTION clause inside any query expression (for example, if exists() or CTE or view).
Here is the that that you can visit to refer more: https://social.msdn.microsoft.com/Forums/sqlserver/en-US/7c7d5fea-38ad-4bc5-9038-a157e640561f/using-option-clause-within-create-function-statement-for-inline-table-functions?forum=transactsql
So in your example, you must specify the OPTION when you call your function:
FUNCTION [dbo].[Test](#d1 DATE, #d2 DATE, #nd VARCHAR(MAX))
RETURNS TABLE
AS
RETURN (
WITH AllDates AS
(
SELECT #d1 AS DateOf
UNION ALL
SELECT DateAdd(day,1,DateOf­)
FROM AllDates
WHERE DateOf<#d2
)
select COUNT() SumOfDays
FROM AllDates
WHERE EXISTS(SELECT 1
FROM STRING_SPLIT(#nd,' ')
WHERE DATENAME(weekday,dat­eof) = value)
)
Now you can use Option at SELECT statement:
SELECT * FROM [dbo].[Test] ( #d1 , #d2, #nd ) OPTION ( MAXRECURSION 0 )
Hope to help, my friend :))

The obvious answer to stop the recursion error would be to get rid of the recursion. If there's no recursion then there's no problem!
This is very simple to do as well, as you simply use a tally instead. This also has a speed benefit, as a Tally is (significantly) faster than a rCTE. I use an inline tally for this:
CREATE FUNCTION dbo.Test (#D1 date, #D2 date, #nd varchar(100)) --There's no need for MAX
RETURNS TABLE
AS
RETURN (WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP (SELECT DATEDIFF(DAY,#D1,#D2)) --It appears you don't include the last day in your logic
--If it should, add +1 to the DATEDIFF value
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1 AS I
FROM N N1, N N2, N N3, N N4, N N5), --More than enough rows
Dates AS(
SELECT DATEADD(DAY, T.I, #D1) AS D
FROM Tally T)
SELECT COUNT(D.D) AS SumOfDays
FROM Dates D
WHERE EXISTS(SELECT 1
FROM STRING_SPLIT(#nd,' ')
WHERE DATENAME(weekday,D.D) = value))
GO
SELECT *
FROM dbo.Test('20000101','20200101','Sunday Saturday');
GO
DROP FUNCTION dbo.Test;
I change the length of #nd as there's no need for it to be a MAX. The string 'Sunday Monday Tuesday Wednesday Thursday Friday Saturday' is only 56 characters long, so a varchar(100) is more than enough. You may, however, want to use an nvarchar, to ensure this function works for other languages; as a character may end up being lost.

The option is associated with the outermost select:
RETURN (
WITH AllDates AS(
SELECT #d1 AS DateOf
UNION ALL
SELECT DATEADD (DAY, 1, DateOf­)
FROM AllDates
WHERE DateOf < #d2
)
SELECT COUNT() SumOfDays
FROM AllDates
WHERE EXISTS (SELECT 1
FROM STRING_SPLIT(#nd,' ')
WHERE DATENAME(weekday,dat­eof) = value)
)
OPTION (MAXRECURSION 0)
)

Related

USE DATENAME IN SQL SERVER IN STORE PRODECURE

I have this stored procedure with 3 parametrs
-1 : Start Date
-2 : End Date
-3 : Days Name
the function of this stored procedure is get difference between two date without counting some specefic days
CREATE PROCEDURE TEST (#start_date DATE,#end_date DATE,#name_days VARCHAR(MAX))
AS
BEGIN
DECLARE #NUMBER_DAYS INT;
;with AllDates AS
(
SELECT #start_date AS DateOf
UNION ALL
SELECT DateAdd(day,1,DateOf)
FROM AllDates
WHERE DateOf<#end_date
)
select SUM(CASE WHEN DATENAME(weekday,dateof) in (#name_days) THEN 0 ELSE 1 END) as SumOfDays
FROM AllDates;
END
my problem is i need to know if i can add in execute procedure two name of days like the example(But this not working with me someone have any suggestion please ! and how to do it)
EXEC TEST '2020-07-01','2020-07-13','Monday Tuesday'
I would recommend using a Calendar table, but if you are using SQL 2016+ you can accomplish this using the STRING_SPLIT function:
CREATE PROCEDURE TEST (#start_date DATE,#end_date DATE,#name_days VARCHAR(MAX))
AS
BEGIN
DECLARE #NUMBER_DAYS INT;
;with AllDates AS
(
SELECT #start_date AS DateOf
UNION ALL
SELECT DateAdd(day,1,DateOf)
FROM AllDates
WHERE DateOf<#end_date
)
select COUNT(*) SumOfDays
FROM AllDates
WHERE NOT EXISTS(SELECT 1
FROM STRING_SPLIT(#name_days,' ')
WHERE DATENAME(weekday,dateof) = value)
END
One method to do this would be to use a table type parameter. I also switch from your iterative (and slow) method of use an rCTE to a much faster Tally Table.
Ideally, however, if you want to do this on a much more regular basis, you would be better off with a Calendar Table (Use your favourite search Engine, you'll find 1000's of examples on how to build these) rather than an inline Tally.
Anyway, using a table type parameter, you can do something like this:
USE Sandbox;
GO
CREATE TYPE dbo.DayNames AS table(DayName varchar(15));
GO
CREATE PROC dbo.TEST #StartDate date, #EndDate Date, #DayNames dbo.DayNames READONLY AS
BEGIN
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP (SELECT DATEDIFF(DAY, #StartDate, #EndDate) +1)
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2, N N3, N N4), --10,000 days
Dates AS(
SELECT DATEADD(DAY, T.I, #StartDate) AS Date,
DATENAME(WEEKDAY, DATEADD(DAY, T.I, #StartDate)) AS DayName
FROM Tally T)
SELECT COUNT(*) AS NumOfDays
FROM Dates D
JOIN #DayNames DN ON D.DayName = DN.DayName;
END;
GO
DECLARE #DayNames dbo.DayNames;
INSERT INTO #DayNames
VALUES ('Monday'),('Tuesday');
EXEC dbo.TEST '20000101', '20200817', #DayNames;
GO
--Cleanup
DROP PROC dbo.TEST;
DROP TYPE dbo.DayNames;
If you don't want to use a table type parameter, you could pass a delimited list in, and then use a string splitter, such as STRING_SPLIT (in SQL Server 2016+) or A user built function, such as DelimitedSplit8K_LEAD (Google it).

Date generator to table variable

I read about a way to generate a range of dates: Generate a set or sequence without loops. I'm having trouble with applying the method.
This code successfully generates a table of dates from #Start to #End:
;WITH d([Date]) AS (
SELECT DATEADD(DAY, n-1, #Start)
FROM (
SELECT TOP (DATEDIFF(DAY, #Start, #End) + 1)
ROW_NUMBER() OVER (ORDER BY Number) FROM master..spt_values
) AS x(n)
)
SELECT d.Date
FROM d
I'm fairly new to more advanced SQL so this is quite the black box. But it works. So now I wanted to save the result into a table variable for further processing:
DECLARE #Days TABLE ([Date] DATE NULL, Val INT NULL)
The date will have a value associated with it. However simply inserting into the table seems not to be working. Neither the following or wrapping it in yet another select statement works:
INSERT INTO #Days (Date)
WITH d([Date]) AS (
SELECT DATEADD(DAY, n-1, #Start)
FROM (
SELECT TOP (DATEDIFF(DAY, #Start, #End) + 1)
ROW_NUMBER() OVER (ORDER BY Number) FROM master..spt_values
) AS x(n)
)
SELECT d.Date
FROM d
It seems the 'last statement must be terminated with a semicolon' or an 'incorrect syntax near ;' is thrown depending on the presence of the semicolon.
Do you have any directions? I'm confused like mad.
Your problem is not the CTE, it is the INSERT syntax. As explained in the documentation, the CTE comes before the INSERT:
WITH d([Date]) AS (
SELECT DATEADD(DAY, n-1, #Start)
FROM (
SELECT TOP (DATEDIFF(DAY, #Start, #End) + 1)
ROW_NUMBER() OVER (ORDER BY Number) FROM master..spt_values
) AS x(n)
)
INSERT INTO #Days (Date)
SELECT d.Date
FROM d;

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

TSQL: avoid MAXRECURSION limit in a inline function

I have this function that given a initial and final date gives the corresponding year/month in that range:
CREATE FUNCTION [dbo].[fnYearMonth]
(
#Initial Date,
#Final Date
)
RETURNS TABLE
AS
RETURN
With dateRange(StatDate) as
(
select #Initial
union all
select dateadd(month, 1, StatDate)
from dateRange
where dateadd(month, 1, StatDate) <= CAST(DATEADD(month,DATEDIFF(month,0,#Final)+1,0)-1 as Date)
)
select DATEPART(year, StatDate) AS MyYear, DATEPART(month, StatDate) AS MyMonth From dateRange where StatDate <= #Final
The problem is that the default limit of MAXRECURSION of 100 only makes available date ranges of a maximum of 8 years and 4 months. That is insufficient.
I tried using "OPTION (MAXRECURSION 2000);" in the functions that use this function but that didn't work because I called this function in a WITH statement.
My only solution now is to turn this inline function into a multi-statement function and use "OPTION (MAXRECURSION 2000);". But I would prefer to avoid this option for performance reasons. ¿Is any other alternative?
Thanks for the help.
Try adding OPTION (MAXRECURSION 0) or recursion limit you wish at bottom like below..
You also can use a Calendar table to avoid all these calculations which gives the output you need..
I have a calendar table populated in my database,the output is so easy to calculate like below..I recommend having a table instead of repeated calculations
select distinct month,year from dbo.calendar
where dAte>=getdate()-200 and date<=getdate()
If you wish to go with recursive option ,add option(recursion) like below
--this wont work with inline table valued functions,see below demo
Alter FUNCTION [dbo].[fnYearMonth]
(
#Initial Datetime,
#Final Datetime
)
RETURNS TABLE
AS
RETURN
With dateRange as
(
select #Initial as statdate
union all
select dateadd(month, 1, StatDate)
from dateRange
where dateadd(month, 1, StatDate) <= CAST(DATEADD(month,DATEDIFF(month,0,#Final)+1,0)-1 as Datetime)
)
select DATEPART(year, StatDate) AS MyYear, DATEPART(month, StatDate) AS MyMonth
From dateRange
where StatDate <= #Final
OPTION (MAXRECURSION 0);
Update:
MAX Recursion option doesnt work with Inline table valued functions,it only works with multi table valued functions..
Demo:
alter function
dbo.getnum_test
(
#n int
)
returns table
as return
With cte as
(
select #n as n
union all
select #n+1
from cte
)
select * from cte
where n<1000
option (maxrecursion 0)
alter function dbo.itvftest
(
#n int
)
returns
#numbers table
(
n int
)
as
begin
With cte as
(
select #n as n
union all
select n+1
from cte
where cte.n<10000
)
Insert into #numbers
select * from cte
where n<1000
option (maxrecursion 0)
return
end

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