PostgreSQLLa base de données la plus sophistiquée au monde.
Documentation PostgreSQL 16.10 » Langage SQL » RequĂȘtes » RequĂȘtes WITH (Common Table Expressions)

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

WITH fournit un moyen d'Ă©crire des ordres auxiliaires pour les utiliser dans des requĂȘtes plus importantes. Ces requĂȘtes, qui sont souvent appelĂ©es Common Table Expressions ou CTE, peuvent ĂȘtre vues comme des tables temporaires qui n'existent que pour une requĂȘte. Chaque ordre auxiliaire dans une clause WITH peut ĂȘtre un SELECT, INSERT, UPDATE, ou DELETE ; et la clause WITH elle-mĂȘme est attachĂ©e Ă  un ordre primaire qui peut lui aussi ĂȘtre un SELECT, INSERT, UPDATE, DELETE ou MERGE.

7.8.1. SELECT dans WITH #

L'intĂ©rĂȘt de SELECT dans WITH est de diviser des requĂȘtes complexes en parties plus simples. Un exemple est:

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 seulement dans les rĂ©gions ayant les meilleures ventes. La clause WITH dĂ©finit deux ordres auxiliaires appelĂ©s ventes_regionales et meilleures_regions, oĂč la sortie de ventes_regionales est utilisĂ©e dans meilleures_regions et la sortie de meilleures_regions est utilisĂ©e dans la requĂȘte SELECT primaire. 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.

7.8.2. RequĂȘtes rĂ©cursives #

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 rĂ©cursif, 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 :

É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

Alors que RECURSIVE autorise que les requĂȘtes soient spĂ©cifiĂ©es rĂ©cursivement, en interne, ce type de requĂȘtes est Ă©valuĂ© itĂ©rativement.

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 * pr.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

7.8.2.2. DĂ©tection de cycles #

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 de nouveau 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, 0
        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 is_cycle et path Ă  la requĂȘte :

WITH RECURSIVE search_graph(id, link, data, depth, is_cycle, path) AS (
    SELECT g.id, g.link, g.data, 0,
      false,
      ARRAY[g.id]
    FROM graph g
  UNION ALL
    SELECT g.id, g.link, g.data, sg.depth + 1,
      g.id = ANY(path),
      path || g.id
    FROM graph g, search_graph sg
    WHERE g.id = sg.link AND NOT is_cycle
)
SELECT * FROM search_graph;
      

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 search_graph(id, link, data, depth, is_cycle, path) AS (
    SELECT g.id, g.link, g.data, 0,
      false,
      ARRAY[ROW(g.f1, g.f2)]
    FROM graph g
  UNION ALL
    SELECT g.id, g.link, g.data, sg.depth + 1,
      ROW(g.f1, g.f2) = ANY(path),
      path || ROW(g.f1, g.f2)
    FROM graph g, search_graph sg
    WHERE g.id = sg.link AND NOT is_cycle
)
SELECT * FROM search_graph;
      

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

Il existe une syntaxe interne pour simplifier la dĂ©tection de cycles. La requĂȘte ci-dessus peut aussi ĂȘtre Ă©crite ainsi :

WITH RECURSIVE search_graph(id, link, data, depth) AS (
    SELECT g.id, g.link, g.data, 1
    FROM graph g
  UNION ALL
    SELECT g.id, g.link, g.data, sg.depth + 1
    FROM graph g, search_graph sg
    WHERE g.id = sg.link
) CYCLE id SET is_cycle USING path
SELECT * FROM search_graph;

et elle sera réécrite en interne sous le forme ci-dessus. La clause CYCLE indique tout d'abord la liste des colonnes à tracer pour une détection de cycle, puis le nom de la colonne qui indiquera si un cycle a été détecté, et enfin le nom d'une autre colonne qui tracera le chemin. Les colonnes cycle et chemin seront automatiquement ajoutées aux lignes en sortie de la CTE.

Astuce

La colonne du chemindu cycle est calculĂ©e de la mĂȘme façon que l'affiche la colonne de tri depth-first dans la section prĂ©cĂ©dente. Une requĂȘte peut avoir Ă  la fois une clause SEARCH et une clause CYCLE, mais une spĂ©cification de recherche depth-first et une spĂ©cification de recherche de cyle vont crĂ©er des calculs redondants, donc il est plus efficace d'utiliser juste la clause CYCLE et trier par la colonne du chemin. Si un tri breadth-first est voulu, alors indiquer les deux, SEARCH and CYCLE, peut ĂȘtre utile.

Si vous n'ĂȘtes pas certain qu'une requĂȘte puisse 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, parce dans ces cas, la requĂȘte extĂ©rieure essaiera habituellement de rĂ©cupĂ©rer toute la sortie de la requĂȘte WITH de toute façon.

