Come funziona effettivamente la ricorsione SQL?


19

Venendo a SQL da altri linguaggi di programmazione, la struttura di una query ricorsiva sembra piuttosto strana. Attraversalo passo dopo passo, e sembra cadere a pezzi.

Considera il seguente semplice esempio:

CREATE TABLE #NUMS
(N BIGINT);

INSERT INTO #NUMS
VALUES (3), (5), (7);

WITH R AS
(
    SELECT N FROM #NUMS
    UNION ALL
    SELECT N*N AS N FROM R WHERE N*N < 10000000
)
SELECT N FROM R ORDER BY N;

Camminiamo attraverso di esso.

Innanzitutto, l'elemento di ancoraggio viene eseguito e il set di risultati viene inserito in R. Quindi R viene inizializzato su {3, 5, 7}.

Quindi, l'esecuzione scende al di sotto di UNION ALL e il membro ricorsivo viene eseguito per la prima volta. Viene eseguito su R (ovvero su R che attualmente abbiamo in mano: {3, 5, 7}). Ciò si traduce in {9, 25, 49}.

Cosa fa con questo nuovo risultato? Aggiunge {9, 25, 49} all'esistente {3, 5, 7}, etichetta l'unione risultante R, e quindi continua con la ricorsione da lì? O ridefinisce R come solo questo nuovo risultato {9, 25, 49} e fa tutto il sindacato più tardi?

Nessuna scelta ha senso.

Se R è ora {3, 5, 7, 9, 25, 49} e eseguiamo la prossima iterazione della ricorsione, allora finiremo con {9, 25, 49, 81, 625, 2401} e abbiamo perso {3, 5, 7}.

Se R ora è solo {9, 25, 49}, allora abbiamo un problema di etichettatura errata. R è inteso come l'unione dell'insieme di risultati dell'elemento di ancoraggio e di tutti i successivi insiemi di risultati dell'elemento ricorsivo. Considerando che {9, 25, 49} è solo una componente di R. Non è la R completa che abbiamo accumulato finora. Pertanto, scrivere l'elemento ricorsivo come selezionando da R non ha senso.


Apprezzo sicuramente ciò che @Max Vernon e @Michael S. hanno dettagliato di seguito. Vale a dire che (1) tutti i componenti vengono creati fino al limite di ricorsione o set null, quindi (2) tutti i componenti vengono uniti insieme. Questo è il modo in cui capisco la ricorsione SQL per funzionare davvero.

Se stessimo ridisegnando SQL, forse applicheremmo una sintassi più chiara ed esplicita, qualcosa del genere:

WITH R AS
(
    SELECT   N
    INTO     R[0]
    FROM     #NUMS
    UNION ALL
    SELECT   N*N AS N
    INTO     R[K+1]
    FROM     R[K]
    WHERE    N*N < 10000000
)
SELECT N FROM R ORDER BY N;

Un po 'come una prova induttiva in matematica.

Il problema con la ricorsione di SQL così com'è attualmente è che è scritto in modo confuso. Il modo in cui è scritto dice che ogni componente è formato selezionando da R, ma non significa che l'intera R che è stata (o, sembra essere stata costruita) finora. Significa solo il componente precedente.


"Se R è ora {3, 5, 7, 9, 25, 49} e eseguiamo la prossima iterazione della ricorsione, allora finiremo con {9, 25, 49, 81, 625, 2401} e noi ' ho perso {3, 5, 7}. " Non vedo come perdi {3,5,7} se funziona così.
ypercubeᵀᴹ

@ yper-crazyhat-cubeᵀᴹ - Stavo seguendo la prima ipotesi che ho proposto, vale a dire, se la R intermedia fosse un accumulo di tutto ciò che era stato calcolato fino a quel punto? Quindi, alla successiva iterazione dell'elemento ricorsivo, ogni elemento di R viene quadrato. Pertanto, {3, 5, 7} diventa {9, 25, 49} e non abbiamo mai più {3, 5, 7} in R. In altre parole, {3, 5, 7} viene perso da R.
UnLogicGuys

Risposte:


26

La descrizione BOL dei CTE ricorsivi descrive la semantica dell'esecuzione ricorsiva come la seguente:

  1. Dividi l'espressione CTE in membri anchor e ricorsivi.
  2. Eseguire i membri di ancoraggio creando la prima chiamata o il set di risultati di base (T0).
  3. Eseguire i membri ricorsivi con Ti come input e Ti + 1 come output.
  4. Ripetere il passaggio 3 fino a quando non viene restituito un set vuoto.
  5. Restituisce il set di risultati. Questa è UNIONE TUTTA da T0 a Tn.

