â 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.

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 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 â 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 :
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); 
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(); 
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âŠ
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.
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
