Рекурсивный CTE и как преодолеть ошибку циклической ссылки

У меня есть сценарий, предназначенный для поиска исходного местоположения. Все начинается с такой таблицы:

ИДЕНТИФИКАТОР ПредыдущийID Расположение 2 НУЛЕВОЙ 1235 3 2 1236 4 3 1239 8 11 1237 9 8 1234 10 9 1235 11 10 1237

Я хочу найти исходное местоположение для выбранного количества значений идентификатора.

ИДЕНТИФИКАТОР 2 4 8 10 11

Результат, который я ищу:

уровень ИДЕНТИФИКАТОР Оригинальный идентификатор Расположение Исходное местоположение 0 2 2 1235 1235 2 4 2 1239 1235 0 8 8 1237 1237 2 10 8 1235 1237 3 11 8 1237 1237

Вывод для ID =2 и =4 хороший. Однако при использовании 8,10,11 это приводит к циклической ссылке и нарушению кода.

Вопрос: Можно ли как-то решить эту проблему, чтобы ошибка не возникала:

Заявление прекращено. Максимальная рекурсия 100 была исчерпана до завершения оператора.

и добиться желаемых результатов? Я знаю, что 8 — это корень, поскольку это наименьшее число в этой цепочке, хотя в этой цепочке есть циклическая ссылка.

Вот сценарий, который у меня есть на данный момент для вывода данных для идентификаторов 2 и 4. Он работает, если вы удалите значения 8,10,11.

DECLARE @IDs TABLE (
  ID INTEGER
  ,PreviousID INTEGER
  ,Location INTEGER
)

INSERT INTO @IDs
SELECT           2,null,1235
UNION ALL SELECT 3,2,1236
UNION ALL SELECT 4,3,1239
UNION ALL SELECT 8,11,1237
UNION ALL SELECT 9,8,1234
UNION ALL SELECT 10,9,1235
UNION ALL SELECT 11,10,1237

Select * from @IDs


DECLARE @ORDERID Table (OrderID nvarchar (100))
Insert into @ORDERID values
('2')
,('4')
--,('8')
--,('10')
--,('11')

;WITH q AS (
    SELECT 0 lvl,  ID, PreviousID,PreviousID LastId
        ,Location,Location as OriginalLocation
    FROM    @IDs
    where ID in (select OrderID from @ORDERID) 
    UNION ALL 
    SELECT lvl+1, q.ID,u.PreviousId,q.PreviousId LastId
       ,q.Location,u.Location
    FROM    q
            INNER JOIN @IDs u ON u.ID = q.PreviousID
            --and q.ID <> u.PreviousID and q.PreviousID <> u.ID
)
select lvl, ID, coalesce(LastId,Id) OriginalId,Location,OriginalLocation 
from q
where PreviousId is null
order by id;

Большое спасибо за любую подсказку или предложение.

@DaleK, 8 — это корень, потому что это наименьшее число в этой цепочке

T340B 28.06.2024 02:35

Это не так. Я добавлю. Спасибо.

T340B 28.06.2024 02:36

@DaleK, могу я спросить, как ты переформатировал таблицу так, чтобы она выглядела так, как сейчас?

T340B 28.06.2024 02:41

Будут ли действительные предыдущие значения всегда иметь нисходящие идентификаторы? Или у вас может быть странная последовательность движений вверх и вниз, например 23 -> 21 -> 22 -> 24 -> 20 -> 26 -> 25, где идентификатор желаемого ответа 20?

T N 28.06.2024 05:05

Кроме того: если ID является суррогатным ключом, например. столбец identity, то, как правило, не следует приписывать ему какое-либо дополнительное значение. Если у вас есть требование, чтобы значения ID только увеличивались с течением времени или имели какое-либо другое свойство, вам следует: (1) задокументировать это требование и (б) рассмотреть возможность использования более подходящего имени, а не инициалов внутреннего диаметра.

HABO 28.06.2024 20:13
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
116
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

