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

Более простой пример

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

Представьте себе Узлы таблицы с иерархией:

A
 - B
    - C

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

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 репутации за нерешенность моей проблемы.

Уголок Nitpickers

† не 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

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

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

Ответы [ 8 ]

19 голосов
/ 19 марта 2009

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

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

По сути, удаление запроса "только покажи мне абсолютных родителей" из запроса инициализации; Таким образом, он генерирует результаты, начиная с каждого из них и начиная с него. Я также добавил в «WHERE P.GenerationsRemoved <= 10» бесконечный рекурсивный улов (замените 10 любым числом до 100, чтобы соответствовать вашим потребностям). Затем добавьте сортировку, чтобы она выглядела так, как вы хотели. </p>

0 голосов
/ 05 декабря 2015
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 и соответствующее числовое значение.

0 голосов
/ 29 августа 2013

Это нарушает предел рекурсии, наложенный на ответ Криса Шаффера.

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

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 IS NOT 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
0 голосов
/ 25 марта 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] + '/%'
0 голосов
/ 25 марта 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 может быть скорректировано вниз, чтобы соответствовать вашему сценарию

0 голосов
/ 13 марта 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 будет когда-либо показывать только один уровень и его прямые подчиненные (и, конечно, по иерархии) - обычно они не пропускают один, два или даже больше уровней.

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

Марк

0 голосов
/ 12 марта 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

Это вернет разницу генерации между всеми узлами, вы можете изменить ее в соответствии с вашими потребностями.

0 голосов
/ 11 марта 2009

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

...