it-swarm.it

Utilizzo di EXCEPT in un'espressione di tabella comune ricorsiva

Perché la seguente query restituisce righe infinite? Mi sarei aspettato che la clausola EXCEPT terminasse la ricorsione.

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

Mi sono imbattuto in questo mentre cercavo di rispondere a un domanda su Stack Overflow.

33
Tom Hunter

Vedere risposta di Martin Smith per informazioni sullo stato corrente di EXCEPT in un CTE ricorsivo.

Per spiegare cosa stavi vedendo e perché:

Sto usando una variabile di tabella qui, per rendere più chiara la distinzione tra i valori di ancoraggio e l'elemento ricorsivo (non cambia la semantica).

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)

Il piano di query è:

Recursive CTE Plan

L'esecuzione inizia dalla radice del piano (SELEZIONA) e il controllo passa l'albero alla bobina dell'indice, alla concatenazione e quindi alla scansione della tabella di livello superiore.

La prima riga della scansione passa l'albero e viene (a) memorizzata nello Stack Spool e (b) restituita al client. Quale riga è la prima non è definita, ma supponiamo che sia la riga con il valore {1}, per ragioni di argomento. La prima riga che appare è quindi {1}.

Il controllo passa di nuovo alla Scansione tabella (l'operatore di concatenazione consuma tutte le righe dal suo input più esterno prima di aprire quello successivo). La scansione emette la seconda riga (valore {2}) e ​​questo passa nuovamente l'albero per essere archiviato nello stack e inviato al client. Il client ha ora ricevuto la sequenza {1}, {2}.

Adozione di una convenzione in cui la parte superiore dello stack LIFO si trova a sinistra, lo stack ora contiene {2, 1}. Quando il controllo passa di nuovo alla Scansione tabella, non riporta più righe e il controllo ritorna all'operatore di concatenazione, che apre il suo secondo input (ha bisogno di una riga per passare allo spool dello stack) e il controllo passa per la Join interna per la prima volta.

Il join interno chiama lo spool della tabella sul suo input esterno, che legge la riga superiore dallo stack {2} e la elimina dal piano di lavoro. Lo stack ora contiene {1}.

Dopo aver ricevuto una riga sul suo input esterno, il Join interno passa il controllo verso il basso del suo input interno al Left Anti-Semi Join (LASJ). Ciò richiede una riga dal suo input esterno, passando il controllo all'ordinamento. L'ordinamento è un iteratore di blocco, quindi legge tutte le righe dalla variabile della tabella e le ordina in ordine crescente (come accade).

La prima riga emessa dall'ordinamento è quindi il valore {1}. Il lato interno di LASJ restituisce il valore corrente del membro ricorsivo (il valore è appena saltato fuori dallo stack), che è {2}. I valori in LASJ sono {1} e {2} quindi viene emesso {1}, poiché i valori non corrispondono.

Questa riga {1} scorre la struttura del piano di query nello spool Index (Stack) dove viene aggiunta allo stack, che ora contiene {1, 1} ed emessa al client. Il client ha ora ricevuto la sequenza {1}, {2}, {1}.

Il controllo ora passa di nuovo alla Concatenazione, indietro lungo il lato interno (è tornato una riga l'ultima volta, potrebbe fare di nuovo), giù attraverso l'Unione Interna, al LASJ. Legge di nuovo l'input interno, ottenendo il valore {2} dall'ordinamento.

Il membro ricorsivo è ancora {2}, quindi questa volta il LASJ trova {2} e {2}, senza che venga emessa alcuna riga. Non trovando più righe nel suo input interno (l'ordinamento è ora fuori dalle righe), il controllo passa nuovamente al Join interno.

Il Join interno legge il suo input esterno, il che comporta che il valore {1} viene rimosso dallo stack {1, 1}, lasciando lo stack con solo {1}. Il processo ora si ripete, con il valore {2} da una nuova invocazione di Scansione e ordinamento tabelle che supera il test LASJ e viene aggiunto allo stack e passa al client, che ora ha ricevuto {1}, {2}, {1}, {2} ... e proseguiamo.

Il mio preferito spiegazione della bobina Stack utilizzata nei piani CTE ricorsivi è quello di Craig Freedman.

26
Paul White 9

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

  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 è un'UNIONE TUTTA da T0 a Tn.

Nota quanto sopra è una descrizione logica . L'ordine fisico delle operazioni può essere leggermente diverso come illustrato qui

Applicando questo al tuo CTE mi aspetterei un ciclo infinito con il seguente schema

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

Perché

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

è l'espressione Anchor. Ciò restituisce chiaramente 1,2,3 Come T0

Successivamente viene eseguita l'espressione ricorsiva

select a
from cte
except
select a
from r

Con 1,2,3 Come input che produrrà un output di 4,5 Come T1, Ricollegandolo per il prossimo round di ricorsione tornerà 1,2,3 E così via indefinitamente.

Questo non è ciò che effettivamente accade comunque. Questi sono i risultati delle prime 5 invocazioni

+-----------+---------+---+---+---+
| 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 |
+-----------+---------+---+---+---+

Usando OPTION (MAXRECURSION 1) e regolando verso l'alto con incrementi di 1 Si può vedere che entra in un ciclo in cui ogni livello successivo commuta continuamente tra l'output 1,2,3,4 E 1,2,3,5.

Come discusso da @ Quassnoi in questo post del blog . Lo schema dei risultati osservati è come se ogni invocazione stesse eseguendo (1),(2),(3),(4),(5) EXCEPT (X) Dove X è l'ultima riga dell'invocazione precedente.

Modifica: Dopo aver letto eccellente risposta di Kiwi SQL è chiaro sia il motivo per cui ciò accade e che questa non è l'intera storia ci sono ancora un sacco di cose rimaste nello stack che non possono mai essere elaborate.

Ancora emette 1,2,3 Al contenuto dello stack client 3,2,1

3 spuntati fuori pila, Contenuti pila 2,1

Il LASJ restituisce 1,2,4,5, Contenuto dello stack 5,4,2,1,2,1

5 saltato fuori dalla pila, Contenuti della pila 4,2,1,2,1

Il LASJ restituisce 1,2,3,4 Contenuto dello stack 4,3,2,1,5,4,2,1,2,1

4 saltati fuori dalla pila, Contenuti della pila 3,2,1,5,4,2,1,2,1

Il LASJ restituisce 1,2,3,5 Contenuto dello stack 5,3,2,1,3,2,1,5,4,2,1,2,1

5 saltato fuori dalla pila, Contenuti della pila 3,2,1,3,2,1,5,4,2,1,2,1

Il LASJ restituisce 1,2,3,4 Contenuto dello stack 4,3,2,1,3,2,1,3,2,1,5,4,2,1,2,1

Se si tenta di sostituire il membro ricorsivo con l'espressione logicamente equivalente (in assenza di duplicati/NULL)

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

Ciò non è consentito e genera l'errore "I riferimenti ricorsivi non sono consentiti nelle sottoquery". quindi forse è una svista che EXCEPT sia persino autorizzato in questo caso.

Aggiunta: Microsoft ha ora risposto al mio Connect Feedback come di seguito

Jack L'ipotesi è corretta: questo avrebbe dovuto essere un errore di sintassi; i riferimenti ricorsivi non dovrebbero in effetti essere ammessi nelle clausole EXCEPT. Abbiamo in programma di risolvere questo bug in una prossima versione del servizio. Nel frattempo, suggerirei di evitare riferimenti ricorsivi nelle clausole EXCEPT.

Nel limitare la ricorsione su EXCEPT seguiamo lo standard ANSI SQL, che ha incluso questa restrizione sin da quando è stata introdotta la ricorsione (nel 1999 credo). Non esiste un accordo diffuso su quale dovrebbe essere la semantica per la ricorsione su EXCEPT (anche chiamata "negazione non stratificata") in linguaggi dichiarativi come SQL. Inoltre, è notoriamente difficile (se non impossibile) implementare tale semantica in modo efficiente (per database di dimensioni ragionevoli) in un sistema RDBMS.

E sembra che l'eventuale implementazione sia stata fatta nel 2014 per i database con un livello di compatibilità di 120 o superiore .

I riferimenti ricorsivi in ​​una clausola EXCEPT generano un errore conforme allo standard ANSI SQL.

31
Martin Smith