The maximum recursion 100 has been exhausted

余生长醉 提交于 2020-04-17 19:09:56

问题


When I run this query I am getting error when I increase the date to be above months

with calender_cte as (
     select convert(date, '2019-01-01') as startdate, convert(date, '2019-12-31') as enddate
     union all
     select dateadd(day, 1, startdate), enddate
     from calender_cte cc

     where startdate < enddate     

)
SELECT DATEADD (week, datediff(week, 0, cc.StartDate), -1) as 'WeekOf',
       DATEADD (week, datediff(week, 0, cc.StartDate), +5) as 'to'
       --ISNULL(DATEPART(wk, Inter.StartDate), 0) as 'WeekNumber'
FROM calender_cte cc LEFT JOIN
     [DESOutage].[dbo].[OPSInterruption] Inter
     ON Inter.StartDate = CC.StartDate
Group by DATEADD (week, datediff(week, 0, cc.StartDate), -1),
         --ISNULL(DATEPART(wk, Inter.StartDate)),
         DATEADD (week, datediff(week, 0, cc.StartDate), +5);

ERROR: Msg 530, Level 16, State 1, Line 1

The statement terminated. The maximum recursion 100 has been exhausted before statement completion.


回答1:


This behavior is described in the documentation:

To prevent an infinite loop, you can limit the number of recursion levels allowed for a particular statement by using the MAXRECURSION hint and a value between 0 and 32,767 in the OPTION clause of the INSERT, UPDATE, DELETE, or SELECT statement. This lets you control the execution of the statement until you resolve the code problem that is creating the loop. The server-wide default is 100. When 0 is specified, no limit is applied.

You have hit the default limit of 100 iterations (which gives you a little more than 3 months of data).

The way your query is built, there is no risk of infinite loop. So, you can just allow an unlimited number of iterations by adding option (maxrecursion 0) at the end of your query.




回答2:


You need:

with calender_cte as (
     select convert(date, '2019-01-01') as startdate, convert(date, '2019-12-31') as enddate
     union all
     select dateadd(day, 1, startdate), enddate
     from calender_cte cc

     where startdate < enddate     

)
SELECT DATEADD (week, datediff(week, 0, cc.StartDate), -1) as 'WeekOf',
       DATEADD (week, datediff(week, 0, cc.StartDate), +5) as 'to'
       --ISNULL(DATEPART(wk, Inter.StartDate), 0) as 'WeekNumber'
FROM calender_cte cc LEFT JOIN
     [DESOutage].[dbo].[OPSInterruption] Inter
     ON Inter.StartDate = CC.StartDate
Group by DATEADD (week, datediff(week, 0, cc.StartDate), -1),
         --ISNULL(DATEPART(wk, Inter.StartDate)),
         DATEADD (week, datediff(week, 0, cc.StartDate), +5)
OPTION (MAXRECURSION 0);

It's important to note, however, that using a recursive CTE for this will devastate your performance. For this type of thing you want to use rangeAB (code below).

This query:

DECLARE @startdate DATE = '2019-01-01',
        @enddate   DATE = '2019-12-31';

SELECT
  DATEADD(week,datediff(week,0,f.Dt), -1) as 'WeekOf',
  DATEADD(week,datediff(week,0,f.Dt), +5) as 'to'
FROM        core.rangeAB(1,DATEDIFF(DAY,@startdate,@enddate)+1,1,1) AS r
CROSS APPLY (VALUES(DATEADD(DAY,r.RN-1,@startdate)))                AS f(Dt);

returns the same thing as your recursive CTE logic:

with calender_cte as (
     select convert(date, '2019-01-03') as startdate, convert(date, '2019-12-31') as enddate
     union all
     select dateadd(day, 1, startdate), enddate
     from calender_cte cc
     where startdate < enddate
)
SELECT DATEADD (week, datediff(week, 0, cc.StartDate), -1) as 'WeekOf',
       DATEADD (week, datediff(week, 0, cc.StartDate), +5) as 'to'
FROM calender_cte cc; 

Let's test performance. Here I'm setting the start date back a little for stress testing.

SET STATISTICS TIME, IO ON;

DECLARE @startdate DATE = '1800-01-01',
        @enddate   DATE = '2019-12-31';

PRINT CHAR(10)+'rCTE:'+CHAR(10)+REPLICATE('-',90);
with calender_cte as (
     select convert(date, @startdate) as startdate, convert(date, @enddate) as enddate
     union all
     select dateadd(day, 1, startdate), enddate
     from calender_cte cc
     where startdate < enddate     
)
SELECT
  DATEADD(week,datediff(week,0,cc.StartDate), -1) as 'WeekOf',
  DATEADD(week,datediff(week,0,cc.StartDate), +5) as 'to'
