Al llegar a SQL desde otros lenguajes de programación, la estructura de una consulta recursiva parece bastante extraña. Camine a través de él paso a paso, y parece desmoronarse.
Considere el siguiente ejemplo sencillo:
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 a atravesarlo.
Primero, el miembro ancla se ejecuta y el conjunto de resultados se coloca en R. Por lo tanto, R se inicializa en {3, 5, 7}.
Luego, la ejecución cae por debajo de UNION ALL y el miembro recursivo se ejecuta por primera vez. Se ejecuta en R (es decir, en la R que actualmente tenemos en la mano: {3, 5, 7}). Esto da como resultado {9, 25, 49}.
¿Qué hace con este nuevo resultado? ¿Anexa {9, 25, 49} a los {3, 5, 7} existentes, etiqueta la unión R resultante y luego continúa con la recursividad desde allí? ¿O redefine R para que sea solo este nuevo resultado {9, 25, 49} y haga toda la unión más tarde?
Ninguna elección tiene sentido.
Si R ahora es {3, 5, 7, 9, 25, 49} y ejecutamos la próxima iteración de la recursión, entonces terminaremos con {9, 25, 49, 81, 625, 2401} y hemos perdido {3, 5, 7}.
Si R ahora es solo {9, 25, 49}, entonces tenemos un problema de etiquetado incorrecto. Se entiende que R es la unión del conjunto de resultados del miembro ancla y todos los conjuntos de resultados del miembro recursivo subsiguientes. Mientras que {9, 25, 49} es solo un componente de R. No es la R completa que hemos acumulado hasta ahora. Por lo tanto, escribir el miembro recursivo como selección de R no tiene sentido.
Ciertamente aprecio lo que @Max Vernon y @Michael S. han detallado a continuación. Es decir, que (1) todos los componentes se crean hasta el límite de recursión o conjunto nulo, y luego (2) todos los componentes se unen entre sí. Así es como entiendo que la recursividad de SQL realmente funcione.
Si estuviéramos rediseñando SQL, tal vez impondríamos una sintaxis más clara y explícita, algo como esto:
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;
Algo así como una prueba inductiva en matemáticas.
El problema con la recursividad de SQL tal como está actualmente es que está escrito de manera confusa. La forma en que está escrito dice que cada componente se forma seleccionando desde R, pero no significa la R completa que ha sido (o parece haber sido) construida hasta ahora. Solo significa el componente anterior.
fuente
Respuestas:
La descripción de BOL de los CTE recursivos describe la semántica de la ejecución recursiva de la siguiente manera:
Por lo tanto, cada nivel solo tiene como entrada el nivel superior, no el conjunto de resultados completo acumulado hasta ahora.
Lo anterior es cómo funciona lógicamente . Los CTE físicamente recursivos actualmente siempre se implementan con bucles anidados y una cola de pila en SQL Server. Esto se describe aquí y aquí y significa que, en la práctica, cada elemento recursivo solo funciona con la fila principal del nivel anterior, no con el nivel completo. Pero las diversas restricciones sobre la sintaxis permitida en los CTE recursivos significan que este enfoque funciona.
Si elimina el
ORDER BY
de su consulta, los resultados se ordenan de la siguiente maneraEsto se debe a que el plan de ejecución funciona de manera muy similar a lo siguiente
C#
NB1: Igual que el anterior para cuando el primer hijo del elemento de anclaje
3
se está procesando toda la información sobre sus hermanos,5
y7
, y sus descendientes, que ya ha sido descartada desde el carrete y ya no es accesible.NB2: El C # anterior tiene la misma semántica general que el plan de ejecución, pero el flujo en el plan de ejecución no es idéntico, ya que allí los operadores trabajan en forma de ejecución canalizada. Este es un ejemplo simplificado para demostrar la esencia del enfoque. Consulte los enlaces anteriores para obtener más detalles sobre el plan en sí.
NB3: el spool de pila en sí mismo aparentemente se implementa como un índice agrupado no único con una columna clave de nivel de recursión y unificadores agregados según sea necesario ( fuente )
fuente
IterateToDepthFirst
-Iterate(seed,rcsv)->PhysIterate(seed,rcsv)
. Solo para tu información. Excelente respuestaEsto es solo una suposición (semi) educada, y probablemente esté completamente equivocado. Pregunta interesante, por cierto.
T-SQL es un lenguaje declarativo; quizás un CTE recursivo se traduzca en una operación de estilo de cursor donde los resultados del lado izquierdo de UNION ALL se agreguen a una tabla temporal, luego el lado derecho de UNION ALL se aplique a los valores en el lado izquierdo.
Entonces, primero insertamos la salida del lado izquierdo de UNION ALL en el conjunto de resultados, luego insertamos los resultados del lado derecho de UNION ALL aplicados al lado izquierdo, e insertamos eso en el conjunto de resultados. El lado izquierdo luego se reemplaza con la salida del lado derecho, y el lado derecho se aplica nuevamente al lado izquierdo "nuevo". Algo como esto:
Puede ver este comportamiento en el plan de ejecución para el CTE recursivo:
Este es el paso 1 anterior, donde el lado izquierdo de UNION ALL se agrega a la salida:
Este es el lado derecho de UNION ALL donde la salida se concatena con el conjunto de resultados:
fuente
La documentación de SQL Server , que menciona T i y T i + 1 , no es muy comprensible ni una descripción precisa de la implementación real.
La idea básica es que la parte recursiva de la consulta mira todos los resultados anteriores, pero solo una vez .
Puede ser útil observar cómo otras bases de datos implementan esto (para obtener el mismo resultado). La documentación de Postgres dice:
La documentación de SQLite sugiere una implementación ligeramente diferente, y este algoritmo de una fila a la vez podría ser el más fácil de entender:
fuente
Mi conocimiento está específicamente en DB2, pero mirar los diagramas de explicación parece ser lo mismo con SQL Server.
El plan viene de aquí:
Véalo en Pegar el plan
El optimizador no ejecuta literalmente una unión para cada consulta recursiva. Toma la estructura de la consulta y asigna la primera parte de la unión a un "miembro de anclaje", luego se ejecutará a través de la segunda mitad de la unión (llamado "miembro recursivo" recursivamente hasta que alcance las limitaciones definidas. Después la recursión se completa, el optimizador une todos los registros.
El optimizador solo lo toma como una sugerencia para realizar una operación predefinida.
fuente