Por que a consulta a seguir retorna linhas infinitas? Eu esperava que a EXCEPT
clá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.
A descrição BOL de CTEs recursivos descreve a semântica da execução recursiva da seguinte forma:
Observe que o acima é uma descrição lógica . A ordem física das operações pode ser um pouco diferente conforme ilustrado aqui
Aplicando isso ao seu CTE, eu esperaria um loop infinito com o seguinte padrão
Porque
é a expressão Anchor. Isso retorna claramente
1,2,3
comoT0
Depois disso, a expressão recursiva é executada
Com
1,2,3
a entrada que produzirá uma saída de4,5
as,T1
então, reconectando-a para a próxima rodada de recursão, retornará1,2,3
e assim por diante indefinidamente.Isso não é o que realmente acontece no entanto. Estes são os resultados das primeiras 5 invocações
Ao usar
OPTION (MAXRECURSION 1)
e ajustar para cima em incrementos1
, pode-se ver que ele entra em um ciclo em que cada nível sucessivo alternará continuamente entre saída1,2,3,4
e1,2,3,5
.Conforme discutido por @Quassnoi nesta postagem do blog . O padrão de resultados observados é como se cada invocação estivesse fazendo
(1),(2),(3),(4),(5) EXCEPT (X)
ondeX
está a última linha da invocação anterior.Editar: Depois de ler a excelente resposta do SQL Kiwi , fica claro por que isso ocorre e que essa não é toda a história, pois ainda há muitas coisas na pilha que nunca podem ser processadas.
Se você tentar substituir o membro recursivo pela expressão logicamente equivalente (na ausência de duplicatas/NULLs)
Isso não é permitido e gera o erro "Referências recursivas não são permitidas em subconsultas". então talvez seja um descuido que
EXCEPT
é permitido neste caso.Adição: a Microsoft agora respondeu ao meu Connect Feedback conforme abaixo
E parece que a eventual implementação foi feita em 2014 para bancos de dados com nível de compatibilidade de 120 ou superior .
Consulte a resposta de Martin Smith para obter informações sobre o status atual de
EXCEPT
um CTE recursivo.Para explicar o que você estava vendo e por quê:
Estou usando uma variável de tabela aqui, para tornar mais clara a distinção entre os valores âncora e o item recursivo (não altera a semântica).
O plano de consulta é:
A execução começa na raiz do plano (o SELECT) e o controle passa pela árvore para o Index Spool, Concatenation e, em seguida, para o Table Scan de nível superior.
A primeira linha da varredura passa pela árvore e é (a) armazenada no Stack Spool e (b) retornada ao cliente. Qual linha é a primeira não está definida, mas vamos supor que seja a linha com o valor {1}, por causa do argumento. A primeira linha a aparecer é, portanto, {1}.
O controle passa novamente para o Table Scan (o operador Concatenation consome todas as linhas de sua entrada mais externa antes de abrir a próxima). A varredura emite a segunda linha (valor {2}), e isso novamente passa pela árvore a ser armazenada na pilha e enviada ao cliente. O cliente agora recebeu a sequência {1}, {2}.
Adotando uma convenção em que o topo da pilha LIFO está à esquerda, a pilha agora contém {2, 1}. À medida que o controle passa novamente para o Table Scan, ele não relata mais linhas e o controle passa de volta para o operador Concatenation, que abre sua segunda entrada (precisa de uma linha para passar para o spool da pilha) e o controle passa para o Inner Join pela primeira vez.
A junção interna chama o Table Spool em sua entrada externa, que lê a linha superior da pilha {2} e a exclui da tabela de trabalho. A pilha agora contém {1}.
Tendo recebido uma linha em sua entrada externa, o Inner Join passa o controle de sua entrada interna para o Left Anti-Semi Join (LASJ). Isso solicita uma linha de sua entrada externa, passando o controle para o Sort. Sort é um iterador de bloqueio, então ele lê todas as linhas da variável de tabela e as classifica em ordem crescente (como acontece).
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}, então {1} é emitido, pois os valores não correspondem.
Essa linha {1} sobe na á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, de volta para o lado interno (retornou uma linha da última vez, pode retornar), para baixo através da Inner Join, para o LASJ. Ele lê sua entrada interna novamente, obtendo o valor {2} do Sort.
O membro recursivo ainda é {2}, então desta vez o LASJ encontra {2} e {2}, resultando em nenhuma linha sendo emitida. Não encontrando mais linhas em sua entrada interna (o Sort agora está sem linhas), o controle passa de volta para o Inner Join.
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 invocação do Table Scan and Sort passando no teste LASJ e sendo adicionado à pilha e passado para o cliente, que agora recebeu {1}, {2}, {1}, {2}...e voltamos.
Minha explicação favorita sobre o spool Stack usado em planos CTE recursivos é a de Craig Freedman.