WITH fournit une façon d'écrire les sous-requêtes pour utilisation dans une requête SELECT plus étendue. Les sous-requêtes, qui sont souvent appelées des expressions communes de table (Common Table Expressions ou CTE), peuvent être considérées comme la déclaration d'une table temporaire n'existant que pour la requête. Une utilisation de cette fonctionnalité est de découper des requêtes complexes en parties plus simples. En voici un exemple :
WITH ventes_regionales AS ( SELECT region, SUM(montant) AS ventes_totales FROM commandes GROUP BY region ), meilleures_regions AS ( SELECT region FROM ventes_regionales WHERE ventes_totales > (SELECT SUM(ventes_totales)/10 FROM ventes_regionales) ) SELECT region, produit, SUM(quantite) AS unites_produit, SUM(montant) AS ventes_produit FROM commandes WHERE region IN (SELECT region FROM meilleures_regions) GROUP BY region, produit;
qui affiche les totaux de ventes par produit dans seulement les régions ayant les meilleures ventes. Cet exemple aurait pu être écrit sans WITH, mais aurait alors nécessité deux niveaux de sous-SELECT imbriqués. Les choses sont un peu plus faciles à suivre de cette façon.
Le modificateur optionnel RECURSIVE fait passer WITH du statut de simple aide syntaxique à celui de quelque chose qu'il serait impossible d'accomplir avec du SQL standard. Grâce à RECURSIVE, une requête WITH peut utiliser sa propre sortie. Un exemple très simple se trouve dans cette requête, qui ajoute les nombres de 1 à 100 :
WITH RECURSIVE t(n) AS ( VALUES (1) UNION ALL SELECT n+1 FROM t WHERE n < 100 ) SELECT sum(n) FROM t;
La forme générale d'une requête WITH est toujours un terme non-recursif, puis UNION (ou UNION ALL), puis un terme récursif. Seul le terme récursif peut contenir une référence à la sortie propre de la requête. Une requête de ce genre est exécutée comme suit :
Procédure 7.1. Évaluation de requête récursive
Évaluer le terme non récursif. Pour UNION (mais pas UNION ALL), supprimer les enregistrements en double. Inclure le reste dans le résultat de la requête récursive et le mettre aussi dans une table temporaire de travail (working table.)
Tant que la table de travail n'est pas vide, répéter ces étapes :
Évaluer le terme récursif, en substituant à la référence récursive le contenu courant de la table de travail. Pour UNION (mais pas UNION ALL), supprimer les doublons, ainsi que les enregistrements en doublon des enregistrements déjà obtenus. Inclure les enregistrements restants dans le résultat de la requête récursive, et les mettre aussi dans une table temporaire intermédiaire (intermediate table).
Remplacer le contenu de la table de travail par celui de la table intermédiaire, puis supprimer la table intermédiaire.
Dans son appellation stricte, ce processus est une itération, pas une récursion, mais RECURSIVE est la terminologie choisie par le comité de standardisation de SQL.
Dans l'exemple précédent, la table de travail a un seul enregistrement à chaque étape, et il prend les valeurs de 1 à 100 en étapes successives. À la centième étape, il n'y a plus de sortie en raison de la clause WHERE, ce qui met fin à la requête.
Les requêtes récursives sont utilisées généralement pour traiter des données hiérarchiques ou sous forme d'arbres. Cette requête est un exemple utile pour trouver toutes les sous-parties directes et indirectes d'un produit, si seule une table donne toutes les inclusions immédiates :
WITH RECURSIVE parties_incluses(sous_partie, partie, quantite) AS ( SELECT sous_partie, partie, quantite FROM parties WHERE partie = 'notre_produit' UNION ALL SELECT p.sous_partie, p.partie, p.quantite FROM parties_incluses pr, parties p WHERE p.partie = pr.sous_partie ) SELECT sous_partie, SUM(quantite) as quantite_totale FROM parties_incluses GROUP BY sous_partie
Quand on travaille avec des requêtes récursives, il est important d'être sûr que la partie récursive de la requête finira par ne retourner aucun enregistrement, au risque sinon de voir la requête boucler indéfiniment. Quelquefois, utiliser UNION à la place de UNION ALL peut résoudre le problème en supprimant les enregistrements qui doublonnent ceux déjà retournés. Toutefois, souvent, un cycle ne met pas en jeu des enregistrements de sortie qui sont totalement des doublons : il peut s'avérer nécessaire de vérifier juste un ou quelques champs, afin de s'assurer que le même point a déjà été atteint précédemment. La méthode standard pour gérer ces situations est de calculer un tableau de valeurs déjà visitées. Par exemple, observez la requête suivante, qui parcourt une table graphe en utilisant un champ lien :
WITH RECURSIVE parcourt_graphe(id, lien, donnee, profondeur) AS ( SELECT g.id, g.lien, g.donnee, 1 FROM graphe g UNION ALL SELECT g.id, g.lien, g.donnee, sg.profondeur + 1 FROM graphe g, parcourt_graphe sg WHERE g.id = sg.lien ) SELECT * FROM parcourt_graphe;
Cette requête va boucler si la liaison lien contient des boucles. Parce que nous avons besoin de la sortie « profondeur », simplement remplacer UNION ALL par UNION ne résoudra pas le problème. À la place, nous avons besoin d'identifier si nous avons atteint un enregistrement que nous avons déjà traité pendant notre parcours des liens. Nous ajoutons deux colonnes chemin et boucle à la requête :
WITH RECURSIVE parcourt_graphe(id, lien, donnee, profondeur, chemin, boucle) AS ( SELECT g.id, g.lien, g.donnee, 1, ARRAY[g.id], false FROM graphe g UNION ALL SELECT g.id, g.lien, g.donnee, sg.profondeur + 1, chemin || g.id, g.id = ANY(chemin) FROM graphe g, parcourt_graphe sg WHERE g.id = sg.lien AND NOT boucle ) SELECT * FROM parcourt_graphe;
En plus de prévenir les boucles, cette valeur de tableau est souvent pratique en elle-même pour représenter le « chemin » pris pour atteindre chaque enregistrement.
De façon plus générale, quand plus d'un champ a besoin d'être vérifié pour identifier une boucle, utilisez un tableau d'enregistrements. Par exemple, si nous avions besoin de comparer les champs f1 et f2 :
WITH RECURSIVE parcourt_graphe(id, lien, donnee, profondeur, chemin, boucle) AS ( SELECT g.id, g.lien, g.donnee, 1, ARRAY[ROW(g.f1, g.f2)], false FROM graphe g UNION ALL SELECT g.id, g.lien, g.donnee, sg.profondeur + 1, chemin || ROW(g.f1, g.f2), ROW(g.f1, g.f2) = ANY(path) FROM graphe g, parcourt_graphe sg WHERE g.id = sg.link AND NOT boucle ) SELECT * FROM parcourt_graphe;
Omettez la syntaxe ROW() dans le cas courant où un seul champ a besoin d'être testé pour déterminer une boucle. Ceci permet, par l'utilisation d'un tableau simple plutôt que d'un tableau de type composite, de gagner en efficacité.
L'algorithme d'évaluation récursive de requête produit sa sortie en ordre de parcours en largeur (algorithme breadth-first). Vous pouvez afficher les résultats en ordre de parcours en profondeur (depth-first) en faisant sur la requête externe un ORDER BY sur une colonne « chemin » construite de cette façon.
Si vous n'êtes pas certain qu'une requête peut boucler, une astuce pratique pour la tester est d'utiliser LIMIT dans la requête parente. Par exemple, cette requête bouclerait indéfiniment sans un LIMIT :
WITH RECURSIVE t(n) AS ( SELECT 1 UNION ALL SELECT n+1 FROM t ) SELECT n FROM t LIMIT 100;
Ceci fonctionne parce que l'implémentation de PostgreSQL™ n'évalue que le nombre d'enregistrements de la requête WITH récupérés par la requête parente. L'utilisation de cette astuce en production est déconseillée parce que d'autres systèmes pourraient fonctionner différemment. Par ailleurs, cela ne fonctionnera pas si vous demandez à la requête externe de trier les résultats de la requête récursive, ou si vous les joignez à une autre table.
Une propriété intéressante des requêtes WITH est qu'elles ne sont évaluées qu'une seule fois par exécution de la requête parente ou des requêtes WITH sœurs. Par conséquent, les calculs coûteux qui sont nécessaires à plusieurs endroits peuvent être placés dans une requête WITH pour éviter le travail redondant. Un autre intérêt peut être d'éviter l'exécution multiple d'une fonction ayant des effets de bord. Toutefois, le revers de la médaille est que l'optimiseur est moins capable d'extrapoler les restrictions de la requête parente vers une requête WITH que vers une sous-requête classique. La requête WITH sera généralement exécutée telle quelle, sans suppression d'enregistrements, que la requête parente devra supprimer ensuite. (Mais, comme mentionné précédemment, l'évaluation pourrait s'arrêter rapidement si la (les) référence(s) à la requête ne demande(nt) qu'un nombre limité d'enregistrements).