Quindi ogni livello ha solo come input il livello sopra non l'intero insieme di risultati accumulato finora.

Quanto sopra è come funziona logicamente . I CTE fisicamente ricorsivi sono attualmente sempre implementati con loop nidificati e uno spool di stack in SQL Server. Questo è descritto qui e qui e significa che in pratica ogni elemento ricorsivo sta funzionando con la riga genitore del livello precedente, non dell'intero livello. Ma le varie restrizioni sulla sintassi consentita nei CTE ricorsivi fanno sì che questo approccio funzioni.

Se si rimuove ORDER BYdalla query i risultati vengono ordinati come segue

+---------+
|    N    |
+---------+
|       3 |
|       5 |
|       7 |
|      49 |
|    2401 |
| 5764801 |
|      25 |
|     625 |
|  390625 |
|       9 |
|      81 |
|    6561 |
+---------+

Questo perché il piano di esecuzione funziona in modo molto simile al seguente C#

using System;
using System.Collections.Generic;
using System.Diagnostics;

public class Program
{
    private static readonly Stack<dynamic> StackSpool = new Stack<dynamic>();

    private static void Main(string[] args)
    {
        //temp table #NUMS
        var nums = new[] { 3, 5, 7 };

        //Anchor member
        foreach (var number in nums)
            AddToStackSpoolAndEmit(number, 0);

        //Recursive part
        ProcessStackSpool();

        Console.WriteLine("Finished");
        Console.ReadLine();
    }

    private static void AddToStackSpoolAndEmit(long number, int recursionLevel)
    {
        StackSpool.Push(new { N = number, RecursionLevel = recursionLevel });
        Console.WriteLine(number);
    }

    private static void ProcessStackSpool()
    {
        //recursion base case
        if (StackSpool.Count == 0)
            return;

        var row = StackSpool.Pop();

        int thisLevel = row.RecursionLevel + 1;
        long thisN = row.N * row.N;

        Debug.Assert(thisLevel <= 100, "max recursion level exceeded");

        if (thisN < 10000000)
            AddToStackSpoolAndEmit(thisN, thisLevel);

        ProcessStackSpool();
    }
}

NB1: come sopra al momento in cui il primo figlio dell'elemento di ancoraggio 3viene elaborato tutte le informazioni sui suoi fratelli 5e 7, e sui loro discendenti, è già stato scartato dalla bobina e non è più accessibile.

NB2: Il C # sopra ha la stessa semantica complessiva del piano di esecuzione ma il flusso nel piano di esecuzione non è identico, in quanto gli operatori lavorano in modo di pipeline exection. Questo è un esempio semplificato per dimostrare l'essenza dell'approccio. Vedi i link precedenti per maggiori dettagli sul piano stesso.

NB3: lo spool dello stack stesso è apparentemente implementato come un indice cluster non univoco con una colonna chiave di livello di ricorsione e unificatori univoci aggiunti secondo necessità ( sorgente )


6
Le query ricorsive in SQL Server vengono sempre convertite da ricorsione a iterazione (con impilamento) durante l'analisi. La regola di implementazione per l'iterazione è IterateToDepthFirst- Iterate(seed,rcsv)->PhysIterate(seed,rcsv). Cordiali saluti. Risposta eccellente.
Paul White dice GoFundMonica

Per inciso, è consentito anche UNION invece di UNION ALL, ma SQL Server non lo farà.
Joshua,

5

Questa è solo un'ipotesi (semi) istruita, ed è probabilmente completamente sbagliata. Domanda interessante, a proposito.

T-SQL è un linguaggio dichiarativo; forse un CTE ricorsivo viene tradotto in un'operazione di tipo cursore in cui i risultati dal lato sinistro di UNION ALL vengono aggiunti in una tabella temporanea, quindi il lato destro di UNION ALL viene applicato ai valori nella parte sinistra.

