【问题标题】:The maximum recursion 100 has been exhausted最大递归100已用完
【发布时间】:2020-07-30 01:45:47
【问题描述】:

当我运行此查询时,当我将日期增加到几个月以上时出现错误

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);

错误: 消息 530,第 16 级,状态 1,第 1 行

语句终止。在语句完成之前,最大递归 100 已用完。

【问题讨论】:

  • 使用Tally,就没有这个问题了。他们也方式更快! :)

标签: sql sql-server tsql date recursive-query


【解决方案1】:

这种行为是described in the documentation:

为防止无限循环,您可以通过使用MAXRECURSION 提示和INSERT、@987654325 的OPTION 子句中的一个介于 0 和 32,767 之间的值来限制特定语句允许的递归级别数@、DELETESELECT 声明。这使您可以控制语句的执行,直到您解决创建循环的代码问题。服务器范围的默认值为 100。指定 0 时,不应用任何限制。

您已达到 100 次迭代的默认限制(这为您提供了 3 个月多一点的数据)。

您的查询的构建方式不存在无限循环的风险。因此,您可以通过在查询末尾添加 option (maxrecursion 0) 来允许无限次数的迭代。

【讨论】:

    【解决方案2】:

    你需要:

    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);
    

    然而,重要的是要注意,为此使用递归 CTE 将 devastate your performance。对于这种类型的东西,你想使用 rangeAB(下面的代码)。

    此查询:

    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);
    

    返回与递归 CTE 逻辑相同的内容:

    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; 
    

    让我们测试一下性能。在这里,我将开始日期设置得稍晚一些,以便进行压力测试。

    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;
    

    结果:

    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
    

    这是 31MS 而不是 641MS,性能提高了 20 倍。另请注意 482119 少读。 RangeAB 准确地说是 0。递归 CTE 变得更慢,每行,你扔给它的行越多。 RangeAB 保持线性。

    范围AB:

    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 incrementing by @Gap until the next number
          in the sequence is greater than @High.
     N2 = BIGINT; a lazy sequence of numbers starting @Low+@Gap and incrementing 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
    

    【讨论】:

      猜你喜欢
      • 2021-09-19
      • 2012-03-27
      • 2017-05-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多