Antipattern di PostgreSQL: «L'infinito non è un limite!», o un po' di ricorsione

Ricorsione — un meccanismo molto potente e comodo, se si eseguono le stesse azioni "in profondità" su dati correlati. Ma la ricorsione incontrollata è il male, che può portare a un'esecuzione infinita del processo, o (cosa che succede più spesso) a una «consunzione» di tutta la memoria disponibile.

Antipattern di PostgreSQL: «L'infinito non è un limite!», o un po' di ricorsione
I DBMS funzionano su basi simili — "hanno detto di scavare, e io scavo". La tua query può non solo rallentare i processi vicini, occupando continuamente le risorse della CPU, ma anche «far crollare» l'intero database, «mangiando» tutta la memoria disponibile. Pertanto, la protezione contro la ricorsione infinita è un obbligo dello sviluppatore stesso.

In PostgreSQL, la possibilità di utilizzare query ricorsive tramite WITH RECURSIVE è disponibile da tempi immemori con la versione 8.4, ma ancora oggi è possibile incontrare regolarmente query potenzialmente vulnerabili e "indifese". Come liberarsi da problemi di questo tipo?

Non scrivere query ricorsive

Ma scrivere query non ricorsive. Cordialmente, Il tuo C.O.

In realtà, PostgreSQL offre un ampio repertorio di funzionalità da sfruttare per non applicare la ricorsione.

Adottare un approccio totalmente differente al problema

A volte è possibile osservare il problema "da un altro punto di vista". Un esempio di tale situazione è stato presentato nell'articolo "SQL HowTo: 1000 e un modo di aggregare" — moltiplicare un insieme di numeri senza utilizzare funzioni di aggregazione personalizzate:

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 -- selezione del risultato finale
  i DESC
LIMIT 1;

Questa query può essere sostituita con una variante degli esperti di matematica:

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

Utilizzare generate_series invece dei cicli

Supponiamo di voler generare tutti i possibili prefissi per la stringa '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;

È davvero necessaria la ricorsione qui?.. Se utilizziamo LATERAL e generate_series, nemmeno il CTE sarà necessario:

SELEZIONA
  substr(str, 1, ln) str
DA
  (VALORI('abcdefgh')) T(str)
, LATERALE(
    SELEZIONA generate_series(length(str), 1, -1) ln
  ) X;

Modificare la struttura del database

Ad esempio, hai una tabella di messaggi del forum con collegamenti su chi ha risposto a chi o un thread in un social network:

CREA TABELLA message(
  message_id
    uuid
      CHIAVE PRIMARIA
, reply_to
    uuid
      RIFERIMENTI message
, body
    text
);
CREA INDICE SU message(reply_to);

Antipattern di PostgreSQL: «L'infinito non è un limite!», o un po' di ricorsione
E il tipo di query per caricare tutti i messaggi su un argomento appare più o meno così:

CON RICORSO T COME (
  SELEZIONA
    *
  DA
    message
  DOVE
    message_id = $1
UNIONE TUTTI
  SELEZIONA
    m.*
  DA
    T
  UNISCI
    message m
      SU m.reply_to = T.message_id
)
TABella T;

Ma dato che abbiamo sempre bisogno di tutto il tema dal messaggio radice, perché non aggiungere il suo identificatore in ogni registrazione automaticamente?

-- aggiungiamo un campo con l'identificatore comune del tema e un indice su di esso
ALTERA TABELLA message
  AGGIUNGI COLONNA theme_id uuid;
CREA INDICE SU message(theme_id);

-- inizializziamo l'identificatore del tema nel trigger all'inserimento
CREA O SOSTITUISCI FUNZIONE ins() RESTITUISCE TRIGGER AS $$
INIZIO
  NEW.theme_id = CASO
    QUANDO NEW.reply_to È NULL ALLORA NEW.message_id -- prendiamo dall'evento di partenza
    ALTRO ( -- o dal messaggio a cui stiamo rispondendo
      SELEZIONA
        theme_id
      DA
        message
      DOVE
        message_id = NEW.reply_to
    )
  FINE;
  RESTITUISCI NEW;
FINE;
$$ LINGUAGGIO plpgsql;

CREA TRIGGER ins PRIMA DI INSERIRE
  SU message
    PER OGNI RIGA
      ESEGUI PROCEDURA ins();

Antipattern di PostgreSQL: «L'infinito non è un limite!», o un po' di ricorsione
Ora, la nostra richiesta ricorsiva può essere ridotta a qualcosa di simile a questo:

SELECT
  *
FROM
  message
WHERE
  theme_id = $1;

Utilizzare 'delimiter' applicativi

Se non possiamo cambiare la struttura del database per qualche motivo, vediamo su cosa possiamo fare affidamento affinché anche la presenza di errori nei dati non porti a un'esecuzione ricorsiva infinita.

Contatore di 'profondità' della ricorsione

Aumentiamo semplicemente il contatore di uno ad ogni passo della ricorsione fino a raggiungere il limite che consideriamo evidentemente inadeguato:

WITH RECURSIVE T AS (
  SELECT
    0 i
  ...
UNION ALL
  SELECT
    i + 1
  ...
  WHERE
    T.i < 64 -- limite
)

Pro: In caso di tentativo di ciclo, comunque non eseguiremo più del limite di iterazioni "in profondità" specificato.
Contro: Non c'è garanzia che non elaboreremo nuovamente la stessa registrazione — ad esempio, a profondità 15 e 25, e poi ogni +10. E nessuno ha promesso nulla riguardo "in larghezza".

Formalmente, questa ricorsione non sarà infinita, ma se ad ogni passo il numero di registrazioni aumenta esponenzialmente, sappiamo tutti come finisce...

Antipattern di PostgreSQL: «L'infinito non è un limite!», o un po' di ricorsionevedi 'Il problema dei granelli sulla scacchiera'

Custode del 'percorso'

Aggiungiamo uno dopo l'altro tutti gli identificativi degli oggetti che incontriamo lungo il percorso della ricorsione in un array, che rappresenta l'unico "percorso" fino a esso:

WITH RECURSIVE T AS (
  SELECT
    ARRAY[id] path
  ...
UNION ALL
  SELECT
    path || id
  ...
  WHERE
    id  ALL(T.path) -- non coincide con nessuno di
)

Pro: In presenza di un ciclo nei dati, sicuramente non elaboreremo di nuovo la stessa registrazione all'interno di un singolo percorso.
Contro: Tuttavia, possiamo comunque scorrere, letteralmente, tutte le registrazioni senza ripeterci.

Antipattern di PostgreSQL: «L'infinito non è un limite!», o un po' di ricorsionevedi "Problema del cavallo"

Limitazione della lunghezza del percorso

Per evitare la situazione di "smarrimento" della ricorsione a una profondità sconosciuta, possiamo combinare i due metodi precedenti. Oppure, se non vogliamo mantenere campi non necessari, possiamo integrare la condizione di continuazione della ricorsione con una valutazione della lunghezza del percorso:

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
)

Scegli il metodo che preferisci!

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster