2012-06-14 7 views
11

Ho scritto un'espressione CTE molto semplice che recupera un elenco di tutti i gruppi di cui un utente è membro.TSQL CTE: come evitare l'attraversamento circolare?

Le regole sono le seguenti: un utente può trovarsi in più gruppi e i gruppi possono essere nidificati in modo che un gruppo possa essere membro di un altro gruppo e inoltre i gruppi possono essere membri reciproci di un altro, quindi il gruppo A è un membro della B Group e del Gruppo B è anche un membro del gruppo A.

mio CTE va come questa e, ovviamente, produce una ricorsione infinita:

  ;WITH GetMembershipInfo(entityId) AS(-- entity can be a user or group 
       SELECT k.ID as entityId FROM entities k WHERE k.id = @userId 
       UNION ALL 
       SELECT k.id FROM entities k 
       JOIN Xrelationships kc on kc.entityId = k.entityId 
       JOIN GetMembershipInfo m on m.entityId = kc.ChildID 
      ) 

non riesco a trovare una soluzione facile per back- rintracciare quei gruppi che ho già registrato.

Stavo pensando di utilizzare un parametro varchar aggiuntivo nel CTE per registrare un elenco di tutti i gruppi che ho visitato, ma l'uso di varchar è troppo semplice, non è vero?

C'è un modo migliore?

+0

Sei sicuro che ricorre per sempre? L'impostazione predefinita del server è 100 iterazioni. Prova a leggere l'hint 'MAXRECURSION' su [MSDN] (http://msdn.microsoft.com/en-us/library/ms175972.aspx). – Bridge

+0

Prima preoccuparsi dell'efficacia, * quindi * preoccuparsi della crudezza, se il tempo lo consente :) – AakashM

+0

non esegue recurse per sempre perché genera un errore dopo 100 chiamate ricorsive. Perdona la mia formulazione – Haoest

risposta

25

È necessario accumulare una stringa sentinella all'interno della ricorsione. Nel seguente esempio ho un rapporto circolare da A, B, C, D, e quindi ad A, e evito un ciclo con la stringa sentinella:

DECLARE @MyTable TABLE(Parent CHAR(1), Child CHAR(1)); 

INSERT @MyTable VALUES('A', 'B'); 
INSERT @MyTable VALUES('B', 'C'); 
INSERT @MyTable VALUES('C', 'D'); 
INSERT @MyTable VALUES('D', 'A'); 

; WITH CTE (Parent, Child, Sentinel) AS (
    SELECT Parent, Child, Sentinel = CAST(Parent AS VARCHAR(MAX)) 
    FROM @MyTable 
    WHERE Parent = 'A' 
    UNION ALL 
    SELECT CTE.Child, t.Child, Sentinel + '|' + CTE.Child 
    FROM CTE 
    JOIN @MyTable t ON t.Parent = CTE.Child 
    WHERE CHARINDEX(CTE.Child,Sentinel)=0 
) 
SELECT * FROM CTE; 

risultati:

Parent Child Sentinel 
------ ----- -------- 
A  B  A 
B  C  A|B 
C  D  A|B|C 
D  A  A|B|C|D 
+1

Mi piace la tua soluzione perché funziona. Ma c'è un modo per farlo senza una stringa sentinella? Sento che è rozzo e duplice che dobbiamo aggiungere qualche tipo di delimitatore attorno a ogni voce sentinella, diciamo Sentinel = '<' + CAST (Genitore AS VARCHAR (MAX)) + '>' Quindi dobbiamo fare il lo stesso nella funzione CharIndex(), perché senza i delimitatori ci possono essere falsi positivi. E cosa succede se la stringa sentinella diventa così grande da superare la lunghezza di varchar (max)? – Haoest

+2

Sono felice di sentire che funziona. È un po 'un trucco, e onestamente non riesco a pensare a un modo "più pulito". Tuttavia, tieni presente che la sentinella cresce indipendentemente su ogni ramo ricorsivo in modo indipendente, e quindi diventerà grande solo quanto i tempi di profondità massima di ogni stringa, oltre al delimitatore. VARCHAR (MAX) ha un limite di 2 GB, mentre la profondità massima può essere ampliata, se necessario, fino a un massimo di 32767. Pertanto, è altamente improbabile che tu possa mai traboccare VARCHAR (MAX). La maggior parte dei lavori di ricorsione potrebbe avere qualche migliaio di alberi, ma le cui profondità raramente superano 5 o più. Quindi, le stringhe sentinella rimarranno generalmente abbastanza piccole. –

+0

buono a sapersi, grazie. – Haoest

2

Invece di una stringa sentinella, usa una variabile tabella sentinella. La funzione acquisirà un riferimento circolare indipendentemente dal numero di hop del cerchio, nessun problema con la lunghezza massima di nvarchar (max), facilmente modificabile per diversi tipi di dati o persino tasti multipart, ed è possibile assegnare la funzione a un vincolo di controllo.

CREATE FUNCTION [dbo].[AccountsCircular] (@AccountID UNIQUEIDENTIFIER) 
RETURNS BIT 
AS 
BEGIN 
    DECLARE @NextAccountID UNIQUEIDENTIFIER = NULL; 
    DECLARE @Sentinel TABLE 
    (
     ID UNIQUEIDENTIFIER 
    ) 
    INSERT INTO  @Sentinel 
       ([ID]) 
    VALUES   (@AccountID) 
    SET @NextAccountID = @AccountID; 

    WHILE @NextAccountID IS NOT NULL 
    BEGIN 
     SELECT @NextAccountID = [ParentAccountID] 
     FROM [dbo].[Accounts] 
     WHERE [AccountID] = @NextAccountID; 
     IF EXISTS(SELECT 1 FROM @Sentinel WHERE ID = @NextAccountID) 
      RETURN 1; 
     INSERT INTO @Sentinel 
       ([ID]) 
     VALUES  (@NextAccountID) 
    END 
    RETURN 0; 
END