SQL Server CTE для поиска пути от одного идентификатора к другому идентификатору

У меня есть таблица в базе данных SQL Server 2008 R2, которая определяет переходы между различными состояниями.

Пример соответствующих столбцов таблицы:

TransitionID | SourceStateID | DestinationStateID | WorkflowID
--------------------------------------------------------------
1            | 17            | 18                 | 3
2            | 17            | 21                 | 3
3            | 17            | 24                 | 3
4            | 17            | 172                | 3
5            | 18            | 17                 | 3
6            | 18            | 24                 | 3
7            | 18            | 31                 | 3
8            | 21            | 17                 | 3
9            | 21            | 20                 | 3
10           | 21            | 141                | 3
11           | 21            | 184                | 3

... так далее.

Моя цель состоит в том, чтобы иметь возможность определить начальный StateID, конечный StateID и WorkflowID и, надеюсь, найти логический путь от начального StateID к конечному StateID внутри этого рабочий процесс.

например Для приведенной выше таблицы, если бы я указал начальный StateID, равный 17, и конечный StateID, равный 184, и WorkflowID, равный 3, я мог бы получить «путь» 17>21>184 (или, что более идеально, TransitionID 2 > TransitionID 11)

Хорошее:

  • Определенные начальный и конечный StateID будут всегда иметь возможный путь в пределах определенного WorkflowID.

Плохое:

  • Безусловно, существуют циклические ссылки практически на каждом StateID источника/назначения (т. е., вероятно, существует переход от SourceStateID 1 к DestinationStateID 2, а также от SourceStateID 2 к DestinationStateID 1).

  • Конечно, существует несколько возможных путей практически из любого определенного начального StateID и конечного StateID.

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

Идеальным решением будет тот, который выбирает кратчайший путь от начального StateID до конечного StateID, но, если быть честным, на данный момент, если я смогу получить что-то работающее, что надежно даст мне любой действительный путь между два состояния я буду так счастлив.

Любые гуру SQL могут указать мне направление? Я, честно говоря, даже не знаю, с чего начать, чтобы предотвратить круговые проблемы, такие как получение пути по строкам 17> 18> 17> 18> 17> 18...

ОТВРАТИТЕЛЬНОЕ ОБНОВЛЕНИЕ Я придумал этот запрос, который причиняет мне боль на эмоциональном уровне (с некоторой помощью этого поста: https://stackoverflow.com/a/11042012/3253311), но, похоже, работает.

DECLARE @sourceStatusId INT = 17
DECLARE @destinationStatusId INT = 24
DECLARE @workflowId INT = 3

DECLARE @sourceStatusIdString NVARCHAR(MAX) = CAST(@sourceStatusId AS             NVARCHAR(MAX))
DECLARE @destinationStatusIdString NVARCHAR(MAX) = CAST(@destinationStatusId     AS NVARCHAR(MAX))
DECLARE @workflowIdString NVARCHAR(MAX) = CAST(@workflowId AS NVARCHAR(MAX))

;WITH CTE ([Source], [Destination], [Sentinel]) AS 
(
SELECT
    CAST(t.[Source] AS NVARCHAR(MAX)) AS [Source],
    CAST(t.[Destination] AS NVARCHAR(MAX)) AS [Destination],
    [Sentinel] = CAST([Source] AS NVARCHAR(MAX))
FROM
    Transitions t
WHERE
    CAST([Source] AS NVARCHAR(MAX)) = @sourceStatusIdString AND 
    CAST([WorkflowID] AS NVARCHAR(MAX)) = @workflowIdString

UNION ALL

SELECT
    CAST(CTE.[Destination] AS NVARCHAR(MAX)),
    CAST(t.[Destination] AS NVARCHAR(MAX)),
    CAST([Sentinel] AS NVARCHAR(MAX)) + CAST('|' AS NVARCHAR(MAX)) + CAST(CTE.[Destination] AS NVARCHAR(MAX))
FROM
    CTE
    JOIN Transitions t
        ON CAST(t.[Source] AS NVARCHAR(MAX)) = CAST(CTE.[Destination] AS     NVARCHAR(MAX))
WHERE
    CHARINDEX(CTE.[Destination], Sentinel)=0
)

SELECT TOP 1
[Sentinel]
FROM
CTE
WHERE
LEFT([Sentinel], LEN(@sourceStatusIdString)) = @sourceStatusIdString AND 
RIGHT([Sentinel], LEN(@destinationStatusIdString)) =     @destinationStatusIdString 
ORDER BY
LEN([Sentinel])

person McFixit    schedule 15.04.2016    source источник
comment
Нет никакого количества sql или даже логики, которые могут это решить. У вас есть одна строка с parentID 17 и дочерним элементом 18, в той же группе у вас есть parentID 18 и дочерний элемент 17. У вас даже нет здесь никаких данных, которые указывают, что такое база или отправная точка. Перед вами неразрешимая загадка. Подумайте об этом так. Курице 17, а яйцу 18. Какое из них было первым?   -  person Sean Lange    schedule 15.04.2016
comment
@SeanLange Хотя я согласен с тем, что это рассол, я с уважением не согласен с тем, что его нельзя решить. Я только что придумал решение, которое работает, однако его реализация причиняет мне боль на эмоциональном уровне. Сейчас я проверю ответ Quassnoi и, надеюсь, мне не понадобится использовать то, что я придумал :)   -  person McFixit    schedule 15.04.2016


Ответы (3)


Аналогично ответу Quassnoi, но предотвращает циклические ссылки, начинающиеся после первого элемента:

DECLARE @src int = 18, @dst int = 184;
WITH cte  AS 
(Select TransitionId, SourceStateId, DestinationStateID, SourceStateID as StartingState, 0 as Depth
  , cast(TransitionId as varchar(max)) + ','  as IDPath  
  , cast(SourceStateID as varchar(max)) + ',' as States
 FROM Transitions
 WHERE SourceStateId = @src
 UNION ALL
 Select t.TransitionId, t.SourceStateId, t.DestinationStateID, StartingState, cte.Depth + 1
  , cte.IDPath + cast(t.TransitionId as varchar(max)) + ',' 
  , cte.States + cast(t.SourceStateID as varchar(max)) + ','
 FROM cte JOIN Transitions t on cte.DestinationStateID = t.SourceStateId 
 and CHARINDEX(',' + cast(t.SourceStateID as varchar(max)) + ',', States) = 0 --prevent loop starting after first element
 and t.DestinationStateID <> StartingState --prevent loop starting with first element
 where cte.Depth < 10 -- prevent going too deep
 )
 select TransitionId, SourceStateId, DestinationStateID, Depth, left(IDPath, len(IDPath) - 1) IDPath, left(States, len(States) - 1) States
 from cte
 where DestinationStateID = @dst
 order by Depth
person Kateract    schedule 15.04.2016
comment
Ваш, кажется, быстрее всего исчерпал рабочие ответы. В итоге я пошел с чем-то немного другим, но это отвечает всем требованиям ... спасибо! - person McFixit; 18.04.2016

WITH    q (id, src, dst, sid, cnt, path) AS
        (
        SELECT  transitionId, sourceStateId, destinationStateId, sourceStateId, 1,
                CAST(transitionId AS VARCHAR(MAX)) path
        FROM    mytable
        WHERE   sourceStateId = 18
        UNION ALL
        SELECT  m.transitionId, m.sourceStateId, m.destinationStateId,
                CASE WHEN sid < sourceStateId THEN sid ELSE sourceStateId END, cnt + 1,
                path + ', ' + CAST(transitionId AS VARCHAR(MAX))
        FROM    q
        JOIN    mytable m
        ON      m.sourceStateId = q.dst
                AND m.sourceStateId <> q.sid
        )
SELECT  TOP 1
        *
FROM    q
WHERE   dst = 184
ORDER BY
        cnt DESC

См. скрипт: http://www.sqlfiddle.com/#!6/9342e/17

person Quassnoi    schedule 15.04.2016
comment
Ваш запрос завершается ошибкой с начальным значением 18 и конечным значением 184. - person Spock; 16.04.2016
comment
@Quassnoi Я получаю бесконечную рекурсию при попытке выполнить запрос в том виде, в котором он написан. - person McFixit; 16.04.2016

Вот мой взгляд на это... Основанный на Quassnoi, но короче, чем Kateract ;-)

http://www.sqlfiddle.com/#!6/322d3/4

WITH data AS (
    SELECT  TransitionId, SourceStateId, DestinationStateId,
        '|' + CAST(transitionId AS VARCHAR(MAX)) as Path
    FROM    States
    WHERE   sourceStateId = 17
    AND WorkflowID = 3
    UNION ALL
    SELECT  m.TransitionId, m.sourceStateId, m.DestinationStateId,
        Path + '|' + CAST(m.TransitionId AS VARCHAR(MAX))
    FROM data d
        JOIN States m ON
            m.sourceStateId = d.DestinationStateId
        AND CHARINDEX( '|' + convert(varchar(10),m.TransitionID) + '|', path) = 0
        AND WorkflowID = 3
)
SELECT TOP 1 *
FROM data
WHERE DestinationStateId = 184
ORDER BY LEN(Path)
-- If you want the longest path... ORDER BY LEN(Path) DESC
person Spock    schedule 15.04.2016