En primer lugar, intentemos simplificar y aclarar la descripción del algoritmo que se proporciona en la página del manual. Para simplificarlo, considere solo union all
en with recursive
cláusula por ahora (y union
más tarde):
WITH RECURSIVE pseudo-entity-name(column-names) AS (
Initial-SELECT
UNION ALL
Recursive-SELECT using pseudo-entity-name
)
Outer-SELECT using pseudo-entity-name
Para aclararlo, describamos el proceso de ejecución de consultas en pseudocódigo:
working-recordset = result of Initial-SELECT
append working-recordset to empty outer-recordset
while( working-recordset is not empty ) begin
new working-recordset = result of Recursive-SELECT
taking previous working-recordset as pseudo-entity-name
append working-recordset to outer-recordset
end
overall-result = result of Outer-SELECT
taking outer-recordset as pseudo-entity-name
O incluso más corto:el motor de la base de datos ejecuta la selección inicial, tomando sus filas de resultados como conjunto de trabajo. Luego, ejecuta repetidamente la selección recursiva en el conjunto de trabajo, cada vez que reemplaza el contenido del conjunto de trabajo con el resultado de la consulta obtenido. Este proceso finaliza cuando la selección recursiva devuelve un conjunto vacío. Y todas las filas de resultados proporcionadas primero por la selección inicial y luego por la selección recursiva se recopilan y se envían a la selección externa, cuyo resultado se convierte en el resultado general de la consulta.
Esta consulta está calculando factorial de 3:
WITH RECURSIVE factorial(F,n) AS (
SELECT 1 F, 3 n
UNION ALL
SELECT F*n F, n-1 n from factorial where n>1
)
SELECT F from factorial where n=1
Selección inicial SELECT 1 F, 3 n
nos da valores iniciales:3 para el argumento y 1 para el valor de la función.
Selección recursiva SELECT F*n F, n-1 n from factorial where n>1
establece que cada vez que necesitamos multiplicar el valor de la última función por el valor del último argumento y disminuir el valor del argumento.
El motor de la base de datos lo ejecuta así:
En primer lugar, ejecuta initail select, lo que proporciona el estado inicial del conjunto de registros de trabajo:
F | n
--+--
1 | 3
Luego transforma el conjunto de registros de trabajo con consulta recursiva y obtiene su segundo estado:
F | n
--+--
3 | 2
Luego tercer estado:
F | n
--+--
6 | 1
En el tercer estado no hay ninguna fila que siga a n>1
condición en selección recursiva, por lo que el conjunto de trabajo es salidas de bucle.
El conjunto de registros externo ahora contiene todas las filas, devueltas por selección inicial y recursiva:
F | n
--+--
1 | 3
3 | 2
6 | 1
La selección externa filtra todos los resultados intermedios del conjunto de registros externo y muestra solo el valor factorial final que se convierte en el resultado general de la consulta:
F
--
6
Y ahora consideremos la tabla forest(id,parent_id,name)
:
id | parent_id | name
---+-----------+-----------------
1 | | item 1
2 | 1 | subitem 1.1
3 | 1 | subitem 1.2
4 | 1 | subitem 1.3
5 | 3 | subsubitem 1.2.1
6 | | item 2
7 | 6 | subitem 2.1
8 | | item 3
'Expandiendo el árbol completo ' aquí significa ordenar los elementos del árbol en orden de profundidad legible por humanos mientras se calculan sus niveles y (tal vez) rutas. Ambas tareas (de ordenación correcta y nivel de cálculo o ruta) no se pueden resolver en uno (o incluso en un número constante de) SELECT sin usar la cláusula WITH RECURSIVE (o la cláusula Oracle CONNECT BY, que no es compatible con PostgreSQL). Pero esta consulta recursiva hace el trabajo (bueno, casi lo hace, vea la nota a continuación):
WITH RECURSIVE fulltree(id,parent_id,level,name,path) AS (
SELECT id, parent_id, 1 as level, name, name||'' as path from forest where parent_id is null
UNION ALL
SELECT t.id, t.parent_id, ft.level+1 as level, t.name, ft.path||' / '||t.name as path
from forest t, fulltree ft where t.parent_id = ft.id
)
SELECT * from fulltree order by path
El motor de base de datos lo ejecuta así:
En primer lugar, ejecuta initail select, que proporciona todos los elementos de nivel más alto (raíces) de forest
tabla:
id | parent_id | level | name | path
---+-----------+-------+------------------+----------------------------------------
1 | | 1 | item 1 | item 1
8 | | 1 | item 3 | item 3
6 | | 1 | item 2 | item 2
Luego, ejecuta una selección recursiva, que proporciona todos los elementos de segundo nivel de forest
tabla:
id | parent_id | level | name | path
---+-----------+-------+------------------+----------------------------------------
2 | 1 | 2 | subitem 1.1 | item 1 / subitem 1.1
3 | 1 | 2 | subitem 1.2 | item 1 / subitem 1.2
4 | 1 | 2 | subitem 1.3 | item 1 / subitem 1.3
7 | 6 | 2 | subitem 2.1 | item 2 / subitem 2.1
Luego, ejecuta la selección recursiva nuevamente, recuperando elementos de nivel 3D:
id | parent_id | level | name | path
---+-----------+-------+------------------+----------------------------------------
5 | 3 | 3 | subsubitem 1.2.1 | item 1 / subitem 1.2 / subsubitem 1.2.1
Y ahora ejecuta la selección recursiva nuevamente, tratando de recuperar elementos de 4to nivel, pero no hay ninguno, por lo que el ciclo sale.
El SELECT externo establece el orden de fila legible por humanos correcto, ordenando en la columna de ruta:
id | parent_id | level | name | path
---+-----------+-------+------------------+----------------------------------------
1 | | 1 | item 1 | item 1
2 | 1 | 2 | subitem 1.1 | item 1 / subitem 1.1
3 | 1 | 2 | subitem 1.2 | item 1 / subitem 1.2
5 | 3 | 3 | subsubitem 1.2.1 | item 1 / subitem 1.2 / subsubitem 1.2.1
4 | 1 | 2 | subitem 1.3 | item 1 / subitem 1.3
6 | | 1 | item 2 | item 2
7 | 6 | 2 | subitem 2.1 | item 2 / subitem 2.1
8 | | 1 | item 3 | item 3
NOTA :El orden de las filas resultante seguirá siendo correcto solo mientras no haya caracteres de puntuación que precedan al /
en los nombres de los artículos. Si renombramos Item 2
en Item 1 *
, romperá el orden de las filas, colocándose entre Item 1
y sus descendientes.
Una solución más estable es usar el carácter de tabulación (E'\t'
) como separador de ruta en la consulta (que se puede sustituir por un separador de ruta más legible más adelante:en la selección externa, antes de mostrarse a humanos, etc.). Las rutas separadas por tabulaciones mantendrán el orden correcto hasta que haya tabulaciones o caracteres de control en los nombres de los elementos, que se pueden verificar y descartar fácilmente sin pérdida de usabilidad.
Es muy simple modificar la última consulta para expandir cualquier subárbol arbitrario; solo necesita sustituir la condición parent_id is null
con perent_id=1
(por ejemplo). Tenga en cuenta que esta variante de consulta devolverá todos los niveles y rutas en relación con Item 1
.
Y ahora sobre errores típicos . El error típico más notable específico de las consultas recursivas es definir condiciones de detención incorrectas en la selección recursiva, lo que da como resultado un bucle infinito.
Por ejemplo, si omitimos where n>1
condición en la muestra factorial anterior, la ejecución de la selección recursiva nunca dará un conjunto vacío (porque no tenemos ninguna condición para filtrar una sola fila) y el bucle continuará infinitamente.
Esa es la razón más probable por la que algunas de sus consultas se bloquean (la otra razón no específica pero aún posible es una selección muy ineficaz, que se ejecuta en un tiempo finito pero muy largo).
No hay muchas directrices de consulta específicas de RECURSIVE por mencionar, que yo sepa. Pero me gustaría sugerir (bastante obvio) un procedimiento de creación de consultas recursivas paso a paso.
-
Cree y depure por separado su selección inicial.
-
Envuélvalo con scaffolding CON construcción RECURSIVA
y comience a construir y depurar su selección recursiva.
La construcción de refuerzo recomendada es así:
WITH RECURSIVE rec( <Your column names> ) AS (
<Your ready and working initial SELECT>
UNION ALL
<Recursive SELECT that you are debugging now>
)
SELECT * from rec limit 1000
Esta selección externa más simple generará el conjunto de registros externo completo, que, como sabemos, contiene todas las filas de salida de la selección inicial y cada ejecución de la selección recurriva en un bucle en su orden de salida original, ¡al igual que en los ejemplos anteriores! El limit 1000
parte evitará que se cuelgue, reemplazándola con una salida de gran tamaño en la que podrá ver el punto de parada perdido.
- Después de depurar la selección inicial y recursiva, construya y depure su selección externa.
Y ahora lo último que mencionar:la diferencia en el uso de union
en lugar de union all
en with recursive
cláusula. Introduce una restricción de unicidad de fila que da como resultado dos líneas adicionales en nuestro pseudocódigo de ejecución:
working-recordset = result of Initial-SELECT
discard duplicate rows from working-recordset /*union-specific*/
append working-recordset to empty outer-recordset
while( working-recordset is not empty ) begin
new working-recordset = result of Recursive-SELECT
taking previous working-recordset as pseudo-entity-name
discard duplicate rows and rows that have duplicates in outer-recordset
from working-recordset /*union-specific*/
append working-recordset to outer-recordset
end
overall-result = result of Outer-SELECT
taking outer-recordset as pseudo-entity-name