Antipatterns PostgreSQL : « L'infini n'est pas une limite ! », ou un peu sur la récursivité

RĂ©cursion — un mĂ©canisme trĂšs puissant et pratique, si les mĂȘmes opĂ©rations sont effectuĂ©es «en profondeur» sur des donnĂ©es liĂ©es. Mais la rĂ©cursion incontrĂŽlĂ©e est malĂ©fique, car elle peut mener soit Ă  une exĂ©cution infinie du processus, soit (ce qui arrive plus souvent) Ă  «l'Ă©puisement» de toute la mĂ©moire disponible.

Antipatterns PostgreSQL : « L'infini n'est pas une limite ! », ou un peu sur la récursivité
Les SGBD fonctionnent selon les mĂȘmes principes — "ils m'ont dit de creuser, je creuse". Votre requĂȘte peut non seulement ralentir les processus voisins en occupant constamment les ressources du processeur, mais aussi «faire tomber» toute la base de donnĂ©es, en «avalant» toute la mĂ©moire disponible. Par consĂ©quent, la protection contre la rĂ©cursion infinie est la responsabilitĂ© du dĂ©veloppeur lui-mĂȘme.

Avec PostgreSQL, la possibilitĂ© d'utiliser des requĂȘtes rĂ©cursives via WITH RECURSIVE est apparue Ă  des temps immĂ©moriaux depuis la version 8.4, mais il est encore courant de rencontrer des requĂȘtes «sans dĂ©fense» potentiellement vulnĂ©rables. Comment se prĂ©munir contre ce genre de problĂšmes ?

Ne pas Ă©crire de requĂȘtes rĂ©cursives

Mais Ă©crire des requĂȘtes non rĂ©cursives. Cordialement, Votre C.O.

En rĂ©alitĂ©, PostgreSQL offre une quantitĂ© suffisante de fonctionnalitĂ©s qui peuvent ĂȘtre exploitĂ©es pour ne appliquer la rĂ©cursion.

Utiliser une approche fondamentalement différente du problÚme

Parfois, il suffit de regarder le problĂšme «sous un autre angle». Un exemple de cette situation a Ă©tĂ© abordĂ© dans l'article «SQL HowTo: 1000 et un moyen d'agrĂ©gation» — la multiplication d'un ensemble de nombres sans utiliser de fonctions d'agrĂ©gation personnalisĂ©es :

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 -- sélection du résultat final
  i DESC
LIMIT 1;

Une telle requĂȘte peut ĂȘtre remplacĂ©e par celle des connaisseurs en mathĂ©matiques :

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

Utiliser generate_series au lieu de boucles

Supposons que nous devons générer tous les préfixes possibles pour la chaßne '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;

Est-ce vraiment nĂ©cessaire d'utiliser la rĂ©cursion ici ? Si nous utilisons LATERAL et generate_series, alors mĂȘme pas besoin de CTE :

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

Changer la structure de la BDD

Par exemple, vous avez une table de messages de forum avec des relations sur qui a répondu à qui ou un fil dans un réseau social:

CRÉER TABLE message(
  message_id
    uuid
      CLÉ PRIMAIRE
, reply_to
    uuid
      RÉFÉRENCES message
, body
    text
);
CRÉER INDEX SUR message(reply_to);

Antipatterns PostgreSQL : « L'infini n'est pas une limite ! », ou un peu sur la récursivité
Voici un exemple de requĂȘte type pour charger tous les messages sur un mĂȘme sujet :

AVEC RÉCURSIF T COMME (
  SÉLECTIONNER
    *
  DE
    message
  OÙ
    message_id = $1
UNION TOUT
  SÉLECTIONNER
    m.*
  DE
    T
  REJOINDRE
    message m
      ON m.reply_to = T.message_id
)
TABLE T;

Étant donnĂ© que nous avons toujours besoin de tous les messages associĂ©s au message racine, pourquoi ne pas ajouter son identifiant Ă  chaque enregistrement automatiquement ?

-- ajoutons un champ pour l'identifiant commun du sujet et un index dessus
ALTER TABLE message
  AJOUTER COLONNE theme_id uuid;
