Vindo para SQL de outras linguagens de programação, a estrutura de uma consulta recursiva parece bastante estranha. Ande por ela passo a passo, e ela parece desmoronar.
Considere o seguinte exemplo simples:
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;
Vamos percorrê-lo.
Primeiro, o membro âncora é executado e o conjunto de resultados é colocado em R. Assim, R é inicializado como {3, 5, 7}.
Então, a execução cai abaixo de UNION ALL e o membro recursivo é executado pela primeira vez. Ele é executado em R (ou seja, no R que temos atualmente em mãos: {3, 5, 7}). Isso resulta em {9, 25, 49}.
O que ele faz com esse novo resultado? Ele anexa {9, 25, 49} ao {3, 5, 7} existente, rotula a união resultante R e então continua com a recursão a partir daí? Ou ele redefine R para ser apenas este novo resultado {9, 25, 49} e faz toda a união depois?
Nenhuma das escolhas faz sentido.
Se R agora for {3, 5, 7, 9, 25, 49} e executarmos a próxima iteração da recursão, terminaremos com {9, 25, 49, 81, 625, 2401} e teremos perdeu {3, 5, 7}.
Se R agora é apenas {9, 25, 49}, então temos um problema de rotulagem incorreta. R é entendido como a união do conjunto de resultados do membro âncora e todos os conjuntos de resultados do membro recursivo subsequentes. Considerando que {9, 25, 49} é apenas um componente de R. Não é o R completo que acumulamos até agora. Portanto, escrever o membro recursivo selecionando de R não faz sentido.
Eu certamente aprecio o que @Max Vernon e @Michael S. detalharam abaixo. Ou seja, que (1) todos os componentes são criados até o limite de recursão ou conjunto nulo, e então (2) todos os componentes são unidos. É assim que entendo a recursão SQL para realmente funcionar.
Se estivéssemos redesenhando o SQL, talvez pudéssemos impor uma sintaxe mais clara e explícita, algo assim:
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;
Mais ou menos como uma prova indutiva em matemática.
O problema com a recursão SQL como está atualmente é que ela é escrita de maneira confusa. A forma como está escrito diz que cada componente é formado selecionando de R, mas isso não significa o R completo que foi (ou parece ter sido) construído até agora. Significa apenas o componente anterior.
A descrição BOL de CTEs recursivos descreve a semântica da execução recursiva da seguinte forma:
Portanto, cada nível tem como entrada apenas o nível acima e não todo o conjunto de resultados acumulado até o momento.
O acima é como ele funciona logicamente . Atualmente, as CTEs recursivas fisicamente são sempre implementadas com loops aninhados e um spool de pilha no SQL Server. Isso é descrito aqui e aqui e significa que, na prática, cada elemento recursivo está apenas trabalhando com a linha pai do nível anterior, não com o nível inteiro. Mas as várias restrições à sintaxe permitida em CTEs recursivas significam que essa abordagem funciona.
Se você remover o
ORDER BY
da sua consulta, os resultados serão ordenados da seguinte maneiraIsso ocorre porque o plano de execução funciona de maneira muito semelhante ao seguinte
C#
NB1: Como acima, no momento em que o primeiro filho do membro âncora
3
está sendo processado, todas as informações sobre seus irmãos,5
e7
, e seus descendentes, já foram descartadas do spool e não estão mais acessíveis.NB2: O C# acima tem a mesma semântica geral do plano de execução, mas o fluxo no plano de execução não é idêntico, pois os operadores trabalham em uma forma de execução em pipeline. Este é um exemplo simplificado para demonstrar a essência da abordagem. Consulte os links anteriores para obter mais detalhes sobre o plano em si.
NB3: O próprio spool de pilha é aparentemente implementado como um índice clusterizado não exclusivo com coluna de chave de nível de recursão e exclusivos adicionados conforme necessário ( source )
Este é apenas um palpite (semi) educado e provavelmente está completamente errado. Pergunta interessante, diga-se de passagem.
T-SQL é uma linguagem declarativa; talvez uma CTE recursiva seja traduzida em uma operação estilo cursor onde os resultados do lado esquerdo de UNION ALL são anexados em uma tabela temporária, então o lado direito de UNION ALL é aplicado aos valores no lado esquerdo.
Então, primeiro inserimos a saída do lado esquerdo do UNION ALL no conjunto de resultados, depois inserimos os resultados do lado direito do UNION ALL aplicado ao lado esquerdo e inserimos isso no conjunto de resultados. O lado esquerdo é então substituído pela saída do lado direito e o lado direito é aplicado novamente ao "novo" lado esquerdo. Algo assim:
Você pode ver esse comportamento no plano de execução do CTE recursivo:
Este é o passo 1 acima, onde o lado esquerdo de UNION ALL é adicionado à saída:
Este é o lado direito de UNION ALL onde a saída é concatenada ao conjunto de resultados:
A documentação do SQL Server , que menciona T i e T i+1 , não é muito compreensível nem uma descrição precisa da implementação real.
A ideia básica é que a parte recursiva da consulta analise todos os resultados anteriores, mas apenas uma vez .
Pode ser útil ver como outros bancos de dados implementam isso (para obter o mesmo resultado). A documentação do Postgres diz:
A documentação do SQLite sugere uma implementação ligeiramente diferente, e este algoritmo de uma linha por vez pode ser o mais fácil de entender:
Meu conhecimento é especificamente em DB2, mas olhando os diagramas de explicação parece ser o mesmo com o SQL Server.
O plano vem daqui:
Veja em Colar o Plano
O otimizador não executa literalmente uma união de todos para cada consulta recursiva. Ele pega a estrutura da consulta e atribui a primeira parte da união all a um "membro âncora" então ele percorrerá a segunda metade da união all (chamada de "membro recursivo" recursivamente até atingir as limitações definidas. a recursão está completa, o otimizador une todos os registros.
O otimizador apenas toma como sugestão fazer uma operação pré-definida.