FROM calender_cte AS cc
OPTION (MAXRECURSION 0);

PRINT CHAR(10)+'rangeAB:'+CHAR(10)+REPLICATE('-',90);
SELECT
  DATEADD(week,datediff(week,0,f.Dt), -1) as 'WeekOf',
  DATEADD(week,datediff(week,0,f.Dt), +5) as 'to'
FROM        core.rangeAB(1,DATEDIFF(DAY,@startdate,@enddate)+1,1,1) AS r
CROSS APPLY (VALUES(DATEADD(DAY,r.RN-1,@startdate)))                AS f(Dt);

SET STATISTICS TIME, IO OFF;

Results:

rCTE:
------------------------------------------------------------------------------------------
Table 'Worktable'. Scan count 2, logical reads 482119, physical reads 0...
 SQL Server Execution Times: CPU time = 641 ms

rangeAB:
------------------------------------------------------------------------------------------
 SQL Server Execution Times: CPU time = 31 ms

That's 31MS instead of 641MS, a 20X performance improvement. Also note 482119 fewer reads. 0 for RangeAB to be exact. The recursive CTE gets slower, per row, the more rows you throw at it. RangeAB stays linear.

RangeAB:

CREATE FUNCTION core.rangeAB
(
  @Low  BIGINT, -- (start) Lowest  number in the set
  @High BIGINT, -- (stop)  Highest number in the set
  @Gap  BIGINT, -- (step)  Difference between each number in the set
  @Row1 BIT     -- Base: 0 or 1; should RN begin with 0 or 1?
)
/****************************************************************************************
[Purpose]:
 Creates a lazy, in-memory, forward-ordered sequence of up to 531,441,000,000 integers
 starting with @Low and ending with @High (inclusive). RangeAB is a pure, 100% set-based
 alternative to solving SQL problems using iterative methods such as loops, cursors and
 recursive CTEs. RangeAB is based on Itzik Ben-Gan's getnums function for producing a
 sequence of integers and uses logic from Jeff Moden's fnTally function which includes a
 parameter for determining if the "row-number" (RN) should begin with 0 or 1.

 I wanted to use the name "Range" because it functions and performs almost identically to
 the Range function built into Python and Clojure. RANGE is a reserved SQL keyword so I 
 went with "RangeAB". Functions/Algorithms developed using rangeAB can be easilty ported
 over to Python, Clojure or any other programming language that leverages a lazy sequence.
 The two major differences between RangeAB and the Python/Clojure versions are:
   1. RangeAB is *Inclusive* where the other two are *Exclusive". range(0,3) in Python and
      Clojure return [0 1 2], core.rangeAB(0,3) returns [0 1 2 3].
   2. RangeAB has a fourth Parameter (@Row1) to determine if RN should begin with 0 or 1.

[Author]:
 Alan Burstein

[Compatibility]: 
 SQL Server 2008+

[Syntax]:
 SELECT r.RN, r.OP, r.N1, r.N2
 FROM   core.rangeAB(@Low,@High,@Gap,@Row1) AS r;

[Parameters]:
 @Low  = BIGINT; represents the lowest  value for N1.
 @High = BIGINT; represents the highest value for N1.
 @Gap  = BIGINT; represents how much N1 and N2 will increase each row. @Gap is also the 
                 difference between N1 and N2.
 @Row1 = BIT;    represents the base (first) value of RN. When @Row1 = 0, RN begins with 0,
                 when @row = 1 then RN begins with 1.

[Returns]:
 Inline Table Valued Function returns:
 RN = BIGINT; a row number that works just like T-SQL ROW_NUMBER() except that it can 
      start at 0 or 1 which is dictated by @Row1. If you need the numbers: 
      (0 or 1) through @High, then use RN as your "N" value, ((@Row1=0 for 0, @Row1=1),
      otherwise use N1.
 OP = BIGINT; returns the "finite opposite" of RN. When RN begins with 0 the first number 
      in the set will be 0 for RN, the last number in will be 0 for OP. When returning the
      numbers 1 to 10, 1 to 10 is retrurned in ascending order for RN and in descending 
      order for OP.
      Given the Numbers 1 to 3, 3 is the opposite of 1, 2 the opposite of 2, and 1 is the
      opposite of 3. Given the numbers -1 to 2, the opposite of -1 is 2, the opposite of 0
      is 1, and the opposite of 1 is 0.
      The best practie is to only use OP when @Gap > 1; use core.O instead. Doing so will
      improve performance by 1-2% (not huge but every little bit counts)      
 N1 = BIGINT; This is the "N" in your tally table/numbers function. this is your *Lazy* 
      sequence of numbers starting at @Low and incrimenting by @Gap until the next number
      in the sequence is greater than @High.
 N2 = BIGINT; a lazy sequence of numbers starting @Low+@Gap and incrimenting by @Gap. N2
      will always be greater than N1 by @Gap. N2 can also be thought of as:
      LEAD(N1,1,N1+@Gap) OVER (ORDER BY RN)

[Dependencies]:
 N/A

[Developer Notes]:
 1.  core.rangeAB returns one billion rows in exactly 90 seconds on my laptop:
     4X 2.7GHz CPU's, 32 GB - multiple versions of SQL Server (2005-2019)       
 2.  The lowest and highest possible numbers returned are whatever is allowable by a 
     bigint. The function, however, returns no more than 531,441,000,000 rows (8100^3). 
 3.  @Gap does not affect RN, RN will begin at @Row1 and increase by 1 until the last row
     unless its used in a subquery where a filter is applied to RN.
 4.  @Gap must be greater than 0 or the function will not return any rows.
 5.  Keep in mind that when @Row1 is 0 then the highest RN value (ROWNUMBER) will be the 
     number of rows returned minus 1
 6.  If you only need is a sequential set beginning at 0 or 1 then, for best performance
     use the RN column. Use N1 and/or N2 when you need to begin your sequence at any 
     number other than 0 or 1 or if you need a gap between your sequence of numbers. 
 7.  Although @Gap is a bigint it must be a positive integer or the function will
     not return any rows.
 8.  The function will not return any rows when one of the following conditions are true:
       * any of the input parameters are NULL
       * @High is less than @Low 
       * @Gap is not greater than 0
     To force the function to return all NULLs instead of not returning anything you can
     add the following code to the end of the query:

       UNION ALL 
       SELECT NULL, NULL, NULL, NULL
       WHERE NOT (@High&@Low&@Gap&@Row1 IS NOT NULL AND @High >= @Low AND @Gap > 0)

     This code was excluded as it adds a ~5% performance penalty.
 9.  There is no performance penalty for sorting by RN ASC; there is a large performance 
     penalty, however for sorting in descending order. If you need a descending sort the
     use OP in place of RN then sort by rn ASC. 
 10. When setting the @Row1 to 0 and sorting by RN you will see that the 0 is added via
     MERGE JOIN concatination. Under the hood the function is essentially concatinating
     but, because it's using a MERGE JOIN operator instead of concatination the cost 
     estimations are needlessly high. You can circumvent this problem by changing:
     ORDER BY core.rangeAB.RN to: ORDER BY ROW_NUMBER() OVER (ORDER BY (SELECT NULL))

*** Best Practices ***
--===== 1. Using RN (rownumber)
 -- (1.1) The best way to get the numbers 1,2,3...@High (e.g. 1 to 5):
 SELECT r.RN
 FROM   core.rangeAB(1,5,1,1) AS r;

 -- (1.2) The best way to get the numbers 0,1,2...@High (e.g. 0 to 5):
 SELECT r.RN
 FROM   core.rangeAB(0,5,1,0) AS r;

--===== 2. Using OP for descending sorts without a performance penalty
 -- (2.1) Best Practice for getting the numbers 5,4,3,2,1 (5 to 1):
 SELECT   r.OP
 FROM     core.rangeAB(1,5,1,1) AS r 
 ORDER BY R.RN;

 -- (2.2) Best Practice for getting the numbers 5,4,3,2,1,0 (5 to 0):
 SELECT   r.OP 
 FROM     core.rangeAB(0,5,1,0) AS r
 ORDER BY r.RN ASC;

 -- (2.3) (ADVANCED) - Ex 2.2. (above) but with better query plan estimations (compare both)
 SELECT   r.OP 
 FROM     core.rangeAB(0,5,1,0) AS r
 ORDER BY ROW_NUMBER() OVER (ORDER BY (SELECT NULL));
 -- This will leverage concatination operator instead of a merge join union;
 -- This will not improve performance but the exection plan will include better estimations
;
 -- (2.4) (ADVANCED) The BEST way (leveraging core.O)
 SELECT      o.OP
 FROM        core.rangeAB(0,5,1,0) AS r
 CROSS APPLY core.O(0,5,r.RN)      AS o
 ORDER BY    ROW_NUMBER() OVER (ORDER BY (SELECT NULL));
 -- Note that core.rangeAB.Op is best when there are gaps (@Gap > 1)

--===== 3. Using N1
 -- (3.1) To begin with numbers other than 0 or 1 use N1 (e.g. -3 to 3):
 SELECT r.N1
 FROM   core.rangeAB(-3,3,1,1) AS r;

 -- (3.2) ROW_NUMBER() is built in. If you want a ROW_NUMBER() include RN:
 SELECT r.RN, r.N1
 FROM   core.rangeAB(-3,3,1,1) AS r;

 -- (3.3) If you wanted a ROW_NUMBER() that started at 0 you would do this:
 SELECT r.RN, r.N1
 FROM   core.rangeAB(-3,3,1,0) AS r;

 -- (3.4) Ex 3.3. Guaranteed ORDER BY without a sort in the execution plan
 SELECT   r.RN, r.N1
 FROM     core.rangeAB(-3,3,1,0) AS r
 ORDER BY r.RN;

 -- (3.5) Ex 3.4. But with better cost estimations (similar to ex 2.4)
 SELECT   r.RN, r.N1
 FROM     core.rangeAB(-3,3,1,0) AS r
 ORDER BY    ROW_NUMBER() OVER (ORDER BY (SELECT NULL));

--===== 4. Using N2 and @Gap
 -- (4.1) To get 0,10,20,30...100, set @Low to 0, @High to 100 and @Gap to 10:
 SELECT r.N1
 FROM   core.rangeAB(0,100,10,1) AS r;

 -- (4.2) Adding N2
   -- Note that N2=N1+@Gap; this allows you to create a sequence of ranges.
   -- For example, to get (0,10),(10,20),(20,30).... (90,100):
 SELECT r.N1, r.N2
 FROM  core.rangeAB(0,90,10,1) AS r;

 -- (4.3) Remember that a rownumber is included and it can begin at 0 or 1:
 SELECT r.RN, r.N1, r.N2
 FROM   core.rangeAB(0,90,10,1) AS r;

[Examples]:
--===== 1. Generating Sample data (using rangeAB to create "dummy rows")
 -- The query below will generate 10,000 ids and random numbers between 50,000 and 500,000
 SELECT
   someId    = r.RN,
   someNumer = ABS(CHECKSUM(NEWID())%450000)+50001 
 FROM core.rangeAB(1,10000,1,1) AS r;

--===== 2. Create a series of dates; rn is 0 to include the first date in the series
 DECLARE @StartDate DATE = '20180101', @enddate DATE = '20180131';

 SELECT r.RN, calDate = DATEADD(dd, r.RN, @StartDate)
 FROM   core.rangeAB(1, DATEDIFF(dd,@StartDate,@enddate),1,0) AS r;
 GO

--===== 3. Splitting (tokenizing) a string with fixed sized items
 -- given a delimited string of identifiers that are always 7 characters long
 DECLARE @String VARCHAR(1000) = 'A601225,B435223,G008081,R678567';

 SELECT
   itemNumber = r.RN, -- item's ordinal position 
   itemIndex  = r.N1, -- item's position in the string (it's CHARINDEX value)
   item       = SUBSTRING(@String, r.N1, 7) -- item (token)
 FROM core.rangeAB(1, LEN(@String), 8,1) AS r;
 GO

--===== 4. Splitting (tokenizing) a string with random delimiters
 DECLARE @String VARCHAR(1000) = 'ABC123,999F,XX,9994443335';

 SELECT
   itemNumber = ROW_NUMBER() OVER (ORDER BY r.RN), -- item's ordinal position 
   itemIndex  = r.N1+1, -- item's position in the string (it's CHARINDEX value)
   item       = SUBSTRING
               (
                 @String,
                 r.N1+1,
                 ISNULL(NULLIF(CHARINDEX(',',@String,r.N1+1),0)-r.N1-1,8000)
               ) -- item (token)
 FROM  core.rangeAB(0,DATALENGTH(@String),1,1) AS r
 WHERE SUBSTRING(@String,r.N1,1) = ',' OR r.N1 = 0;
 -- logic borrowed from: http://www.sqlservercentral.com/articles/Tally+Table/72993/

--===== 5. Grouping by a weekly intervals
 -- 5.1. how to create a series of start/end dates between @StartDate & @endDate
 DECLARE @StartDate DATE = '1/1/2015', @endDate DATE = '2/1/2015';
 SELECT 
   WeekNbr   = r.RN,
   WeekStart = DATEADD(DAY,r.N1,@StartDate), 
   WeekEnd   = DATEADD(DAY,r.N2-1,@StartDate)
 FROM core.rangeAB(0,datediff(DAY,@StartDate,@EndDate),7,1) AS r;
 GO

 -- 5.2. LEFT JOIN to the weekly interval table

 DECLARE @StartDate DATETIME = '1/1/2015', @endDate DATETIME = '2/1/2015';
 BEGIN
   -- sample data 
   DECLARE @loans TABLE (loID INT, lockDate DATE);
   INSERT  @loans 
   SELECT r.RN, DATEADD(DD, ABS(CHECKSUM(NEWID())%32), @StartDate)
   FROM   core.rangeAB(1,50,1,1) AS r;

   -- solution 
   SELECT 
     WeekNbr   = r.RN,
     WeekStart = dt.WeekStart, 
     WeekEnd   = dt.WeekEnd,
     total     = COUNT(l.lockDate)
   FROM core.rangeAB(0,datediff(DAY,@StartDate,@EndDate),7,1) AS r
   CROSS APPLY (VALUES (
     CAST(DATEADD(DAY,r.N1,@StartDate) AS DATE), 
     CAST(DATEADD(DAY,r.N2-1,@StartDate) AS DATE))) dt(WeekStart,WeekEnd)
   LEFT JOIN @loans l ON l.LockDate BETWEEN  dt.WeekStart AND dt.WeekEnd
   GROUP BY  r.RN, dt.WeekStart, dt.WeekEnd ;
 END;

--===== 6. Identify the first vowel and last vowel in a along with their positions
 DECLARE @String VARCHAR(200) = 'This string has vowels';

 BEGIN
   SELECT TOP(1) Position = r.RN, Letter = SUBSTRING(@String,r.RN,1)
   FROM     core.rangeAB(1,LEN(@String),1,1) AS r
   WHERE    SUBSTRING(@String,r.RN,1) LIKE '%[aeiou]%'
   ORDER BY r.RN;

   -- To avoid a sort in the execution plan we'll use OP instead of RN
   SELECT TOP(1) position = r.OP, letter = SUBSTRING(@String,r.OP,1)
   FROM     core.rangeAB(1,LEN(@String),1,1) AS r
   WHERE    SUBSTRING(@String,r.RN,1) LIKE '%[aeiou]%'
   ORDER BY r.RN;
 END;

-----------------------------------------------------------------------------------------
[Revision History]:
 Rev 00 - 20140518 - Initial Development - AJB
 Rev 01 - 20151029 - Added 65 rows. Now L1=465; 465^3=100.5M. Updated comments - AJB
 Rev 02 - 20180613 - Complete re-design including opposite number column (op)
 Rev 03 - 20180920 - Added additional CROSS JOIN to L2 for 530B rows max - AJB
 Rev 04 - 20190306 - Added inline aliasing function(f): 
                     f.R=(@High-@Low)/@Gap, f.N=@Gap+@Low - AJB
 Rev 05 - 20191122 - Developed this "core" version for open source distribution;
                     updated notes and did some final code clean-up
 Rev 06 - 20200329 - Removed startup predicate that dicatated that:
                     @High >= @Low AND @Gap > 0 AND @Row1 = @Row1.
                     That means that this must be handled outside the function - AJB
*****************************************************************************************/
RETURNS TABLE WITH SCHEMABINDING AS RETURN
WITH
L1(N) AS 
(
  SELECT 1
  FROM (VALUES
   ($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),
   ($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),
   ($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),
   ($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),
   ($),($)) T(N) -- 90 values
),
L2(N)      AS (SELECT 1 FROM L1 a CROSS JOIN L1 b CROSS JOIN L1 c),
iTally(RN) AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 1)) FROM L2 a CROSS JOIN L2 b)
SELECT r.RN, r.OP, r.N1, r.N2
FROM
(
  SELECT
    RN = 0,
    OP = (@High-@Low)/@Gap,
    N1 = @Low,
    N2 = @Gap+@Low
  WHERE @Row1 = 0
  UNION ALL
  SELECT TOP ((@High-@Low)/@Gap+@Row1)
    RN = i.RN,
    OP = (@High-@Low)/@Gap+(2*@Row1)-i.RN,
    N1 = (i.rn-@Row1)*@Gap+@Low,
    N2 = (i.rn-(@Row1-1))*@Gap+@Low
  FROM       iTally AS i
  ORDER BY   i.RN
) AS r;
GO


来源:https://stackoverflow.com/questions/61256694/the-maximum-recursion-100-has-been-exhausted

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!