В основном вы были там... вам просто нужно было внести два изменения:

  1. Добавьте дополнительное условие соединения q.ID > u.Id, чтобы оно остановилось, когда вы достигнете наименьшего идентификатора.
  2. Подсчитайте, какие результаты показать, чтобы включить в них те, по которым произошел отказ. Я нашел более понятным добавить конкретный расчет для определения корня, чем усложнять логику предложения where.
WITH q AS (
    SELECT 0 lvl
        , ID
        , PreviousID
        , PreviousID LastId
        , Location
        , Location AS OriginalLocation
        , CONVERT(bit, CASE WHEN PreviousID IS NULL OR PreviousId > Id THEN 1 ELSE 0 END) IsRoot
    FROM @IDs
    WHERE ID in (select OrderID from @ORDERID) 
  
    UNION ALL 
  
    SELECT lvl + 1
        , q.ID
        , u.PreviousId
        , q.PreviousId LastId
        , q.Location
        , u.Location
        , CONVERT(bit, CASE WHEN u.PreviousID IS NULL OR u.PreviousId > u.Id THEN 1 ELSE 0 END) IsRoot
    FROM q
    INNER JOIN @IDs u ON u.ID = q.PreviousID
        -- Ensure we stop at the lowest Id
        AND q.ID > u.Id
)
SELECT lvl, ID, coalesce(LastId,Id) OriginalId, Location, OriginalLocation
FROM q
WHERE IsRoot = 1
ORDER BY id;

DBFiddle

вау, это элегантно. Спасибо. В чем преимущество convert в данном случае?

T340B 28.06.2024 07:31

На самом деле ничего :) Я просто привык конвертировать в бит, чтобы в клиентском коде он поступал как bool, а не как int. Но в этом случае вы можете полностью отказаться от него.

Dale K 28.06.2024 07:42

Предлагаемые настройки: (1) Замените OR PreviousId > Id на OR PreviousId => Id в вычислении IsRoot (в обоих местах), чтобы обрабатывать случай самоцикла, такой как 12 -> 12*. (2) Если применяется № 1, завершающее условие CTE AND q.ID > u.Id можно заменить на WHERE q.IsRoot = 0. Это также будет обрабатывать случай, подобный 23 -> 22 -> 21 -> 22*, когда цикл полностью находится ниже исходного идентификатора.

T N 28.06.2024 18:17

@TN, можешь ли ты оставить другое предложенное решение? Оно было там пару дней назад, но, думаю, сейчас вы его удалили. У меня возникла проблема с моими живыми данными, когда ошибка рекурсии все еще достигает максимума 100. Я хотел бы использовать предложенное вами решение для устранения неполадок, в которых что-то могло пойти не так.

T340B 29.06.2024 23:38

@ T340B просто установите ограничение рекурсии на свой cte и удалите фильтр - вот как его отладить, потому что вы увидите каждое вычисление.

Dale K 30.06.2024 00:31

@T340B. - Вы также можете попробовать применить две предложенные мной модификации, которые я опубликовал вчера (28 июня в 16:17). Это исправляет несколько случаев, которые по-прежнему могут вызывать неограниченную рекурсию. При наличии этих изменений все сценарии должны достичь терминального состояния. Скрипка.

T N 30.06.2024 02:40

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

Я собираюсь предположить общий случай, когда порядок ID не имеет значения, а последовательность значений PreviousID может произвольно прыгать вверх и вниз. Я также допускаю случаи, когда последовательность может развиваться линейно, прежде чем перейти в цикл, который может включать или не включать исходное значение идентификатора.

Один из способов обнаружения петель — создать список значений посещенных идентификаторов и сверить каждый новый идентификатор со списком посещенных идентификаторов. Поскольку в SQL Server нет массивов или подобных многозначных типов, мы можем вернуться к списку посещенных идентификаторов, разделенному запятыми, который хранится в виде строки. Этот список будет инициализирован исходным значением идентификатора, а дополнительные идентификаторы будут добавлены в рекурсивную часть CTE. Тест CHARINDEX() с соответствующим разделением может проверить, достигли ли мы ранее посещенного идентификатора.

Что-то вроде:

WITH q AS (
    SELECT
        0 lvl, ID, PreviousID, PreviousID LastId
        ,Location, Location as OriginalLocation
        ,CAST(CONCAT(',', ID, ',') AS VARCHAR(MAX)) as VisitedIDs
        ,CAST(0 AS BIGINT) AS LoopDetected
    FROM    IDs
    where ID in (select OrderID from ORDERID) 
    UNION ALL 
    SELECT lvl+1, q.ID,u.PreviousId,q.PreviousId LastId
       ,q.Location,u.Location
       ,CONCAT(VisitedIDs, u.ID, ',')
       ,CHARINDEX(CONCAT(',', u.ID, ','), VisitedIDs) AS LoopDetected
    FROM    q
    INNER JOIN IDs u ON u.ID = q.PreviousID
    WHERE q.LoopDetected = 0
)
select lvl, ID, coalesce(LastId,Id) OriginalId,Location,OriginalLocation
    , q.LoopDetected, q.VisitedIDs
    , CASE WHEN q.LoopDetected > 0 THEN 'Loop detected' ELSE '' END AS ErrorCheck
from q
where (PreviousId is null OR q.LoopDetected > 0)
order by id, lvl;

Обратите внимание, что тест CHARINDEX() заключает значение идентификатора в запятую, чтобы гарантировать, например, что ID = 5 соответствует только ...,5,..., а не ...,15,... или ...,51,.... В VisitedIDs также есть дополнительные запятые для поддержки этого теста.

Примерные результаты (с некоторыми дополнительными данными испытаний):

уровень ИДЕНТИФИКАТОР Оригинальный идентификатор Расположение Исходное местоположение Обнаружена петля Посещенные идентификаторы Проверка ошибок 0 2 2 1235 1235 0 ,2, 2 4 2 1239 1235 0 ,4,3,2, 4 8 8 1237 1237 1 ,8,11,10,9,8, Обнаружена петля 4 10 10 1235 1235 1 ,10,9,8,11,10, Обнаружена петля 4 11 11 1237 1237 1 ,11,10,9,8,11, Обнаружена петля 8 12 10 9999 1235 11 ,12,5,13,6,10,9,8,11,10, Обнаружена петля 3 23 22 9999 9999 0 ,23,21,24,22,

Если вы не хотите создавать список, вы можете отслеживать образец предыдущего идентификатора (первоначально начальный идентификатор) и сверять каждый шаг с этим отслеживаемым идентификатором. Проблема в том, что может быть достигнут цикл, не включающий исходный идентификатор. Чтобы справиться с этим случаем, вы можете сбрасывать отслеживаемый идентификатор через увеличивающиеся интервалы (степени 2), чтобы при входе в петлю отслеживаемый идентификатор в конечном итоге сбрасывался, и петля в конечном итоге была обнаружена.

WITH q AS (
    SELECT
        0 lvl, ID, PreviousID, PreviousID LastId
        ,Location, Location as OriginalLocation
        ,ID as LoopCheckID
    FROM    IDs
    where ID in (select OrderID from ORDERID) 
    UNION ALL 
    SELECT lvl+1, q.ID,u.PreviousId,q.PreviousId LastId
       ,q.Location,u.Location
       ,CASE WHEN lvl & (lvl - 1) = 0 THEN u.ID ELSE q.LoopCheckID END
    FROM    q
    INNER JOIN IDs u ON u.ID = q.PreviousID
    WHERE q.PreviousID <> q.LoopCheckID
)
select lvl, ID, coalesce(LastId,Id) OriginalId,Location,OriginalLocation
    , q.PreviousID, q.LoopCheckID
    , CASE WHEN q.PreviousID = q.LoopCheckID THEN 'Loop detected' ELSE '' END AS ErrorCheck
from q
where (PreviousId is null OR q.PreviousID = q.LoopCheckID)
order by id, lvl;

lvl & (lvl - 1) = 0 — это сложная проверка манипуляции с битами, которая верна, когда lvl является степенью 2 (1, 2, 4, 8, 16, ...).

Примеры результатов:

уровень ИДЕНТИФИКАТОР Оригинальный идентификатор Расположение Исходное местоположение ПредыдущийID LoopCheckID Проверка ошибок 0 2 2 1235 1235 нулевой 2 2 4 2 1239 1235 нулевой 2 8 8 8 1237 1237 11 11 Обнаружена петля 8 10 10 1235 1235 9 9 Обнаружена петля 8 11 11 1237 1237 10 10 Обнаружена петля 8 12 10 9999 1235 9 9 Обнаружена петля 3 23 22 9999 9999 нулевой 22

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

См. эту db<>fiddle для демонстрации обоих вышеперечисленных методов.

Еще один возможный подход — заранее проверить всю таблицу IDs, чтобы определить, какие идентификаторы доступны (в обратном направлении) из строк, имеющих PreviousID = null. Как только этот список будет построен, любые идентификаторы, не входящие в этот список, будут признаны частью цикла. (Я не кодировал этот подход.)

Однако это не дает желаемых результатов ОП... они указали, что цикл должен заканчиваться на самом низком посещенном идентификаторе, а не когда он возвращается к самому себе.

Dale K 28.06.2024 04:54

@DaleK - Хорошая мысль. Я этого не уловил. Возможно, потребуется отслеживать MIN(ID) во время процесса и снова присоединиться к этой строке при окончательном выборе. Это позволит справиться со странными случаями, такими как 23 -> 21 -> 22 -> 24 -> 20 -> 26 -> 25, когда мы не знаем минимума, пока не дойдем до конца.

T N 28.06.2024 05:02

Хотя я восхищаюсь вашей приверженностью общему решению, я считаю, что для этого и многих других сценариев это излишне. Лучшие практики SQL больше ориентированы на решение конкретных проблем, чем на построение общих запросов, как это можно сделать в других языках программирования. И я вполне уверен, что уже видел общее решение этого вопроса раньше, так что в этом случае это будет дубль.

Dale K 28.06.2024 05:08

@TN, ваше решение интересно, даже если оно не дает желаемого результата. Ваше предложение имеет для меня ценность. Хотя мне все еще нужно время, чтобы переварить это. Спасибо.

T340B 28.06.2024 06:48

Разве CHARINDEX(CONCAT(',', u.ID), VistiedIDs) AS LoopDetected не должно быть CHARINDEX(CONCAT(',', u.ID, ','), VistiedIDs) AS LoopDetected? В противном случае ',5' может совпадать с ',42,55,9,'. А что значит Vistied?

HABO 28.06.2024 07:26

@ХАБО - Да. Спасибо. Это было то, что я намеревался сделать - теперь исправлено. Под «посещенным» я имею в виду значения идентификаторов (строки), которые были обнаружены до тех пор, пока CTE пересекает связанный список, определенный значениями PreviousID.

T N 28.06.2024 09:36

Если вы посмотрите на предыдущий вопрос ОП по этой теме, вы увидите, что переходы previousId->Id имеют дату перехода. Учитывая эту дату, циклы могут быть нарушены. Например, из цепочки (Id,Предыдущий идентификатор,Дата заказа) (1,null,'2024-06-01')(2,1,'2024-06-01'),(3,2,'2024- 06-02'),(‌​4,3,'2024-06-03'),(2‌​,4,'2024-06-04') ,(5,2,'2024-06-05') определенно следует по пути 1-2-3-4-2-5. Было бы полезно рассмотреть этот вопрос с учетом даты. (stackoverflow.com/questions/78648963/…)

ValNik 28.06.2024 16:17

Другие вопросы по теме