Antipatrones de PostgreSQL: "¡La infinitud no es el límite!", o Un poco sobre la recursión

Recursión — es un mecanismo muy potente y conveniente, si se realizan las mismas acciones "en profundidad" sobre datos relacionados. Pero la recursión incontrolada es un mal que puede llevar a una ejecución infinita del proceso, o (lo que sucede con más frecuencia) a «consumir» toda la memoria disponible.

Antipatrones de PostgreSQL: "¡La infinitud no es el límite!", o Un poco sobre la recursión
Las bases de datos funcionan en este sentido de acuerdo con los mismos principios — "dijeron que cavara, y yo estoy cavando". Su consulta puede no solo ralentizar los procesos vecinos, ocupando constantemente los recursos del procesador, sino también «caer» toda la base por completo, «consumiendo» toda la memoria disponible. Por lo tanto, la protección contra la recursión infinita es responsabilidad del propio desarrollador.

En PostgreSQL la posibilidad de utilizar consultas recursivas a través de WITH RECURSIVE apareció hace mucho tiempo en la versión 8.4, pero aún se pueden encontrar regularmente consultas "desprotegidas" potencialmente vulnerables. ¿Cómo liberarse de problemas de este tipo?

No escribir consultas recursivas

Sino escribir consultas no recursivas. Atentamente, su K.O.

De hecho, PostgreSQL proporciona una cantidad bastante grande de funcionalidad que se puede utilizar para no aplicar recursión.

Utilizar un enfoque fundamentalmente diferente para el problema

A veces, se puede simplemente mirar el problema "desde otro ángulo". Un ejemplo de tal situación lo mencioné en el artículo «SQL HowTo: 1000 y un modo de agregación» — multiplicación de un conjunto de números sin utilizar funciones de agregación personalizadas:

WITH RECURSIVE src AS (
  SELECT '{2,3,5,7,11,13,17,19}'::integer[] arr
)
, T(i, val) AS (
  SELECT
    1::bigint
  , 1
UNION ALL
  SELECT
    i + 1
  , val * arr[i]
  FROM
    T
  , src
  WHERE
    i <= array_length(arr, 1)
)
SELECT
  val
FROM
  T
ORDER BY -- selección del resultado final
  i DESC
LIMIT 1;

Dicha consulta se puede reemplazar por una opción de matemáticos expertos:

WITH src AS (
  SELECT unnest('{2,3,5,7,11,13,17,19}'::integer[]) prime
)
SELECT
  exp(sum(ln(prime)))::integer val
FROM
  src;

Utilizar generate_series en lugar de ciclos

Supongamos que tenemos el problema de generar todos los posibles prefijos para la cadena 'abcdefgh':

WITH RECURSIVE T AS (
  SELECT 'abcdefgh' str
UNION ALL
  SELECT
    substr(str, 1, length(str) - 1)
  FROM
    T
  WHERE
    length(str) > 1
)
TABLE T;

¿Realmente se necesita recursión aquí?.. Si se utiliza LATERAL y generate_series, entonces ni siquiera se necesitarán CTE:

SELECT
  substr(str, 1, ln) str
FROM
  (VALUES('abcdefgh')) T(str)
, LATERAL(
    SELECT generate_series(length(str), 1, -1) ln
  ) X;

Modificar la estructura de la base de datos

Por ejemplo, tiene una tabla de mensajes de foro con relaciones de quién respondió a quién o hilo en una red social:

CREAR TABLA message(
  message_id
    uuid
      CLAVE PRIMARIA
, reply_to
    uuid
      REFERENCIAS message
, body
    texto
);
CREAR ÍNDICE EN message(reply_to);

Antipatrones de PostgreSQL: "¡La infinitud no es el límite!", o Un poco sobre la recursión
Un ejemplo típico de consulta para cargar todos los mensajes sobre un mismo tema se vería así:

CON RECURSIVO T COMO (
  SELECCIONAR
    *
  DE
    message
  DONDE
    message_id = $1
UNIÓN TODO
  SELECCIONAR
    m.*
  DE
    T
  UNIR
    message m
      EN m.reply_to = T.message_id
)
TABLA T;

