Utilizzo di EXCEPT in un'espressione di tabella comune ricorsiva


33

Perché la seguente query restituisce righe infinite? Mi sarei aspettato che la EXCEPTclausola 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 una domanda su Stack Overflow.

Risposte:


26

Vedere la risposta di Martin Smith per informazioni sullo stato attuale di EXCEPTun 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 è:

Piano CTE ricorsivo

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 nuovamente 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 al Join interno 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 (quando succede).

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 il suo 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 di nuovo 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.

La mia spiegazione preferita della bobina Stack utilizzata nei piani CTE ricorsivi è quella di Craig Freedman.


31

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.

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ò ritorna chiaramente 1,2,3comeT0

Successivamente viene eseguita l'espressione ricorsiva

select a
from cte
except
select a
from r

Con 1,2,3come input che produrrà un output di 4,5come T1quindi ricollegandolo che tornerà per il prossimo round di ricorsione tornerà 1,2,3e 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 |
+-----------+---------+---+---+---+

Dall'uso OPTION (MAXRECURSION 1)e dalla regolazione verso l'alto con incrementi di 1esso si può vedere che entra in un ciclo in cui ogni livello successivo commuta continuamente tra l'output 1,2,3,4e 1,2,3,5.

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

Modifica: dopo aver letto l'eccellente risposta di SQL Kiwi, è chiaro sia il motivo per cui ciò si verifica sia che questa non è l'intera storia in quanto vi sono ancora un sacco di cose rimaste nello stack che non possono mai essere elaborate.

Ancoraggio Emette 1,2,3al contenuto dello stack client3,2,1

3 fuoriuscite dallo stack, Contenuti dello stack 2,1

Il LASJ ritorna 1,2,4,5, Stack Stack5,4,2,1,2,1

5 saltar fuori pila, Stack Stack 4,2,1,2,1

Il LASJ restituisce i 1,2,3,4 contenuti dello stack4,3,2,1,5,4,2,1,2,1

4 spuntato fuori pila, pila contenuti 3,2,1,5,4,2,1,2,1

Il LASJ restituisce i 1,2,3,5 contenuti dello stack5,3,2,1,3,2,1,5,4,2,1,2,1

5 saltar fuori pila, Stack Stack 3,2,1,3,2,1,5,4,2,1,2,1

Il LASJ restituisce i 1,2,3,4 contenuti 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 EXCEPTpersino consentita in questo caso.

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

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

Nel limitare la ricorsione EXCEPTseguiamo lo standard ANSI SQL, che ha incluso questa restrizione sin dall'introduzione della ricorsione (nel 1999 credo). Non esiste un accordo diffuso su ciò che dovrebbe essere la semantica per la ricorsione 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 effettuata nel 2014 per 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.

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.