SQL Server: как ограничить рекурсию CTE только что рекурсивно добавленными строками?

Простой пример

Давайте попробуем более простой пример, чтобы люди могли понять концепции и получить практический пример, который вы можете скопировать и вставить в анализатор запросов SQL:

Представьте себе таблицу Nodes с иерархией:

A
 - B
    - C

Мы можем начать тестирование в Query Analyzer:

CREATE TABLE ##Nodes
(
 NodeID varchar(50) PRIMARY KEY NOT NULL,
 ParentNodeID varchar(50) NULL
)

INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('A', null)
INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('B', 'A')
INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('C', 'B')

Желаемый результат:

ParentNodeID    NodeID    GenerationsRemoved
============    ======    ==================
NULL            A         1
NULL            B         2
NULL            C         3
A               B         1
A               C         2
B               C         1

Теперь предлагаемое выражение CTE с неправильным выводом:

WITH NodeChildren AS
(
   --initialization
   SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
   FROM ##Nodes
   WHERE ParentNodeID IS NULL

   UNION ALL

   --recursive execution
   SELECT P.ParentNodeID, N.NodeID, P.GenerationsRemoved + 1
   FROM NodeChildren AS P
      INNER JOIN ##Nodes AS N
      ON P.NodeID = N.ParentNodeID
)
SELECT ParentNodeID, NodeID, GenerationsRemoved
FROM NodeChildren

Фактический результат:

ParentNodeID    NodeID    GenerationsRemoved
============    ======    ==================
NULL            A         1
NULL            B         2
NULL            C         3

Примечание. Если SQL Server 2005† CTE не может делать то, что я делал раньше в 2000‡, это нормально, и это ответ. И тот, кто ответит «это невозможно», получит награду. Но я подожду несколько дней, чтобы убедиться, что все согласны с тем, что это невозможно, прежде чем я безвозвратно дам 250 репутации за нерешение моей проблемы.

Уголок придирок

† не 2008 г.

‡не прибегая к UDF*, что уже есть решение

* если вы не видите способ улучшить производительность UDF в исходном вопросе


Оригинальный вопрос

у меня есть таблица узлов, каждый из которых имеет родителя, который указывает на другой узел (или на ноль).

Для иллюстрации:

1 My Computer
    2 Drive C
         4 Users
         5 Program Files
         7 Windows
             8 System32
    3 Drive D
         6 mp3

мне нужна таблица, которая возвращает все отношения родитель-потомок и количество поколений между ними

Для всех прямых родительских отношений:

ParentNodeID  ChildNodeID  GenerationsRemoved
============  ===========  ===================
(null)        1            1
1             2            1
2             4            1
2             5            1
2             7            1
1             3            1
3             6            1
7             8            1

Но тогда есть отношения бабушек и дедушек:

ParentNodeID  ChildNodeID  GenerationsRemoved
============  ===========  ===================
(null)        2            2
(null)        3            2
1             4            2
1             5            2
1             7            2
1             6            2
2             8            2

И есть отношения прапрадедушки и прадедушки:

ParentNodeID  ChildNodeID  GenerationsRemoved
============  ===========  ===================
(null)        4            3
(null)        5            3
(null)        7            3
(null)        6            3
1             8            3

Итак, я могу понять базовую инициализацию CTE:

WITH (NodeChildren) AS
{
   --initialization
   SELECT ParentNodeID, NodeID AS ChildNodeID, 1 AS GenerationsRemoved
   FROM Nodes
} 

Теперь проблема в рекурсивной части. Очевидный ответ, конечно, не работает:

WITH (NodeChildren) AS
{
   --initialization
   SELECT ParentNodeID, ChildNodeID, 1 AS GenerationsRemoved
   FROM Nodes

   UNION ALL

   --recursive execution
   SELECT parents.ParentNodeID, children.NodeID, parents.Generations+1
   FROM NodeChildren parents
    INNER JOIN NodeParents children
    ON parents.NodeID = children.ParentNodeID
} 

Msg 253, Level 16, State 1, Line 1
Recursive member of a common table expression 'NodeChildren' has multiple recursive references.

Вся информация, необходимая для создания всего рекурсивного списка, содержится в исходной таблице CTE. Но если это не разрешено, я попробую:

WITH (NodeChildren) AS
{
   --initialization
   SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
   FROM Nodes

   UNION ALL

   --recursive execution
   SELECT parents.ParentNodeID, Nodes.NodeID, parents.Generations+1
   FROM NodeChildren parents
    INNER JOIN Nodes
    ON parents.NodeID = nodes.ParentNodeID
} 