Pero dado que siempre necesitamos todo el tema desde el mensaje raíz, ¿por qué no agregar su identificador en cada registro automáticamente?

-- añadamos un campo con el identificador general del tema y un índice sobre él
ALTERAR TABLA message
  AÑADIR COLUMNA theme_id uuid;
CREAR ÍNDICE EN message(theme_id);

-- inicializamos el identificador del tema en un activador al insertar
CREAR O REEMPLAZAR FUNCIÓN ins() RETORNA DISPARADOR AS $$
COMIENZO
  NUEVO.theme_id = CASO
    CUANDO NUEVO.reply_to ES NULO ENTONCES NUEVO.message_id -- tomamos del evento inicial
    SINO ( -- o del mensaje al que estamos respondiendo
      SELECCIONAR
        theme_id
      DE
        message
      DONDE
        message_id = NUEVO.reply_to
    )
  FIN;
  RETORNAR NUEVO;
FIN;
$$ LENGUAJE plpgsql;

CREAR DISPARADOR ins ANTES DE INSERTAR
  EN message
    POR CADA FILA
      EJECUTAR PROCEDIMIENTO ins();

Antipatrones de PostgreSQL: "¡La infinitud no es el límite!", o Un poco sobre la recursión
Ahora nuestra consulta recursiva puede reducirse a solo esto:

SELECCIONAR
  *
DE
  message
DONDE
  theme_id = $1;

Utilizar "limitadores" aplicados

Si no podemos cambiar la estructura de la base de datos por alguna razón, veamos en qué podemos apoyarnos, para que incluso la presencia de un error en los datos no conduzca a una ejecución recursiva infinita.

Contador de "profundidad" recursiva

Simplemente aumentamos el contador en uno en cada paso de la recursión hasta alcanzar un límite que consideramos evidentemente inadecuado:

CON RECURSIVO T COMO (
  SELECCIONAR
    0 i
  ...
UNIÓN TODO
  SELECCIONAR
    i + 1
  ...
  DONDE
    T.i < 64 -- límite
)

Pro: Al intentar un ciclo, no haremos más de lo indicado como límite de iteraciones "hacia adentro".
Contra: No hay garantía de que no procesemos el mismo registro nuevamente, por ejemplo, en profundidad 15 y 25, y así sucesivamente cada +10. Además, no hay ninguna promesa sobre "hacia afuera".

Formalmente, esta recursión no será infinita, pero si en cada paso el número de registros aumenta exponencialmente, todos sabemos cómo termina esto...

Antipatrones de PostgreSQL: "¡La infinitud no es el límite!", o Un poco sobre la recursiónver "El problema de los granos en el tablero de ajedrez"

Guardador del "camino"

Por orden, vamos anotando todos los identificadores que encontramos en nuestro camino de recursión en un arreglo, que es un "camino" único hasta él:

CON RECURSIVO T COMO (
  SELECCIONAR
    ARRAY[id] path
  ...
UNIÓN TODO
  SELECCIONAR
    path || id
  ...
  DONDE
    id  TODOS(T.path) -- no coincide con ninguno de
)

Pro: Si hay un ciclo en los datos, no procesaremos absolutamente la misma entrada de nuevo en el mismo camino.
Contra: Pero podemos recorrer, literalmente, todas las entradas sin repetirnos.

Antipatrones de PostgreSQL: "¡La infinitud no es el límite!", o Un poco sobre la recursiónver 'El problema del caballo'

Restricción de la longitud del camino

Para evitar la situación de 'errante' en una profundidad de recursión incierta, podemos combinar los dos métodos anteriores. O, si no queremos mantener campos adicionales, complementar la condición de continuación de la recursión con la evaluación de la longitud del camino:

WITH RECURSIVE T AS (
  SELECT
    ARRAY[id] path
  ...
UNION ALL
  SELECT
    path || id
  ...
  WHERE
    id  ALL(T.path) AND
    array_length(T.path, 1) < 10
)

¡Elija el método que prefiera!

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster