Usando EXCEPT em uma expressão de tabela comum recursiva

33

Por que a consulta a seguir retorna infinitas linhas? Eu esperava que a EXCEPTcláusula encerrasse a recursão ..

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

Me deparei com isso enquanto tentava responder a uma pergunta no Stack Overflow.

Tom Hunter
fonte

Respostas:

26

Consulte a resposta de Martin Smith para obter informações sobre o status atual de EXCEPTuma CTE recursiva.

Para explicar o que você estava vendo e por que:

Estou usando uma variável de tabela aqui, para tornar mais clara a distinção entre os valores âncora e o item recursivo (isso não altera a semântica).

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)

O plano de consulta é:

Plano recursivo de CTE

A execução começa na raiz do plano (o SELECT) e o controle passa a árvore para o Spool de Índice, Concatenação e, em seguida, para a Verificação de Tabela de nível superior.

A primeira linha da varredura passa pela árvore e é (a) armazenada no Spool de Pilha e (b) retornada ao cliente. Qual linha é a primeira não está definida, mas vamos assumir que é a linha com o valor {1}, por uma questão de argumento. A primeira linha a aparecer é, portanto, {1}.

O controle passa novamente para a varredura de tabela (o operador de concatenação consome todas as linhas da entrada mais externa antes de abrir a próxima). A varredura emite a segunda linha (valor {2}) e, novamente, passa a árvore para ser armazenada na pilha e enviada ao cliente. O cliente agora recebeu a sequência {1}, {2}.

Adotando uma convenção em que a parte superior da pilha LIFO está à esquerda, a pilha agora contém {2, 1}. Quando o controle passa novamente para a Varredura de tabela, ele não reporta mais linhas e o controle volta para o operador Concatenation, que abre sua segunda entrada (ele precisa de uma linha para passar para o spool da pilha) e o controle passa para a Junção interna pela primeira vez.

A junção interna chama o spool de tabela em sua entrada externa, que lê a linha superior da pilha {2} e a exclui da mesa de trabalho. A pilha agora contém {1}.

Após receber uma linha em sua entrada externa, a Junção Interna passa o controle para baixo para a Junta Anti-Semi Esquerda (LASJ). Isso solicita uma linha de sua entrada externa, passando o controle para o Sort. Sort é um iterador de bloqueio, portanto, ele lê todas as linhas da variável da tabela e as classifica em ordem crescente (por acaso).

A primeira linha emitida pelo Sort é, portanto, o valor {1}. O lado interno do LASJ retorna o valor atual do membro recursivo (o valor que acabou de sair da pilha), que é {2}. Os valores no LASJ são {1} e {2} para que {1} seja emitido, pois os valores não correspondem.

Essa linha {1} flui pela árvore do plano de consulta para o Spool de Índice (Pilha), onde é adicionada à pilha, que agora contém {1, 1} e emitida para o cliente. O cliente agora recebeu a sequência {1}, {2}, {1}.

O controle agora passa de volta para a concatenação, volta para o lado interno (retornou uma linha da última vez, pode ser novamente), pela junção interna, para o LASJ. Ele lê sua entrada interna novamente, obtendo o valor {2} da Classificação.

O membro recursivo ainda é {2}, portanto, desta vez, o LASJ localiza {2} e {2}, resultando em nenhuma linha sendo emitida. Não encontrando mais linhas em sua entrada interna (a Classificação agora está fora de linhas), o controle passa novamente para a Junção Interna.

O Inner Join lê sua entrada externa, o que resulta no valor {1} sendo retirado da pilha {1, 1}, deixando a pilha com apenas {1}. O processo agora se repete, com o valor {2} de uma nova chamada da Tabela Scan and Sort passando no teste LASJ e sendo adicionado à pilha, e passa para o cliente, que agora recebeu {1}, {2}, {1}, {2} ... e vamos lá.

Minha explicação favorita do spool Stack usado nos planos recursivos de CTE é o de Craig Freedman.

Paul White diz que a GoFundMonica
fonte
31