CRÉER INDEX SUR message(theme_id);

-- initialiser l'identifiant du sujet dans le déclencheur lors de l'insertion
CRÉER OU REMPLACER FONCTION ins() RENVOIE TRIGGER AS $$
DÉBUT
  NOUVEAU.theme_id = CAS
    QUAND NOUVEAU.reply_to EST NULL ALORS NOUVEAU.message_id -- prenons depuis l'événement de départ
    SINON ( -- ou depuis le message auquel nous répondons
      SÉLECTIONNER
        theme_id
      DE
        message
      OÙ
        message_id = NOUVEAU.reply_to
    )
  FIN;
  RETOURNER NOUVEAU;
FIN;
$$ LANGUE plpgsql;

CRÉER DÉCLENCHEUR ins AVANT INSERTION
  SUR message
    POUR CHAQUE LIGNE
      EXÉCUTER PROCÉDURE ins();

Antipatterns PostgreSQL : « L'infini n'est pas une limite ! », ou un peu sur la récursivité
Notre requĂȘte rĂ©cursive se rĂ©duit donc Ă  ceci :

SÉLECTIONNER
  *
DE
  message
OÙ
  theme_id = $1;

Utiliser des « limites » applicatives

Si nous ne sommes pas en mesure de modifier la structure de la base de données pour diverses raisons, examinons ce sur quoi nous pouvons nous appuyer pour éviter qu'une erreur dans les données n'entraßne une exécution récursive infinie.

Compteur de « profondeur » de récursion

Il suffit d'augmenter le compteur de un à chaque étape de la récursion jusqu'à atteindre une limite que nous considérons manifestement inappropriée :

AVEC RÉCURSIF T COMME (
  SÉLECTIONNER
    0 i
  ...
UNION TOUT
  SÉLECTIONNER
    i + 1
  ...
  OÙ
    T.i < 64 -- limite
)

Pour : En cas de boucle infinie, nous ne ferons pas plus que la limite d'itérations « en profondeur » spécifiée.
Contre : Il n'y a aucune garantie que nous ne traiterons pas plusieurs fois le mĂȘme enregistrement — par exemple, Ă  la profondeur 15 et 25, et ainsi de suite chaque +10. De plus, personne ne garantit quoi que ce soit pour le « large ».

Formellement, cette récursion ne sera pas infinie, mais si à chaque étape, le nombre d'enregistrements augmente exponentiellement, nous savons tous comment cela finit


Antipatterns PostgreSQL : « L'infini n'est pas une limite ! », ou un peu sur la récursivitévoir le « ProblÚme des grains sur l'échiquier »

Gardien du « chemin »

Nous enregistrons successivement tous les identifiants d'objets rencontrés sur notre chemin de récursion dans un tableau qui représente le « chemin » unique jusqu'à celui-ci :

AVEC RÉCURSIF T COMME (
  SÉLECTIONNER
    ARRAY[id] chemin
  ...
UNION TOUT
  SÉLECTIONNER
    chemin || id
  ...
  OÙ
    id  TOUS(T.chemin) -- ne correspond Ă  aucun de
)

Pour : S'il y a un cycle dans les donnĂ©es, nous ne traiterons absolument pas la mĂȘme entrĂ©e plusieurs fois dans le mĂȘme chemin.
Contre : Mais en mĂȘme temps, nous pouvons parcourir, littĂ©ralement, toutes les entrĂ©es sans jamais nous rĂ©pĂ©ter.

Antipatterns PostgreSQL : « L'infini n'est pas une limite ! », ou un peu sur la récursivitévoir « Le problÚme du cavalier »

Limite de la longueur du chemin

Pour éviter la situation de « errance » de la récursion à une profondeur indéterminée, nous pouvons combiner les deux méthodes précédentes. Ou, si nous ne souhaitons pas conserver des champs supplémentaires, compléter la condition de continuation de la récursion par une évaluation de la longueur du chemin :

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
)

Choisissez la méthode qui vous plaßt !

Source : habr.com

Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS đŸ”„ Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster