У меня есть сценарий, предназначенный для поиска исходного местоположения. Все начинается с такой таблицы:
Я хочу найти исходное местоположение для выбранного количества значений идентификатора.
Результат, который я ищу:
Вывод для 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, могу я спросить, как ты переформатировал таблицу так, чтобы она выглядела так, как сейчас?
Будут ли действительные предыдущие значения всегда иметь нисходящие идентификаторы? Или у вас может быть странная последовательность движений вверх и вниз, например 23 -> 21 -> 22 -> 24 -> 20 -> 26 -> 25
, где идентификатор желаемого ответа 20
?
Кроме того: если ID
является суррогатным ключом, например. столбец identity
, то, как правило, не следует приписывать ему какое-либо дополнительное значение. Если у вас есть требование, чтобы значения ID
только увеличивались с течением времени или имели какое-либо другое свойство, вам следует: (1) задокументировать это требование и (б) рассмотреть возможность использования более подходящего имени, а не инициалов внутреннего диаметра.
В основном вы были там... вам просто нужно было внести два изменения:
q.ID > u.Id
, чтобы оно остановилось, когда вы достигнете наименьшего идентификатора.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;
вау, это элегантно. Спасибо. В чем преимущество convert
в данном случае?
На самом деле ничего :) Я просто привык конвертировать в бит, чтобы в клиентском коде он поступал как bool, а не как int. Но в этом случае вы можете полностью отказаться от него.
Предлагаемые настройки: (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*
, когда цикл полностью находится ниже исходного идентификатора.
@TN, можешь ли ты оставить другое предложенное решение? Оно было там пару дней назад, но, думаю, сейчас вы его удалили. У меня возникла проблема с моими живыми данными, когда ошибка рекурсии все еще достигает максимума 100. Я хотел бы использовать предложенное вами решение для устранения неполадок, в которых что-то могло пойти не так.
@ T340B просто установите ограничение рекурсии на свой cte и удалите фильтр - вот как его отладить, потому что вы увидите каждое вычисление.
@T340B. - Вы также можете попробовать применить две предложенные мной модификации, которые я опубликовал вчера (28 июня в 16:17). Это исправляет несколько случаев, которые по-прежнему могут вызывать неограниченную рекурсию. При наличии этих изменений все сценарии должны достичь терминального состояния. Скрипка.
Как отмечено в комментариях ниже, это не дает желаемого результата «наименьшего числа» ОП. Я пока оставляю его опубликованным, ожидая дальнейшего обсуждения и возможного улучшения. Другой опубликованный ответ может быть лучшим простым ответом.
Я собираюсь предположить общий случай, когда порядок 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
также есть дополнительные запятые для поддержки этого теста.
Примерные результаты (с некоторыми дополнительными данными испытаний):
Если вы не хотите создавать список, вы можете отслеживать образец предыдущего идентификатора (первоначально начальный идентификатор) и сверять каждый шаг с этим отслеживаемым идентификатором. Проблема в том, что может быть достигнут цикл, не включающий исходный идентификатор. Чтобы справиться с этим случаем, вы можете сбрасывать отслеживаемый идентификатор через увеличивающиеся интервалы (степени 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, ...).
Примеры результатов:
Обратите внимание, что в нескольких случаях, описанных выше, обнаружение цикла происходило на несколько шагов позже, чем в предыдущем решении на основе списка.
См. эту db<>fiddle для демонстрации обоих вышеперечисленных методов.
Еще один возможный подход — заранее проверить всю таблицу IDs
, чтобы определить, какие идентификаторы доступны (в обратном направлении) из строк, имеющих PreviousID = null
. Как только этот список будет построен, любые идентификаторы, не входящие в этот список, будут признаны частью цикла. (Я не кодировал этот подход.)
Однако это не дает желаемых результатов ОП... они указали, что цикл должен заканчиваться на самом низком посещенном идентификаторе, а не когда он возвращается к самому себе.
@DaleK - Хорошая мысль. Я этого не уловил. Возможно, потребуется отслеживать MIN(ID)
во время процесса и снова присоединиться к этой строке при окончательном выборе. Это позволит справиться со странными случаями, такими как 23 -> 21 -> 22 -> 24 -> 20 -> 26 -> 25
, когда мы не знаем минимума, пока не дойдем до конца.
Хотя я восхищаюсь вашей приверженностью общему решению, я считаю, что для этого и многих других сценариев это излишне. Лучшие практики SQL больше ориентированы на решение конкретных проблем, чем на построение общих запросов, как это можно сделать в других языках программирования. И я вполне уверен, что уже видел общее решение этого вопроса раньше, так что в этом случае это будет дубль.
@TN, ваше решение интересно, даже если оно не дает желаемого результата. Ваше предложение имеет для меня ценность. Хотя мне все еще нужно время, чтобы переварить это. Спасибо.
Разве CHARINDEX(CONCAT(',', u.ID), VistiedIDs) AS LoopDetected
не должно быть CHARINDEX(CONCAT(',', u.ID, ','), VistiedIDs) AS LoopDetected
? В противном случае ',5'
может совпадать с ',42,55,9,'
. А что значит Vistied?
@ХАБО - Да. Спасибо. Это было то, что я намеревался сделать - теперь исправлено. Под «посещенным» я имею в виду значения идентификаторов (строки), которые были обнаружены до тех пор, пока CTE пересекает связанный список, определенный значениями PreviousID
.
Если вы посмотрите на предыдущий вопрос ОП по этой теме, вы увидите, что переходы 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/…)
@DaleK, 8 — это корень, потому что это наименьшее число в этой цепочке