¿Cómo funciona realmente la recursividad SQL?


19

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.


"Si R es ahora {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 nosotros ' he perdido {3, 5, 7} ". No veo cómo pierdes {3,5,7} si funciona así.
ypercubeᵀᴹ

@ yper-crazyhat-cubeᵀᴹ - Estaba siguiendo la primera hipótesis que propuse, a saber, ¿qué pasa si la R intermedia es una acumulación de todo lo que se había calculado hasta ese momento? Luego, en la siguiente iteración del miembro recursivo, cada elemento de R se eleva al cuadrado. Por lo tanto, {3, 5, 7} se convierte en {9, 25, 49} y nunca más tenemos {3, 5, 7} en R. En otras palabras, {3, 5, 7} se pierde de R.
UnLogicGuys

Respuestas:


26

La descripción de BOL de los CTE recursivos describe la semántica de la ejecución recursiva de la siguiente manera:

  1. Divida la expresión CTE en miembros ancla y recursivos.
  2. Ejecute los miembros de anclaje que crean la primera invocación o el conjunto de resultados base (T0).
  3. Ejecute los miembros recursivos con Ti como entrada y Ti + 1 como salida.
  4. Repita el paso 3 hasta que se devuelva un conjunto vacío.
  5. Devuelve el conjunto de resultados. Esta es una UNIÓN TODO de T0 a Tn.

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 BYde su consulta, los resultados se ordenan de la siguiente manera

+---------+
|    N    |
+---------+
|       3 |
|       5 |
|       7 |
|      49 |
|    2401 |
| 5764801 |
|      25 |
|     625 |
|  390625 |
|       9 |
|      81 |
|    6561 |
+---------+

Esto se debe a que el plan de ejecución funciona de manera muy similar a lo siguiente C#

using System;
using System.Collections.Generic;
using System.Diagnostics;

public class Program
{
    private static readonly Stack<dynamic> StackSpool = new Stack<dynamic>();

    private static void Main(string[] args)
    {
        //temp table #NUMS
        var nums = new[] { 3, 5, 7 };

        //Anchor member
        foreach (var number in nums)
            AddToStackSpoolAndEmit(number, 0);

        //Recursive part
        ProcessStackSpool();

        Console.WriteLine("Finished");
        Console.ReadLine();
    }

    private static void AddToStackSpoolAndEmit(long number, int recursionLevel)
    {
        StackSpool.Push(new { N = number, RecursionLevel = recursionLevel });
        Console.WriteLine(number);
    }

    private static void ProcessStackSpool()
    {
        //recursion base case
        if (StackSpool.Count == 0)
            return;

        var row = StackSpool.Pop();

        int thisLevel = row.RecursionLevel + 1;
        long thisN = row.N * row.N;

        Debug.Assert(thisLevel <= 100, "max recursion level exceeded");

        if (thisN < 10000000)
            AddToStackSpoolAndEmit(thisN, thisLevel);

        ProcessStackSpool();
    }
}

NB1: Igual que el anterior para cuando el primer hijo del elemento de anclaje 3se está procesando toda la información sobre sus hermanos, 5y 7, 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 )


66
Las consultas recursivas en SQL Server siempre se convierten de recursividad a iteración (con apilamiento) durante el análisis. La regla de implementación para la iteración es IterateToDepthFirst- Iterate(seed,rcsv)->PhysIterate(seed,rcsv). Solo para tu información. Excelente respuesta
Paul White dice GoFundMonica

Por cierto, UNION también está permitido en lugar de UNION ALL pero SQL Server no lo hará.
Joshua

5

Esto 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:

  1. {3,5,7} -> conjunto de resultados
  2. declaraciones recursivas aplicadas a {3,5,7}, que es {9,25,49}. {9,25,49} se agrega al conjunto de resultados y reemplaza el lado izquierdo de UNION ALL.
  3. declaraciones recursivas aplicadas a {9,25,49}, que es {81,625,2401}. {81,625,2401} se agrega al conjunto de resultados y reemplaza el lado izquierdo de UNION ALL.
  4. declaraciones recursivas aplicadas a {81,625,2401}, que es {6561,390625,5764801}. Se agrega {6561,390625,5764801} al conjunto de resultados.
  5. El cursor está completo, ya que la siguiente iteración da como resultado que la cláusula WHERE devuelva false.

Puede ver este comportamiento en el plan de ejecución para el CTE recursivo:

ingrese la descripción de la imagen aquí

Este es el paso 1 anterior, donde el lado izquierdo de UNION ALL se agrega a la salida:

ingrese la descripción de la imagen aquí

Este es el lado derecho de UNION ALL donde la salida se concatena con el conjunto de resultados:

ingrese la descripción de la imagen aquí


4

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:

Evaluación de consultas recursivas

  1. Evaluar el término no recursivo. Para UNION(pero no UNION ALL), descarte filas duplicadas. Incluya todas las filas restantes en el resultado de la consulta recursiva y también colóquelas en una tabla de trabajo temporal .
  2. Mientras la mesa de trabajo no esté vacía, repita estos pasos:
    1. Evalúe el término recursivo, sustituyendo el contenido actual de la tabla de trabajo por la auto-referencia recursiva. Para UNION(pero no UNION ALL), descarte filas duplicadas y filas que duplican cualquier fila de resultados anterior. Incluya todas las filas restantes en el resultado de la consulta recursiva, y también colóquelas en una tabla intermedia temporal .
    2. Reemplace el contenido de la tabla de trabajo con el contenido de la tabla intermedia, luego vacíe la tabla intermedia.

Nota
Estrictamente hablando, este proceso es iteración, no recursividad, pero RECURSIVEes la terminología elegida por el comité de estándares SQL.

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:

El algoritmo básico para calcular el contenido de la tabla recursiva es el siguiente:

  1. Ejecute initial-selecty agregue los resultados a una cola.
  2. Mientras la cola no está vacía:
    1. Extraiga una sola fila de la cola.
    2. Inserte esa fila individual en la tabla recursiva
    3. Suponga que la fila única que se acaba de extraer es la única fila en la tabla recursiva y ejecute recursive-select, agregando todos los resultados a la cola.

El procedimiento básico anterior puede modificarse mediante las siguientes reglas adicionales:

  • Si un operador UNION conecta el initial-selectcon el recursive-select, entonces solo agregue filas a la cola si no se ha agregado previamente una fila idéntica a la cola. Las filas repetidas se descartan antes de agregarse a la cola, incluso si las filas repetidas ya se han extraído de la cola mediante el paso de recursión. Si el operador es UNION ALL, entonces todas las filas generadas tanto por el initial-selecty el recursive-selectsiempre se agregan a la cola, incluso si son repeticiones.
    [...]

0

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

Plan de SQL Server Explain

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.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.