PostgreSQLLa base de données la plus sophistiquée au monde.

7.8. RequĂȘtes WITH (Common Table Expressions)

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

  1. É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.)

  2. Tant que la table de travail n'est pas vide, rĂ©pĂ©ter ces Ă©tapes :

    1. É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).

    2. Remplacer le contenu de la table de travail par celui de la table intermédiaire, puis supprimer la table intermédiaire.

[Note]

Note

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;
[Astuce]

Astuce

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Ă©.

[Astuce]

Astuce

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