7.8.3. MatĂ©rialisation des CTE #

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. NĂ©anmoins, le revers de la mĂ©daille est que l'optimiseur n'est pas en mesure de faire descendre les restrictions de la requĂȘte parent dans une requĂȘte WITH Ă  rĂ©fĂ©rences multiples, car cela pourrait affecter toutes les utilisations de la sortie de la requĂȘte WITH alors que cela ne devrait en affecter qu'une seule. 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).

NĂ©anmoins, si une requĂȘte WITH est non rĂ©cursive et qu'elle est libre de tout effet de bord (autrement dit un SELECT ne contenant aucune fonction volatile), alors elle peut ĂȘtre intĂ©grĂ©e dans la requĂȘte parent, permettant ainsi une optimisation de la jointure sur les deux niveaux de la requĂȘte. Par dĂ©faut, ceci survient si la requĂȘte parente fait rĂ©fĂ©rence une seule fois Ă  la requĂȘte WITH mais si elle y fait rĂ©fĂ©rence plusieurs fois. Vous pouvez surcharger cette dĂ©cision en indiquant MATERIALIZED pour forcer un calcul sĂ©parĂ© de la requĂȘte WITH ou en spĂ©cifiant NOT MATERIALIZED pour la forcer pour ĂȘtre intĂ©grĂ©e dans la requĂȘte parente. Ce dernier choix risque de dupliquer des calculs sur la requĂȘte WITH, mais cela peut apporter un gain net si chaque utilisation de la requĂȘte WITH ne nĂ©cessite qu'une petite partie de la sortie complĂšte de la requĂȘte WITH.

Un exemple simple de ces rĂšgles est le suivant :

WITH w AS (
    SELECT * FROM big_table
)
SELECT * FROM w WHERE key = 123;
      

Cette requĂȘte WITH va ĂȘtre intĂ©grĂ©e, produisant le mĂȘme plan d'exĂ©cution que :

SELECT * FROM big_table WHERE key = 123;
      

EN particulier, s'il existe un index sur key, il sera probablement utilisé pour récupérer les lignes pour lesquelles key = 123. D'un autre cÎté, dans

WITH w AS (
    SELECT * FROM big_table
)
SELECT * FROM w AS w1 JOIN w AS w2 ON w1.key = w2.ref
WHERE w2.key = 123;
      

la requĂȘte WITH sera matĂ©rialisĂ©e, produisant une copie temporaire de big_table qui est ensuite jointe avec elle-mĂȘme -- sans intĂ©rĂȘt pour un index. Cette requĂȘte sera exĂ©cutĂ©e bien plus efficacement s'il est Ă©crite ainsi :

WITH w AS NOT MATERIALIZED (
    SELECT * FROM big_table
)
SELECT * FROM w AS w1 JOIN w AS w2 ON w1.key = w2.ref
WHERE w2.key = 123;
      

pour que les restrictions de la requĂȘte parent puissent ĂȘtre appliquĂ©es directement aux parcours de big_table.

Voici un exemple oĂč NOT MATERIALIZED pourrait ĂȘtre indĂ©sirable :

WITH w AS (
    SELECT key, very_expensive_function(val) as f FROM some_table
)
SELECT * FROM w AS w1 JOIN w AS w2 ON w1.f = w2.f;
      

Ici, la matĂ©rialisation de la requĂȘte WITH assure que la very_expensive_function est Ă©valuĂ©e uniquement une fois par ligne de table, et non pas deux fois.

Les exemples prĂ©cĂ©dents ne montrent que des cas d'utilisation de WITH avec SELECT, mais on peut les attacher de la mĂȘme façon Ă  un INSERT, UPDATE, DELETE ou MERGE. Dans chaque cas, le mĂ©canisme fournit en fait des tables temporaires auxquelles on peut faire rĂ©fĂ©rence dans la commande principale.

7.8.4. Ordres de modification de donnĂ©es avec WITH #

Vous pouvez utiliser des ordres de modification de donnĂ©es (INSERT, UPDATE ou DELETE, mais pas MERGE) dans WITH. Cela vous permet d'effectuer plusieurs opĂ©rations diffĂ©rentes dans la mĂȘme requĂȘte. Par exemple:

WITH lignes_deplacees AS (
    DELETE FROM produits
    WHERE
        "date" >= '2010-10-01' AND
        "date" < '2010-11-01'
    RETURNING *
)
INSERT INTO log_produits
SELECT * FROM lignes_deplacees;
      

Cette requĂȘte dĂ©place les enregistrements de produits vers log_produits. Le DELETE du WITH supprime les enregistrements spĂ©cifiĂ©s de produits, en retournant leurs contenus par la clause RETURNING; puis la requĂȘte primaire lit cette sortie et l'insĂšre dans log_produits.

Un point important à noter de l'exemple précédent est que la clause WITH est attachée à l'INSERT, pas au sous-SELECT de l' INSERT. C'est nécessaire parce que les ordres de modification de données ne sont autorisés que dans les clauses WITH qui sont attachées à l'ordre de plus haut niveau. Toutefois, les rÚgles de visibilité normales de WITH s'appliquent, il est donc possible de faire référence à la sortie du WITH dans le sous-SELECT.

