Verwenden von EXCEPT in einem rekursiven allgemeinen Tabellenausdruck

33

Warum gibt die folgende Abfrage unendlich viele Zeilen zurück? Ich hätte erwartet, dass die EXCEPTKlausel die Rekursion beendet.

with cte as (
    select *
    from (
        values(1),(2),(3),(4),(5)
    ) v (a)
)
,r as (
    select a
    from cte
    where a in (1,2,3)
    union all
    select a
    from (
        select a
        from cte
        except
        select a
        from r
    ) x
)
select a
from r

Ich bin darauf gestoßen, als ich versucht habe, eine Frage zu Stack Overflow zu beantworten .

Tom Hunter
quelle

Antworten:

26

In der Antwort von Martin Smith finden Sie Informationen zum aktuellen Status EXCEPTeines rekursiven CTE.

Um zu erklären, was Sie gesehen haben und warum:

Ich verwende hier eine Tabellenvariable, um die Unterscheidung zwischen Ankerwerten und rekursivem Element klarer zu machen (dies ändert die Semantik nicht).

DECLARE @V TABLE (a INTEGER NOT NULL)
INSERT  @V (a) VALUES (1),(2)
;
WITH rCTE AS 
(
    -- Anchor
    SELECT
        v.a
    FROM @V AS v

    UNION ALL

    -- Recursive
    SELECT
        x.a
    FROM
    (
        SELECT
            v2.a
        FROM @V AS v2

        EXCEPT

        SELECT
            r.a
        FROM rCTE AS r
    ) AS x
)
SELECT
    r2.a
FROM rCTE AS r2
OPTION (MAXRECURSION 0)

Der Abfrageplan lautet:

Rekursiver CTE-Plan

Die Ausführung beginnt im Stammverzeichnis des Plans (SELECT), und die Steuerung übergibt den Baum an die Index-Spool, die Verkettung und dann an den Tabellenscan der obersten Ebene.

Die erste Zeile des Scans geht über den Baum und wird (a) in der Stapelspule gespeichert und (b) an den Client zurückgegeben. Welche Zeile zuerst definiert wird, ist nicht festgelegt. Nehmen wir jedoch an, dass es sich um die Zeile mit dem Wert {1} handelt. Die erste Zeile, die angezeigt wird, ist daher {1}.

Die Steuerung wird wieder an den Tabellenscan übergeben (der Verkettungsoperator verbraucht alle Zeilen von seiner äußersten Eingabe, bevor er die nächste öffnet). Der Scan gibt die zweite Zeile aus (Wert {2}). Dadurch wird der Baum erneut übergeben, der auf dem Stapel gespeichert und an den Client ausgegeben werden soll. Der Client hat jetzt die Sequenz {1}, {2} erhalten.

Wenn Sie eine Konvention übernehmen, bei der sich der obere Rand des LIFO-Stapels links befindet, enthält der Stapel jetzt {2, 1}. Wenn die Steuerung wieder an den Tabellenscan übergeben wird, werden keine Zeilen mehr gemeldet und die Steuerung wird an den Verkettungsoperator zurückgegeben, der die zweite Eingabe öffnet (für die Weitergabe an die Stapelspule ist eine Zeile erforderlich), und die Steuerung wird an den inneren Join übergeben zum ersten Mal.

Der Inner-Join ruft die Tabellenspule an ihrer äußeren Eingabe auf, die die oberste Zeile aus dem Stapel {2} liest und sie aus der Arbeitstabelle löscht. Der Stapel enthält jetzt {1}.

Nachdem der Inner Join eine Zeile an seiner äußeren Eingabe empfangen hat, übergibt er die Kontrolle über seine innere Eingabe an den Left Anti-Semi Join (LASJ). Dies fordert eine Zeile von ihrer äußeren Eingabe an und übergibt die Steuerung an die Sortierung. Sort ist ein blockierender Iterator, der alle Zeilen aus der Tabellenvariablen liest und aufsteigend sortiert (wie es geschieht).

Die erste von der Sortierung ausgegebene Zeile ist daher der Wert {1}. Die Innenseite des LASJ gibt den aktuellen Wert des rekursiven Elements (den Wert, der gerade vom Stapel abgefallen ist) zurück, nämlich {2}. Die Werte am LASJ sind {1} und {2}, daher wird {1} ausgegeben, da die Werte nicht übereinstimmen.

Diese Zeile {1} leitet den Abfrageplanbaum an den Index (Stack) Spool weiter, wo er dem Stack hinzugefügt wird, der jetzt {1, 1} enthält und an den Client gesendet wird. Der Client hat jetzt die Sequenz {1}, {2}, {1} erhalten.

Die Steuerung geht nun zurück zur Verkettung, zurück zur Innenseite (beim letzten Mal wurde eine Zeile zurückgegeben, möglicherweise erneut), über den Inner Join zum LASJ. Es liest seine innere Eingabe erneut und erhält den Wert {2} aus der Sortierung.

Das rekursive Mitglied ist immer noch {2}. Diesmal findet der LASJ {2} und {2}, sodass keine Zeile ausgegeben wird. Wenn in der inneren Eingabe keine Zeilen mehr gefunden werden (die Sortierung ist jetzt nicht mehr zeilenfähig), wird die Steuerung an die innere Verknüpfung zurückgegeben.

Die innere Verknüpfung liest ihre äußere Eingabe, was dazu führt, dass der Wert {1} vom Stapel {1, 1} entfernt wird und der Stapel nur noch {1} enthält. Der Vorgang wird nun wiederholt, wobei der Wert {2} nach einem erneuten Aufruf von Table Scan and Sort den LASJ-Test besteht und dem Stapel hinzugefügt wird, und an den Client weitergeleitet wird, der nun {1}, {2} empfangen hat. {1}, {2} ... und los geht's.

Meine Lieblings Erklärung der Spule Stapel in rekursiven CTE Pläne verwendet wird , ist Craig Freedmans.

Paul White sagt GoFundMonica
quelle
31

Die BOL-Beschreibung von rekursiven CTEs beschreibt die Semantik der rekursiven Ausführung wie folgt:

  1. Teilen Sie den CTE-Ausdruck in Anker- und rekursive Member auf.
  2. Führen Sie die Ankerelemente aus, die die erste Aufruf- oder Basisergebnismenge (T0) erstellen.
  3. Führen Sie die rekursiven Elemente mit Ti als Eingabe und Ti + 1 als Ausgabe aus.
  4. Wiederholen Sie Schritt 3, bis ein leerer Satz zurückgegeben wird.
  5. Gibt die Ergebnismenge zurück. Dies ist eine UNION ALL von T0 bis Tn.

Beachten Sie, dass das oben Gesagte eine logische Beschreibung ist. Die physikalische Reihenfolge der Operationen kann, wie hier dargestellt, etwas anders sein

Wenn Sie dies auf Ihren CTE anwenden, würde ich eine Endlosschleife mit dem folgenden Muster erwarten

+-----------+---------+---+---+---+
| Invocation| Results             |
+-----------+---------+---+---+---+
|         1 |       1 | 2 | 3 |   |
|         2 |       4 | 5 |   |   |
|         3 |       1 | 2 | 3 |   |
|         4 |       4 | 5 |   |   |
|         5 |       1 | 2 | 3 |   |
+-----------+---------+---+---+---+ 

weil

select a
from cte
where a in (1,2,3)

ist der Ankerausdruck. Dies zeigt deutlich zurück 1,2,3alsT0

Danach wird der rekursive Ausdruck ausgeführt

select a
from cte
except
select a
from r

Mit 1,2,3as input, das einen Output von 4,5as T1then ergibt, kehrt das Wiedereinstecken für die nächste Runde der Rekursion 1,2,3auf unbestimmte Zeit zurück und so weiter.

Dies ist jedoch nicht der Fall. Dies sind die Ergebnisse der ersten 5 Aufrufe

