Lassen Sie uns zunächst versuchen, die auf der Handbuchseite angegebene Algorithmusbeschreibung zu vereinfachen und zu verdeutlichen. Betrachten Sie zur Vereinfachung nur union all
in with recursive
-Klausel für jetzt (und union
später):
WITH RECURSIVE pseudo-entity-name(column-names) AS (
Initial-SELECT
UNION ALL
Recursive-SELECT using pseudo-entity-name
)
Outer-SELECT using pseudo-entity-name
Lassen Sie uns zur Verdeutlichung den Abfrageausführungsprozess in Pseudocode beschreiben:
working-recordset = result of Initial-SELECT
append working-recordset to empty outer-recordset
while( working-recordset is not empty ) begin
new working-recordset = result of Recursive-SELECT
taking previous working-recordset as pseudo-entity-name
append working-recordset to outer-recordset
end
overall-result = result of Outer-SELECT
taking outer-recordset as pseudo-entity-name
Oder noch kürzer:Die Datenbank-Engine führt die anfängliche Auswahl aus und nimmt ihre Ergebniszeilen als Arbeitssatz. Dann führt es wiederholt eine rekursive Auswahl auf dem Arbeitssatz aus, wobei jedes Mal der Inhalt des Arbeitssatzes durch das erhaltene Abfrageergebnis ersetzt wird. Dieser Prozess endet, wenn eine leere Menge durch rekursive Auswahl zurückgegeben wird. Und alle Ergebniszeilen, die zuerst durch die anfängliche Auswahl und dann durch die rekursive Auswahl gegeben wurden, werden gesammelt und der äußeren Auswahl zugeführt, deren Ergebnis zum Gesamtabfrageergebnis wird.
Diese Abfrage berechnet fakultativ von 3:
WITH RECURSIVE factorial(F,n) AS (
SELECT 1 F, 3 n
UNION ALL
SELECT F*n F, n-1 n from factorial where n>1
)
SELECT F from factorial where n=1
Wählen Sie zunächst SELECT 1 F, 3 n
gibt uns Anfangswerte:3 für Argument und 1 für Funktionswert.
Rekursive Auswahl SELECT F*n F, n-1 n from factorial where n>1
gibt an, dass wir jedes Mal den letzten Funktionswert mit dem letzten Argumentwert multiplizieren und den Argumentwert verringern müssen.
Die Datenbank-Engine führt dies folgendermaßen aus:
Zuerst führt es initail select aus, was den Anfangszustand des Arbeitsrecordsets angibt:
F | n
--+--
1 | 3
Dann transformiert es das Arbeits-Recordset mit einer rekursiven Abfrage und erhält seinen zweiten Status:
F | n
--+--
3 | 2
Dann dritter Zustand:
F | n
--+--
6 | 1
Im dritten Zustand folgt keine Zeile auf n>1
Bedingung in rekursiver Auswahl, so dass der Arbeitssatz Schleifenausgänge ist.
Der äußere Datensatz enthält jetzt alle Zeilen, die von der anfänglichen und rekursiven Auswahl zurückgegeben werden:
F | n
--+--
1 | 3
3 | 2
6 | 1
Die äußere Auswahl filtert alle Zwischenergebnisse aus dem äußeren Datensatz heraus und zeigt nur den endgültigen Fakultätswert an, der zum Gesamtabfrageergebnis wird:
F
--
6
Betrachten wir nun die Tabelle forest(id,parent_id,name)
:
id | parent_id | name
---+-----------+-----------------
1 | | item 1
2 | 1 | subitem 1.1
3 | 1 | subitem 1.2
4 | 1 | subitem 1.3
5 | 3 | subsubitem 1.2.1
6 | | item 2
7 | 6 | subitem 2.1
8 | | item 3
'Kompletter Baum erweitern ' bedeutet hier das Sortieren von Baumelementen in menschenlesbarer Tiefenreihenfolge, während ihre Ebenen und (möglicherweise) Pfade berechnet werden. Beide Aufgaben (des korrekten Sortierens und Berechnens von Ebene oder Pfad) sind nicht in einem (oder sogar einer konstanten Anzahl von) SELECT lösbar, ohne die WITH RECURSIVE-Klausel (oder die Oracle CONNECT BY-Klausel, die von PostgreSQL nicht unterstützt wird) zu verwenden. Aber diese rekursive Abfrage erledigt die Aufgabe (na ja, fast, siehe Anmerkung unten):
WITH RECURSIVE fulltree(id,parent_id,level,name,path) AS (
SELECT id, parent_id, 1 as level, name, name||'' as path from forest where parent_id is null
UNION ALL
SELECT t.id, t.parent_id, ft.level+1 as level, t.name, ft.path||' / '||t.name as path
from forest t, fulltree ft where t.parent_id = ft.id
)
SELECT * from fulltree order by path
Die Datenbank-Engine führt es wie folgt aus:
Zuerst führt es initail select aus, das alle Elemente der höchsten Ebene (Roots) aus forest
liefert Tabelle:
id | parent_id | level | name | path
---+-----------+-------+------------------+----------------------------------------
1 | | 1 | item 1 | item 1
8 | | 1 | item 3 | item 3
6 | | 1 | item 2 | item 2
Dann führt es eine rekursive Auswahl aus, die alle Elemente der 2. Ebene aus forest
liefert Tabelle:
id | parent_id | level | name | path
---+-----------+-------+------------------+----------------------------------------
2 | 1 | 2 | subitem 1.1 | item 1 / subitem 1.1
3 | 1 | 2 | subitem 1.2 | item 1 / subitem 1.2
4 | 1 | 2 | subitem 1.3 | item 1 / subitem 1.3
7 | 6 | 2 | subitem 2.1 | item 2 / subitem 2.1
Dann führt es erneut eine rekursive Auswahl aus und ruft Elemente der 3D-Ebene ab:
id | parent_id | level | name | path
---+-----------+-------+------------------+----------------------------------------
5 | 3 | 3 | subsubitem 1.2.1 | item 1 / subitem 1.2 / subsubitem 1.2.1
Und jetzt führt es erneut eine rekursive Auswahl aus und versucht, Elemente der 4. Ebene abzurufen, aber es gibt keine davon, also wird die Schleife beendet.
Das äußere SELECT legt die richtige, für Menschen lesbare Zeilenreihenfolge fest und sortiert nach der Pfadspalte:
id | parent_id | level | name | path
---+-----------+-------+------------------+----------------------------------------
1 | | 1 | item 1 | item 1
2 | 1 | 2 | subitem 1.1 | item 1 / subitem 1.1
3 | 1 | 2 | subitem 1.2 | item 1 / subitem 1.2
5 | 3 | 3 | subsubitem 1.2.1 | item 1 / subitem 1.2 / subsubitem 1.2.1
4 | 1 | 2 | subitem 1.3 | item 1 / subitem 1.3
6 | | 1 | item 2 | item 2
7 | 6 | 2 | subitem 2.1 | item 2 / subitem 2.1
8 | | 1 | item 3 | item 3
HINWEIS :Die resultierende Zeilenreihenfolge bleibt nur dann korrekt, wenn keine Satzzeichen vor der Sortierung /
vorhanden sind in den Artikelnamen. Wenn wir Item 2
umbenennen in Item 1 *
, wird es die Zeilenreihenfolge aufheben und zwischen Item 1
stehen und seine Nachkommen.
Eine stabilere Lösung ist die Verwendung des Tabulatorzeichens (E'\t'
) als Pfadtrennzeichen in der Abfrage (das später durch besser lesbare Pfadtrennzeichen ersetzt werden kann:in der äußeren Auswahl, vor der Anzeige für den Menschen usw.). Durch Tabulatoren getrennte Pfade behalten die korrekte Reihenfolge bei, bis Tabulatoren oder Steuerzeichen in den Elementnamen vorhanden sind - was leicht überprüft und ohne Einbußen bei der Benutzerfreundlichkeit ausgeschlossen werden kann.
Es ist sehr einfach, die letzte Abfrage zu ändern, um einen beliebigen Unterbaum zu erweitern - Sie müssen nur die Bedingung parent_id is null
ersetzen mit perent_id=1
(zum Beispiel). Beachten Sie, dass diese Abfragevariante alle Ebenen und Pfade relativ zu zurückgibt Item 1
.
Und nun zu typischen Fehlern . Der bemerkenswerteste typische Fehler, der für rekursive Abfragen spezifisch ist, ist das Definieren von Stoppbedingungen in der rekursiven Auswahl, was zu einer Endlosschleife führt.
Zum Beispiel, wenn wir where n>1
weglassen Bedingung im faktoriellen Beispiel oben, wird die Ausführung der rekursiven Auswahl niemals eine leere Menge ergeben (weil wir keine Bedingung haben, um eine einzelne Zeile herauszufiltern) und die Schleife wird unendlich fortgesetzt.
Das ist der wahrscheinlichste Grund, warum einige Ihrer Abfragen hängen bleiben (der andere unspezifische, aber immer noch mögliche Grund ist sehr ineffektives select, das in begrenzter, aber sehr langer Zeit ausgeführt wird).
Es gibt nicht viele RECURSIVE-spezifische Abfrage-Richtlinien zu erwähnen, soweit ich weiß. Aber ich möchte (ziemlich naheliegend) ein schrittweises rekursives Verfahren zum Erstellen von Abfragen vorschlagen.
-
Erstellen und debuggen Sie Ihre anfängliche Auswahl separat.
-
Verpacken Sie es mit einem Scaffolding WITH RECURSIVE-Konstrukt
und beginnen Sie mit dem Erstellen und Debuggen Ihrer rekursiven Auswahl.
Das empfohlene Scuffolding-Konstrukt sieht so aus:
WITH RECURSIVE rec( <Your column names> ) AS (
<Your ready and working initial SELECT>
UNION ALL
<Recursive SELECT that you are debugging now>
)
SELECT * from rec limit 1000
Diese einfachste äußere Auswahl gibt das gesamte äußere Recordset aus, das, wie wir wissen, alle Ausgabezeilen der anfänglichen Auswahl und jede Ausführung der rekursiven Auswahl in einer Schleife in ihrer ursprünglichen Ausgabereihenfolge enthält - genau wie in den obigen Beispielen! Das limit 1000
Ein Teil verhindert das Aufhängen und ersetzt es durch eine übergroße Ausgabe, in der Sie den verpassten Haltepunkt sehen können.
- Nach dem Debuggen der initialen und rekursiven Auswahl erstellen und debuggen Sie Ihre äußere Auswahl.
Und jetzt das letzte, was zu erwähnen ist – der Unterschied bei der Verwendung von union
statt union all
in with recursive
Klausel. Es führt eine Zeileneindeutigkeitsbeschränkung ein, die zu zwei zusätzlichen Zeilen in unserem Ausführungs-Pseudocode führt:
working-recordset = result of Initial-SELECT
discard duplicate rows from working-recordset /*union-specific*/
append working-recordset to empty outer-recordset
while( working-recordset is not empty ) begin
new working-recordset = result of Recursive-SELECT
taking previous working-recordset as pseudo-entity-name
discard duplicate rows and rows that have duplicates in outer-recordset
from working-recordset /*union-specific*/
append working-recordset to outer-recordset
end
overall-result = result of Outer-SELECT
taking outer-recordset as pseudo-entity-name