2012-06-14 7 views
11

Ich habe einen sehr einfachen CTE-Ausdruck geschrieben, der eine Liste aller Gruppen abruft, zu denen ein Benutzer gehört.TSQL CTE: Wie vermeidet man Kreisdurchlauf?

Die Regeln gehen so, ein Benutzer kann in mehreren Gruppen sein, und Gruppen können geschachtelt werden, so dass eine Gruppe ein Mitglied einer anderen Gruppe sein kann, und darüber hinaus Gruppen können gegenseitige Mitglied eines anderen sein, so Gruppe A ist ein Mitglied der Gruppe B und Gruppe B ist auch ein Mitglied der Gruppe A.

Mein CTE so geht und offensichtlich ergibt es eine unendliche Rekursion:

  ;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 
      ) 

ich kann nicht eine einfache Lösung finden Hinter- Verfolgen Sie die Gruppen, die ich bereits aufgenommen habe.

Ich dachte daran, einen zusätzlichen varchar-Parameter im CTE zu verwenden, um eine Liste aller Gruppen aufzuspeichern, die ich besucht habe, aber die Verwendung von varchar ist einfach zu grob, oder?

Gibt es einen besseren Weg?

+0

Sind Sie sicher, dass es für immer rekursiv ist? Der Serverstandard ist 100 Iterationen. Versuchen Sie, den 'MAXRECURSION'-Hinweis auf [MSDN] (http://msdn.microsoft.com/en-us/library/ms175972.aspx) nachzulesen. – Bridge

+0

Erste Sorge um die Wirksamkeit, * dann * Sorge um Grobheit, wenn die Zeit es erlaubt :) – AakashM

+0

es nicht für immer rekursiv, weil es einen Fehler nach 100 rekursiven Aufrufe wirft. Verzeihen Sie meine Formulierung. – Haoest

Antwort

25

Sie müssen eine Sentinel-Zeichenfolge innerhalb Ihrer Rekursion akkumulieren. Im folgende Beispiel habe ich eine kreisförmige Beziehung von A, B, C, D, und dann zu einem zurück, und ich vermeiden, dass eine Schleife mit der Sentinel string:

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; 

Ergebnis:

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

Ich mag Ihre Lösung, weil es funktioniert. Aber gibt es eine Möglichkeit, dies ohne eine Wächter-Schnur zu tun? Ich finde es klobig und doppelt, dass wir um jeden Sentinel-Eintrag eine Art Trennzeichen hinzufügen müssen, sagen wir Sentinel = '<' + CAST (Eltern AS VARCHAR (MAX)) + '>' Dann müssen wir tun Gleiches in der CharIndex() - Funktion, da ohne die Begrenzer falsche positive Werte auftreten können. Und was passiert, wenn die Sentinel-Zeichenfolge so groß wird, dass sie die Länge von Varchar (max) überschreitet? – Haoest

+2

Ich bin froh zu hören, dass das funktioniert. Es ist ein bisschen wie ein Hack, und ich kann mir ehrlich gesagt keinen "saubereren" Weg vorstellen. Beachten Sie jedoch, dass der Sentinel unabhängig von jedem rekursiven Zweig wächst und daher nur so groß wie die maximale Tiefe jeder Zeichenfolge plus Begrenzer wird. VARCHAR (MAX) hat eine Grenze von 2 GB, während die maximale Tiefe bei Bedarf auf maximal 32767 erhöht werden kann. Es ist daher sehr unwahrscheinlich, dass Sie VARCHAR (MAX) jemals überlaufen lassen. Die meisten Rekursionsjobs können ein paar tausend Bäume haben, deren Tiefen aber selten 5 oder so übersteigen. Ihre Sentinel-Strings bleiben also im Allgemeinen ziemlich klein. –

+0

gut zu wissen, danke. – Haoest

2

Verwenden Sie anstelle einer Sentinel-Zeichenfolge eine Sentinel-Tabellenvariable. Die Funktion fängt einen Zirkelverweis auf, egal wie viele Hops der Kreis ist, keine Probleme mit der maximalen Länge von nvarchar (max), leicht modifiziert für verschiedene Datentypen oder sogar mehrteilige Schlüssel, und Sie können die Funktion einer Prüfbedingung zuweisen.

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 
Verwandte Themen