it-swarm.com.de

Verwenden von EXCEPT in einem rekursiven allgemeinen Tabellenausdruck

Warum gibt die folgende Abfrage unendlich viele Zeilen zurück? Ich hätte erwartet, dass die EXCEPT -Klausel 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 auf Stack Overflow zu beantworten.

33
Tom Hunter

Siehe Martin Smiths Antwort für Informationen zum aktuellen Status von EXCEPT in einem rekursiven CTE.

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

Ich verwende hier eine Tabellenvariable, um die Unterscheidung zwischen den Ankerwerten und dem rekursiven Element klarer zu machen (die Semantik wird dadurch nicht geändert).

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:

Recursive CTE Plan

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

Die erste Zeile des Scans passiert den Baum und wird (a) in der Stapelspule gespeichert und (b) an den Client zurückgegeben. Welche Zeile zuerst kommt, ist nicht definiert, aber nehmen wir an, dass es sich aus Gründen der Argumentation um die Zeile mit dem Wert {1} handelt. Die erste Zeile, die angezeigt wird, ist daher {1}.

Die Steuerung wird erneut 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}), und diese übergibt erneut den Baum, der auf dem Stapel gespeichert und an den Client ausgegeben werden soll. Der Client hat nun die Sequenz {1}, {2} erhalten.

Wenn Sie eine Konvention anwenden, bei der sich der obere Rand des Stapels LIFO links) befindet, enthält der Stapel jetzt {2, 1}. Wenn die Steuerung erneut an den Tabellenscan übergeben wird, werden keine weiteren Zeilen und gemeldet Die Steuerung wird an den Verkettungsoperator zurückgegeben, der die zweite Eingabe öffnet (für die Übergabe an die Stapelspule ist eine Zeile erforderlich), und die Steuerung wird zum ersten Mal an die innere Verknüpfung übergeben.

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

Nachdem der innere Join eine Zeile an seinem äußeren Eingang erhalten hat, übergibt er seinen inneren Eingang an den linken Antisemi-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 sie aufsteigend sortiert (wie es passiert).

Die erste von der Sortierung ausgegebene Zeile ist daher der Wert {1}. Die Innenseite des LASJ gibt den aktuellen Wert des rekursiven Elements (der Wert, der gerade vom Stapel gesprungen 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} fließt den Abfrageplanbaum zum Index (Stack) -Spool hinauf, wo er dem Stapel hinzugefügt wird, der jetzt {1, 1} enthält, und an den Client ausgegeben wird. Der Client hat nun die Sequenz {1}, {2}, {1} erhalten.

Die Steuerung geht jetzt zurück zur Verkettung, zurück auf der Innenseite (sie hat das letzte Mal eine Zeile zurückgegeben, könnte es erneut tun), zurück über die innere Verknüpfung zum LASJ. Es liest seine innere Eingabe erneut und erhält den Wert {2} von der Sortierung.

Das rekursive Element ist immer noch {2}, daher findet der LASJ diesmal {2} und {2}, was dazu führt, dass keine Zeile ausgegeben wird. Wenn an der inneren Eingabe keine Zeilen mehr gefunden werden (die Sortierung ist jetzt nicht mehr in Zeilen enthalten), wird die Steuerung wieder an die innere Verknüpfung übergeben.

Der innere Join liest seine äußere Eingabe, was dazu führt, dass der Wert {1} vom Stapel {1, 1} entfernt wird und der Stapel nur mit {1} belassen wird. Der Vorgang wird nun wiederholt, wobei der Wert {2} aus einem neuen Aufruf von Table Scan and Sort den LASJ-Test besteht und dem Stapel hinzugefügt wird und an den Client übergeben wird, der jetzt {1}, {2} erhalten hat. {1}, {2} ... und los geht's.

Mein Favorit Erklärung der in rekursiven CTE-Plänen verwendeten Stapelspule ist Craig Freedmans.

26
Paul White 9

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

  1. Teilen Sie den CTE-Ausdruck in Anker- und rekursive Elemente auf.
  2. Führen Sie die Ankerelemente aus, die den ersten Aufruf oder die erste 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. Geben Sie die Ergebnismenge zurück. Dies ist eine UNION ALL von T0 bis Tn.

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

Wenn ich dies auf Ihren CTE anwende, 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 gibt eindeutig 1,2,3 Als T0 Zurück.

Danach wird der rekursive Ausdruck ausgeführt

select a
from cte
except
select a
from r

Mit 1,2,3 Als Eingabe, die eine Ausgabe von 4,5 Als T1 Ergibt, wird durch erneutes Einstecken für die nächste Rekursionsrunde 1,2,3 Zurückgegeben und so weiter unbegrenzt.

Dies ist jedoch nicht das, was tatsächlich passiert. 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 |
+-----------+---------+---+---+---+

Wenn Sie OPTION (MAXRECURSION 1) verwenden und in Schritten von 1 Nach oben korrigieren, können Sie sehen, dass es in einen Zyklus eintritt, in dem jede aufeinanderfolgende Ebene kontinuierlich zwischen der Ausgabe von 1,2,3,4 Und 1,2,3,5.

Wie von @ Quassnoi in dieser Blog-Beitrag besprochen. Das Muster der beobachteten Ergebnisse ist so, als würde jeder Aufruf (1),(2),(3),(4),(5) EXCEPT (X) Ausführen, wobei X die letzte Zeile des vorherigen Aufrufs ist.

Bearbeiten: Nach dem Lesen von SQL Kiwis ausgezeichnete Antwort ist klar, warum dies geschieht und dass dies nicht die ganze Geschichte ist Es sind noch viele Dinge auf dem Stapel, die niemals verarbeitet werden können.

Anchor sendet 1,2,3 An den Client-Stapelinhalt 3,2,1

3 vom Stapel geknallt, Stapelinhalt 2,1

Der LASJ gibt 1,2,4,5, Stapelinhalt 5,4,2,1,2,1 Zurück.

5 vom Stapel geknallt, Stapelinhalt 4,2,1,2,1

Der LASJ gibt 1,2,3,4 Stapelinhalt zurück 4,3,2,1,5,4,2,1,2,1

4 vom Stapel gestapelt, Stapelinhalt 3,2,1,5,4,2,1,2,1

Der LASJ gibt 1,2,3,5 Stapelinhalt zurück 5,3,2,1,3,2,1,5,4,2,1,2,1

5 vom Stapel geknallt, Stapelinhalt 3,2,1,3,2,1,5,4,2,1,2,1

Der LASJ gibt 1,2,3,4 Stapelinhalt zurück 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 (ohne Duplikate/NULL) 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 Verweise sind in Unterabfragen nicht zulässig" aus. Vielleicht ist es ein Versehen, dass EXCEPT in diesem Fall sogar erlaubt ist.

Ergänzung: Microsoft hat jetzt auf meine Connect Feedback wie folgt geantwortet

Jack 's Vermutung ist richtig: Dies sollte ein Syntaxfehler gewesen sein; rekursive Verweise sollten in EXCEPT -Klauseln in der Tat nicht zulässig 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 über EXCEPT folgen wir dem ANSI-SQL-Standard, der diese Einschränkung seit Einführung der Rekursion enthält (glaube ich 1999). Es gibt keine weit verbreitete Übereinstimmung darüber, wie die Semantik für die Rekursion über EXCEPT (auch als "nicht geschichtete Negation" bezeichnet) in deklarativen Sprachen wie SQL sein sollte. 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 endgültige Implementierung im Jahr 2014 für Datenbanken durchgeführt wurde mit einem Kompatibilitätsgrad von 120 oder höher .

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

31
Martin Smith