+-----------+---------+---+---+---+
| Invocation| Results             |
+-----------+---------+---+---+---+
|         1 |       1 | 2 | 3 |   |
|         2 |       1 | 2 | 4 | 5 |
|         3 |       1 | 2 | 3 | 4 |
|         4 |       1 | 2 | 3 | 5 |
|         5 |       1 | 2 | 3 | 4 |
+-----------+---------+---+---+---+

Aus der Verwendung OPTION (MAXRECURSION 1)und Einstellung in Schritten nach oben 1ist ersichtlich, dass sie in einen Zyklus eintritt, in dem jede aufeinanderfolgende Ebene kontinuierlich zwischen Ausgabe 1,2,3,4und umschaltet 1,2,3,5.

Wie von @Quassnoi in diesem Blog-Beitrag besprochen . Das Muster der beobachteten Ergebnisse ist so, als würde jeder Aufruf dort ausgeführt, (1),(2),(3),(4),(5) EXCEPT (X)wo Xsich die letzte Zeile des vorherigen Aufrufs befindet.

Bearbeiten: Nach dem Lesen von SQL Kiwis exzellenter Antwort ist sowohl klar, warum dies auftritt, als auch, dass dies nicht die ganze Geschichte ist.

Anker wird an den Inhalt 1,2,3des Client-Stacks gesendet3,2,1

3 herausgesprungener Stapel, Stapelinhalt 2,1

Der LASJ kehrt zurück 1,2,4,5, Stack Contents5,4,2,1,2,1

5 herausgesprungener Stapel, Stapelinhalt 4,2,1,2,1

Die LASJ gibt 1,2,3,4 Stapel Inhalt4,3,2,1,5,4,2,1,2,1

4 herausgesprungener Stapel, Stapelinhalt 3,2,1,5,4,2,1,2,1

Die LASJ gibt 1,2,3,5 Stapel Inhalt5,3,2,1,3,2,1,5,4,2,1,2,1

5 herausgesprungener Stapel, Stapelinhalt 3,2,1,3,2,1,5,4,2,1,2,1

Die LASJ gibt 1,2,3,4 Stapel Inhalt 4,3,2,1,3,2,1,3,2,1,5,4,2,1,2,1

Wenn Sie versuchen, das rekursive Element durch den logisch äquivalenten Ausdruck (in Abwesenheit von Duplikaten / NULL-Werten) zu ersetzen

select a
from (
    select a
    from cte
    where a not in 
    (select a
    from r)
) x

Dies ist nicht zulässig und löst den Fehler "Rekursive Referenzen sind in Unterabfragen nicht zulässig" aus. Vielleicht ist es ein Versehen, EXCEPTdas in diesem Fall sogar erlaubt ist.

Zusatz: Microsoft hat jetzt auf mein Connect Feedback wie unten

Jacks Vermutung ist richtig: Dies hätte ein Syntaxfehler sein müssen. rekursive Verweise sollten in der Tat nicht in EXCEPTKlauseln erlaubt sein . Wir planen, diesen Fehler in einer kommenden Service-Version zu beheben. In der Zwischenzeit würde ich vorschlagen, rekursive Verweise in EXCEPT Klauseln zu vermeiden .

Bei der Einschränkung der Rekursion EXCEPTfolgen wir dem ANSI-SQL-Standard, der diese Einschränkung seit Einführung der Rekursion (glaube ich 1999) enthält. EXCEPTIn deklarativen Sprachen wie SQL gibt es keine weit verbreitete Übereinstimmung darüber, wie die Semantik für die Rekursion sein soll (auch als "nicht geschichtete Negation" bezeichnet). Darüber hinaus ist es notorisch schwierig (wenn nicht unmöglich), eine solche Semantik effizient (für Datenbanken mit angemessener Größe) in einem RDBMS-System zu implementieren.

Und es sieht so aus, als ob die spätere Implementierung im Jahr 2014 für Datenbanken mit einem Kompatibilitätsgrad von 120 oder höher durchgeführt wurde .

Rekursive Verweise in einer EXCEPT-Klausel erzeugen einen Fehler in Übereinstimmung mit dem ANSI-SQL-Standard.

Martin Smith
quelle