Quindi, prima inseriamo l'output del lato sinistro di UNION ALL nel set di risultati, quindi inseriamo i risultati del lato destro di UNION ALL applicati sul lato sinistro e inseriamo quello nel set di risultati. Il lato sinistro viene quindi sostituito con l'uscita dal lato destro e il lato destro viene nuovamente applicato al "nuovo" lato sinistro. Qualcosa come questo:

  1. {3,5,7} -> set di risultati
  2. dichiarazioni ricorsive applicate a {3,5,7}, che è {9,25,49}. {9,25,49} viene aggiunto al set di risultati e sostituisce il lato sinistro di UNION ALL.
  3. dichiarazioni ricorsive applicate a {9,25,49}, che è {81,625,2401}. {81.625,2401} viene aggiunto al set di risultati e sostituisce il lato sinistro di UNION ALL.
  4. dichiarazioni ricorsive applicate a {81.625,2401}, che è {6561.390625.5764801}. {6561.390625.5764801} è stato aggiunto al set di risultati.
  5. Il cursore è completo, poiché la successiva iterazione porta alla clausola WHERE che restituisce false.

È possibile visualizzare questo comportamento nel piano di esecuzione per il CTE ricorsivo:

inserisci qui la descrizione dell'immagine

Questo è il passaggio 1 sopra, in cui il lato sinistro di UNION ALL viene aggiunto all'output:

inserisci qui la descrizione dell'immagine

Questo è il lato destro di UNION ALL in cui l'output è concatenato al set di risultati:

inserisci qui la descrizione dell'immagine


4

La documentazione di SQL Server , che menziona T i e T i + 1 , non è né molto comprensibile, né una descrizione accurata dell'implementazione effettiva.

L'idea di base è che la parte ricorsiva della query esamina tutti i risultati precedenti, ma solo una volta .

Potrebbe essere utile esaminare come altri database lo implementano (per ottenere lo stesso risultato). La documentazione di Postgres dice:

Valutazione di query ricorsive

  1. Valuta il termine non ricorsivo. Per UNION(ma non UNION ALL), elimina le righe duplicate. Includi tutte le righe rimanenti nel risultato della query ricorsiva e inseriscile in una tabella di lavoro temporanea .
  2. Finché il tavolo di lavoro non è vuoto, ripetere questi passaggi:
    1. Valutare il termine ricorsivo, sostituendo l'attuale contenuto della tabella di lavoro con l'auto-riferimento ricorsivo. Per UNION(ma non UNION ALL), elimina le righe duplicate e le righe che duplicano qualsiasi riga del risultato precedente. Includi tutte le righe rimanenti nel risultato della query ricorsiva e inseriscile in una tabella intermedia temporanea .
    2. Sostituire il contenuto della tabella di lavoro con il contenuto della tabella intermedia, quindi svuotare la tabella intermedia.

Nota A
rigor di termini, questo processo è iterazione non ricorsione, ma RECURSIVEè la terminologia scelta dal comitato degli standard SQL.

La documentazione di SQLite suggerisce un'implementazione leggermente diversa e questo algoritmo di una riga alla volta potrebbe essere il più facile da capire:

L'algoritmo di base per calcolare il contenuto della tabella ricorsiva è il seguente:

  1. Esegui initial-selecte aggiungi i risultati a una coda.
  2. Mentre la coda non è vuota:
    1. Estrarre una singola riga dalla coda.
    2. Inserisci quella singola riga nella tabella ricorsiva
    3. Fai finta che la singola riga appena estratta sia l'unica riga nella tabella ricorsiva ed esegui recursive-select, aggiungendo tutti i risultati alla coda.

La procedura di base sopra può essere modificata dalle seguenti regole aggiuntive:

  • Se un operatore UNION si connette initial-selecta recursive-select, quindi aggiungere le righe alla coda solo se non è stata precedentemente aggiunta alla stessa riga identica. Le righe ripetute vengono eliminate prima di essere aggiunte alla coda anche se le righe ripetute sono già state estratte dalla coda dal passaggio di ricorsione. Se l'operatore è UNION ALL, tutte le righe generate da sia initial-selecte che recursive-selectvengono sempre aggiunte alla coda anche se sono ripetizioni.
    [...]

0

La mia conoscenza è specifica in DB2 ma guardare i diagrammi di spiegazione sembra essere lo stesso con SQL Server.

Il piano viene da qui:

Guardalo su Incolla il piano

Spiega il piano di SQL Server

L'ottimizzatore non esegue letteralmente un'unione per ogni query ricorsiva. Prende la struttura della query e assegna la prima parte dell'unione a un "membro di ancoraggio", quindi attraverserà tutta la seconda metà dell'unione (chiamata "membro ricorsivo" in modo ricorsivo fino a raggiungere i limiti definiti. la ricorsione è completa, l'ottimizzatore unisce tutti i record.

L'ottimizzatore lo considera solo un suggerimento per eseguire un'operazione predefinita.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.