Но это не удается, потому что он не только объединяет рекурсивные элементы, но и продолжает рекурсивно добавлять одни и те же строки снова и снова:

Msg 530, Level 16, State 1, Line 1
The statement terminated. The maximum recursion 100 has been exhausted before statement completion.

В SQL Server 2000 я смоделировал CTE, используя определяемую пользователем функцию (UDF):

CREATE FUNCTION [dbo].[fn_NodeChildren] ()
RETURNS @Result TABLE (
    ParentNodeID int NULL,
    ChildNodeID int NULL,
    Generations int NOT NULL) 
AS  
/*This UDF returns all "ParentNode" - "Child Node" combinations
    ...even multiple levels separated
BEGIN 
    DECLARE @Generations int
    SET @Generations = 1

    --Insert into the Return table all "Self" entries
    INSERT INTO @Result
    SELECT ParentNodeID, NodeID, @Generations
    FROM Nodes
    WHILE @@rowcount > 0 
    BEGIN
        SET @Generations = @Generations + 1
        --Add to the Children table: 
        --  children of all nodes just added 
        -- (i.e. Where @Result.Generation = CurrentGeneration-1)
        INSERT @Result
        SELECT CurrentParents.ParentNodeID, Nodes.NodeID, @Generations
        FROM Nodes
            INNER JOIN @Result CurrentParents
            ON Nodes.ParentNodeID = CurrentParents.ChildNodeID
        WHERE CurrentParents.Generations = @Generations - 1
    END
    RETURN
END

И волшебство, чтобы не взорвать его, было ограничивающим предложением where: WHERE CurrentParents.Generations - @Generations-1

Как вы предотвращаете рекурсивное CTE от рекурсии навсегда?


person Ian Boyd    schedule 11.03.2009    source источник
comment
Честно говоря, я не думаю, что вы можете сделать это простым одношаговым процессом. То, что вы пытаетесь достичь, - это не обычное иерархическое отображение от уровня к уровню, а сочетание нескольких уровней. Это не может быть обработано CTE или другими методами....   -  person marc_s    schedule 23.03.2009


Ответы (8)


Попробуй это:

WITH Nodes AS
(
   --initialization
   SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
   FROM ##Nodes

   UNION ALL

   ----recursive execution
   SELECT P.ParentNodeID, N.NodeID, P.GenerationsRemoved + 1
   FROM Nodes AS P
      INNER JOIN ##Nodes AS N
      ON P.NodeID = N.ParentNodeID
   WHERE P.GenerationsRemoved <= 10

)
SELECT ParentNodeID, NodeID, GenerationsRemoved
FROM Nodes
ORDER BY ParentNodeID, NodeID, GenerationsRemoved

В основном удаление «показать мне только абсолютных родителей» из запроса инициализации; Таким образом, он генерирует результаты, начиная с каждого из них и заканчивая оттуда. Я также добавил в «ГДЕ P.GenerationsRemoved ‹= 10» как улов бесконечной рекурсии (замените 10 на любое число до 100, чтобы оно соответствовало вашим потребностям). Затем добавьте сортировку, чтобы результаты выглядели так, как вы хотели.

person Chris Shaffer    schedule 18.03.2009
comment
Гениальное решение! Если вы по-прежнему получаете ошибку максимальной рекурсии, это связано с тем, что у вас есть циклические ссылки в вашей иерархии. - person Richard Poole; 25.03.2009
comment
Вы можете правильно ограничить рекурсии, используя OPTION (MAXRECURSION nn). Это не правильное решение - person gbn; 26.03.2009
comment
OPTION(MAXRECURSION nn) приводит к сбою оператора; Моя цель с включением предложения WHERE заключалась в том, чтобы гарантировать возврат (очевидно, будут случаи, когда неудача лучше). - person Chris Shaffer; 26.03.2009
comment
Это решение работает для меня там, где MAXRECURSION не будет. У меня особая ситуация с устаревшими данными, когда слишком далекое повторение часто может привести к тому, что данные ссылаются на себя. Это позволяет мне спускаться только на n вглубь, где, если вы установите MAXRECURSION, скажем, 5, он выдаст ошибку на 5. Это решение возвращает первые 5 независимо от того, был ли бы бесконечный цикл. - person Michael K; 09.03.2017

В сторону: у вас есть SQL Server 2008? Это может подойти для типа данных hierarchyid.

person Marc Gravell    schedule 11.03.2009
comment
Нет, 2008 года нет. А в 2003 году, когда мне изначально нужно было решить проблему, люди сказали ждать 2005 года. - person Ian Boyd; 11.03.2009
comment
Кроме того, я не могу получить 2008, установить его, перенести базу данных на 2008, перепроектировать всю систему, убедить клиента купить, установить и настроить 2008 - все это в ближайшие несколько часов. - person Ian Boyd; 18.03.2009

Если я понимаю ваши намерения, вы можете получить результат, выполнив что-то вроде этого:

DECLARE @StartID INT;
SET @StartID = 1;
WITH CTE (ChildNodeID, ParentNodeID, [Level]) AS
(
  SELECT  t1.ChildNodeID, 
          t1.ParentNodeID, 
          0
  FROM tblNodes AS t1
  WHERE ChildNodeID = @StartID
  UNION ALL
  SELECT  t1.ChildNodeID, 
          t1.ParentNodeID, 
          t2.[Level]+1
  FROM tblNodes AS t1
    INNER JOIN CTE AS t2 ON t1.ParentNodeID = t2.ChildNodeID    
)
SELECT t1.ChildNodeID, t2.ChildNodeID, t1.[Level]- t2.[Level] AS GenerationsDiff
FROM CTE AS t1
  CROSS APPLY CTE t2

Это вернет разницу поколений между всеми узлами, вы можете изменить ее для своих нужд.

person Joakim Backman    schedule 12.03.2009
comment
Это не работает, потому что оно начинается только с определенного узла — мне нужны все узлы. Затем все эти узлы дочерние. Затем все дочерние узлы. и т.п. - person Ian Boyd; 18.03.2009

Ну, твой ответ не совсем очевиден :-)

WITH (NodeChildren) AS
{
   --initialization
   SELECT ParentNodeID, ChildNodeID, 1 AS GenerationsRemoved
   FROM Nodes

Эта часть называется «якорной» частью рекурсивного CTE, но на самом деле она должна выбирать только одну или несколько строк из вашей таблицы — это выбирает все!

Я предполагаю, что вам здесь не хватает просто подходящего предложения WHERE:

WITH (NodeChildren) AS
{
   --initialization
   SELECT ParentNodeID, ChildNodeID, 1 AS GenerationsRemoved
   FROM Nodes
   **WHERE ParentNodeID IS NULL**

Однако я боюсь, что ваше требование иметь не только «прямую» иерархию, но и строки «прародитель-потомок», может быть не так просто удовлетворить... обычно рекурсивный CTE всегда будет показывать только один уровень и его прямые подчиненные ( и это вниз по иерархии, конечно) - обычно он не пропускает один, два или даже больше уровней.

Надеюсь это немного поможет.

Марк

person marc_s    schedule 13.03.2009
comment
я не могу ограничить его строками без родителя, потому что тогда он ограничивается строками без родителя. мне также нужно включить строки с родителем. - person Ian Boyd; 18.03.2009

Проблема связана с ограничением рекурсии по умолчанию для Sql Server (100). Если вы попробуете свой пример вверху с удаленным ограничением привязки (также добавленным Order By):

WITH NodeChildren AS
(
   --initialization
   SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
   FROM Nodes

   UNION ALL

   --recursive execution
   SELECT P.ParentNodeID, N.NodeID, P.GenerationsRemoved + 1
   FROM NodeChildren AS P
      inner JOIN Nodes AS N
      ON P.NodeID = N.ParentNodeID
)
SELECT ParentNodeID, NodeID, GenerationsRemoved
FROM NodeChildren
ORDER BY ParentNodeID ASC

Это дает желаемые результаты. Проблема, с которой вы сталкиваетесь, связана с большим количеством строк, которые вы будете повторять более 100 раз, что является пределом по умолчанию. Это можно изменить, добавив option (max recursion x) после вашего запроса, где x — это число от 1 до 32767. x также можно установить равным 0, что не устанавливает ограничений, однако может очень быстро оказать очень пагубное влияние на производительность вашего сервера. Очевидно, что по мере увеличения количества строк в узлах количество рекурсий может расти очень быстро, и я бы избегал этого подхода, если только не было известного верхнего предела для строк в таблице. Для полноты окончательный запрос должен выглядеть так:

 WITH NodeChildren AS
    (
       --initialization
       SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
       FROM Nodes

       UNION ALL

       --recursive execution
       SELECT P.ParentNodeID, N.NodeID, P.GenerationsRemoved + 1
       FROM NodeChildren AS P
          inner JOIN Nodes AS N
          ON P.NodeID = N.ParentNodeID
    )
    SELECT * 
    FROM NodeChildren
    ORDER BY ParentNodeID
    OPTION (MAXRECURSION 32767)

Где 32767 может быть скорректировано вниз, чтобы соответствовать вашему сценарию

person Macros    schedule 25.03.2009

Вы пытались построить путь в CTE и использовать его для идентификации предков?

Затем вы можете вычесть глубину узла-потомка из глубины узла-предка, чтобы вычислить столбец GenerationsRemoved, например...

DECLARE @Nodes TABLE
(
    NodeId varchar(50) PRIMARY KEY NOT NULL,
    ParentNodeId varchar(50) NULL
)

INSERT INTO @Nodes (NodeId, ParentNodeId) VALUES ('A', NULL)
INSERT INTO @Nodes (NodeId, ParentNodeId) VALUES ('B', 'A')
INSERT INTO @Nodes (NodeId, ParentNodeId) VALUES ('C', 'B')

DECLARE @Hierarchy TABLE
(
    NodeId varchar(50) PRIMARY KEY NOT NULL,
    ParentNodeId varchar(50) NULL,
    Depth int NOT NULL,
    [Path] varchar(2000) NOT NULL
)

WITH Hierarchy AS
(
    --initialization
    SELECT NodeId, ParentNodeId, 0 AS Depth, CONVERT(varchar(2000), NodeId) AS [Path]
    FROM @Nodes
    WHERE ParentNodeId IS NULL

    UNION ALL

    --recursive execution
    SELECT n.NodeId, n.ParentNodeId, p.Depth + 1, CONVERT(varchar(2000), p.[Path] + '/' + n.NodeId)
    FROM Hierarchy AS p
    INNER JOIN @Nodes AS n
    ON p.NodeId = n.ParentNodeId
)
INSERT INTO @Hierarchy
SELECT *
FROM Hierarchy

SELECT parent.NodeId AS AncestorNodeId, child.NodeId AS DescendantNodeId, child.Depth - parent.Depth AS GenerationsRemoved
FROM @Hierarchy AS parent
INNER JOIN @Hierarchy AS child
ON child.[Path] LIKE parent.[Path] + '/%'
person Richard Poole    schedule 25.03.2009

Это нарушает ограничение рекурсии, наложенное на ответ Криса Шаффера.

Я создаю таблицу с циклом:

CREATE TABLE ##Nodes
(
   NodeID varchar(50) PRIMARY KEY NOT NULL,
   ParentNodeID varchar(50) NULL
)

INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('A', 'C');
INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('B', 'A');
INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('C', 'B');

В случаях, когда существует потенциальный цикл (т.е. ParentNodeId НЕ NULL), удаление генерации начинается с 2. Затем мы можем идентифицировать циклы, проверяя (P.ParentNodeID == N.NodeID), который мы просто не добавляем. . После этого мы добавляем пропущенное поколение remove = 1.

WITH ParentNodes AS
(
   --initialization
   SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
   FROM ##Nodes
   WHERE ParentNodeID IS NULL

   UNION ALL

   SELECT P.ParentNodeID, N.NodeID, 2 AS GenerationsRemoved
   FROM ##Nodes N
   JOIN ##Nodes P ON N.ParentNodeID=P.NodeID
   WHERE P.ParentNodeID IS NOT NULL

   UNION ALL

   ----recursive execution
   SELECT P.ParentNodeID, N.NodeID, P.GenerationsRemoved + 1
   FROM ParentNodes AS P
     INNER JOIN ##Nodes AS N
     ON P.NodeID = N.ParentNodeID
   WHERE P.ParentNodeID IS NULL OR P.ParentNodeID <> N.NodeID

),
Nodes AS (
   SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved 
   FROM ##Nodes 
   WHERE ParentNodeID IS NOT NULL

   UNION ALL

   SELECT ParentNodeID, NodeID, GenerationsRemoved FROM ParentNodes
)
SELECT ParentNodeID, NodeID, GenerationsRemoved
FROM Nodes
ORDER BY ParentNodeID, NodeID, GenerationsRemoved
person Tom    schedule 28.08.2013

with cte as
(
    select a=65, L=1
    union all
    select a+1, L=L+1
    from cte
    where L<=100
)
select 
IsRecursion=Case When L>1 then 'Recursion' else 'Not Recursion' end,
AsciiValue=a,
AsciiCharacter=char(a)
from cte
  1. Создайте столбец, содержащий текущий уровень.
  2. Проверьте, если уровень> 1

В моем примере показан рекурсивный CTE, который останавливает рекурсию после 100 уровней (максимум). В качестве бонуса он отображает набор символов ASCII и соответствующее числовое значение.

person FistOfFury    schedule 04.12.2015