Les ordres de modification de donnĂ©es dans WITH ont habituellement des clauses RETURNING (voir Section 6.4), comme dans l'exemple prĂ©cĂ©dent. C'est la sortie de la clause RETURNING, pas la table cible de l'ordre de modification de donnĂ©es, qui forme la table temporaire Ă  laquelle on pourra faire rĂ©fĂ©rence dans le reste de la requĂȘte. Si un ordre de modification de donnĂ©es dans WITH n'a pas de clause RETURNING, alors il ne produit pas de table temporaire et ne peut pas ĂȘtre utilisĂ© dans le reste de la requĂȘte. Un ordre de ce type sera toutefois exĂ©cutĂ©. En voici un exemple (dĂ©nuĂ© d'intĂ©rĂȘt) :

WITH t AS (
    DELETE FROM foo
)
DELETE FROM bar;
      

Cet exemple supprimerait tous les éléments des tables foo et bar. Le nombre d'enregistrements retourné au client n'inclurait que les enregistrements supprimés de bar.

Les autoréférences récursives dans les ordres de modification de données ne sont pas autorisées. Dans certains cas, il est possible de contourner cette limitation en faisant référence à la sortie d'un WITH, par exemple:

WITH RECURSIVE pieces_incluses(sous_piece, piece) AS (
    SELECT sous_piece, piece FROM pieces WHERE piece = 'notre_produit'
  UNION ALL
    SELECT p.sous_piece, p.piece
    FROM pieces_incluses pr, pieces p
    WHERE p.piece = pr.sous_piece
  )
DELETE FROM pieces
  WHERE piece IN (SELECT piece FROM pieces_incluses);
      

Cette requĂȘte supprimerait toutes les piĂšces directes et indirectes d'un produit.

Les ordres de modification de donnĂ©es dans WITH sont exĂ©cutĂ©s exactement une fois, et toujours jusqu'Ă  la fin, indĂ©pendamment du fait que la requĂȘte primaire lise tout (ou mĂȘme une partie) de leur sortie. Notez que c'est diffĂ©rent de la rĂšgle pour SELECT dans WITH: comme prĂ©cisĂ© dans la section prĂ©cĂ©dente, l'exĂ©cution d'un SELECT n'est poursuivie que tant que la requĂȘte primaire consomme sa sortie.

Les sous-requĂȘtes du WITH sont toutes exĂ©cutĂ©es simultanĂ©ment et simultanĂ©ment avec la requĂȘte principale. Par consĂ©quent, quand vous utilisez un ordre de modification de donnĂ©es avec WITH, l'ordre dans lequel les mises Ă  jour sont effectuĂ©es n'est pas prĂ©visible. Toutes les requĂȘtes sont exĂ©cutĂ©es dans le mĂȘme instantanĂ© (voyez Chapitre 13), elles ne peuvent donc pas voir les effets des autres sur les tables cibles. Ceci rend sans importance le problĂšme de l'imprĂ©visibilitĂ© de l'ordre des mises Ă  jour, et signifie que RETURNING est la seule façon de communiquer les modifications entre les diffĂ©rentes sous-requĂȘtes WITH et la requĂȘte principale. En voici un exemple :

WITH t AS (
    UPDATE produits SET prix = prix * 1.05
    RETURNING *
)
SELECT * FROM produits;
      

Le SELECT externe retournerait les prix originaux avant l'action de UPDATE, alors qu'avec :

WITH t AS (
    UPDATE produits SET prix = prix * 1.05
    RETURNING *
)
SELECT * FROM t;
      

le SELECT externe retournerait les données mises à jour.

Essayer de mettre Ă  jour le mĂȘme enregistrement deux fois dans le mĂȘme ordre n'est pas supportĂ©. Seule une des deux modifications a lieu, mais il n'est pas aisĂ© (et quelquefois impossible) de dĂ©terminer laquelle. Ceci s'applique aussi pour la suppression d'un enregistrement qui a dĂ©jĂ  Ă©tĂ© mis Ă  jour dans le mĂȘme ordre : seule la mise Ă  jour est effectuĂ©e. Par consĂ©quent, vous devriez Ă©viter en rĂšgle gĂ©nĂ©rale de mettre Ă  jour le mĂȘme enregistrement deux fois en un seul ordre. En particulier, Ă©vitez d'Ă©crire des sous-requĂȘtes qui modifieraient les mĂȘmes enregistrements que la requĂȘte principale ou une autre sous-requĂȘte. Les effets d'un ordre de ce type seraient imprĂ©visibles.

À l'heure actuelle, les tables utilisĂ©es comme cibles d'un ordre modifiant les donnĂ©es dans un WITH ne doivent avoir ni rĂšgle conditionnelle, ni rĂšgle ALSO, ni une rĂšgle INSTEAD qui gĂ©nĂšre plusieurs ordres.