A descrição da BOL de CTEs recursivas descreve a semântica da execução recursiva da seguinte maneira:

  1. Divida a expressão CTE em membros âncora e recursivos.
  2. Execute o (s) membro (s) âncora (s) criando a primeira chamada ou conjunto de resultados base (T0).
  3. Execute os membros recursivos com Ti como entrada e Ti + 1 como saída.
  4. Repita a etapa 3 até que um conjunto vazio seja retornado.
  5. Retorne o conjunto de resultados. Este é um UNION ALL de T0 a Tn.

Observe o acima é uma descrição lógica .A ordem física das operações pode ser um pouco diferente, como ilustrado aqui

Aplicando isso ao seu CTE, eu esperaria um loop infinito com o seguinte padrão

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

Porque

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

é a expressão âncora. Isso claramente retorna 1,2,3comoT0

Depois disso, a expressão recursiva é executada

select a
from cte
except
select a
from r

Com a 1,2,3entrada que produzirá uma saída da 4,5mesma forma T1que conectá-la novamente para a próxima rodada de recursão retornará 1,2,3e assim por diante indefinidamente.

Isto não é o que realmente acontece no entanto. Estes são os resultados das 5 primeiras invocações

+-----------+---------+---+---+---+
| 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 ajustando para cima em incrementos 1, pode-se ver que ele entra em um ciclo em que cada nível sucessivo alterna continuamente entre a saída 1,2,3,4e 1,2,3,5.

Como discutido por @Quassnoi em este post . O padrão dos resultados observados é como se cada chamada estivesse fazendo (1),(2),(3),(4),(5) EXCEPT (X)onde Xestá a última linha da chamada anterior.

Edit: Depois de ler a excelente resposta do SQL Kiwi , fica claro por que isso ocorre e que essa não é a história toda, pois ainda há muitas coisas na pilha que nunca podem ser processadas.

A âncora é emitida 1,2,3para o conteúdo da pilha do cliente3,2,1

3 saiu da pilha, Conteúdo da pilha 2,1

O LASJ retorna 1,2,4,5, Conteúdo da pilha5,4,2,1,2,1

5 saiu da pilha, Conteúdo da pilha 4,2,1,2,1

O LASJ retorna o 1,2,3,4 Conteúdo da pilha4,3,2,1,5,4,2,1,2,1

4 saiu da pilha, Conteúdo da pilha 3,2,1,5,4,2,1,2,1

O LASJ retorna o 1,2,3,5 Conteúdo da pilha5,3,2,1,3,2,1,5,4,2,1,2,1

5 saiu da pilha, Conteúdo da pilha 3,2,1,3,2,1,5,4,2,1,2,1

O LASJ retorna o 1,2,3,4 Conteúdo da pilha 4,3,2,1,3,2,1,3,2,1,5,4,2,1,2,1

Se você tentar substituir o membro recursivo pela expressão logicamente equivalente (na ausência de duplicatas / NULLs)

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

Isso não é permitido e gera o erro "Referências recursivas não são permitidas em subconsultas". então talvez seja uma supervisão que EXCEPTseja permitida neste caso.

Adição: a Microsoft agora respondeu aos meus comentários do Connect, conforme abaixo

O palpite de Jack está correto: deveria ter sido um erro de sintaxe; referências recursivas não devem, de fato, ser permitidas em EXCEPTcláusulas. Planejamos solucionar esse bug em um próximo release de serviço. Enquanto isso, sugiro evitar referências recursivas nas EXCEPT cláusulas.

Ao restringir a recursão EXCEPT, seguimos o padrão ANSI SQL, que inclui essa restrição desde que a recursão foi introduzida (em 1999, acredito). Não há um consenso generalizado sobre o que a semântica deve ser para recursão EXCEPT(também chamada de "negação não estratificada") em linguagens declarativas como SQL. Além disso, é notoriamente difícil (se não impossível) implementar essa semântica de maneira eficiente (para bancos de dados de tamanho razoável) em um sistema RDBMS.

E parece que a eventual implementação foi feita em 2014 para bancos de dados com nível de compatibilidade de 120 ou superior .

Referências recursivas em uma cláusula EXCEPT geram um erro de conformidade com o padrão ANSI SQL.

Martin Smith
fonte