La fonction PostgreSQL n'est pas exécutée lorsqu'elle est appelée depuis l'intérieur de CTE

14

Espérant simplement confirmer mon observation et obtenir une explication de la raison pour laquelle cela se produit.

J'ai une fonction définie comme:

CREATE OR REPLACE FUNCTION "public"."__post_users_id_coin" ("coins" integer, "userid" integer) RETURNS TABLE (id integer) AS '
UPDATE
users
SET
coin = coin + coins
WHERE
userid = users.id
RETURNING
users.id' LANGUAGE "sql" COST 100 ROWS 1000
VOLATILE
RETURNS NULL ON NULL INPUT
SECURITY INVOKER

Lorsque j'appelle cette fonction à partir d'un CTE, elle exécute la commande SQL mais ne déclenche pas la fonction, par exemple:

WITH test AS
(SELECT * FROM __post_users_id_coin(10, 1))

SELECT
1 -- Select 1 but update not performed

D'un autre côté, si j'appelle la fonction à partir d'un CTE et que je sélectionne ensuite le résultat du CTE (ou que j'appelle la fonction directement sans CTE), il exécute la commande SQL et déclenche la fonction, par exemple:

WITH test AS
(SELECT * FROM __post_users_id_coin(10, 1))

SELECT
*
FROM
test -- Select result and update performed

ou

SELECT * FROM __post_users_id_coin(10,1)

Étant donné que je ne me soucie pas vraiment du résultat de la fonction (j'en ai juste besoin pour effectuer la mise à jour), existe-t-il un moyen de faire fonctionner cela sans sélectionner le résultat du CTE?

Andy
la source

Réponses:

11

C'est une sorte de comportement attendu. Les CTE sont matérialisés mais il y a une exception.

Si un CTE n'est pas référencé dans la requête parent, il n'est pas matérialisé du tout. Vous pouvez essayer ceci par exemple et cela fonctionnera bien:

WITH not_executed AS (SELECT 1/0),
     executed AS (SELECT 1)
SELECT * FROM executed ;

Code copié à partir d'un commentaire dans le blog de Craig Ringer:
les CTE de PostgreSQL sont des barrières d'optimisation .


Avant d'essayer ceci et plusieurs requêtes similaires, je pensais que l'exception était: "lorsqu'un CTE n'est pas référencé dans la requête parent ou dans un autre CTE et ne se référence pas lui-même un autre CTE". Donc, si vous vouliez que le CTE soit exécuté mais que les résultats ne soient pas affichés dans le résultat de la requête, je pensais que ce serait une solution de contournement (en le référençant dans un autre CTE).

Mais hélas, cela ne fonctionne pas comme je m'y attendais:

WITH test AS
    (SELECT * FROM __post_users_id_coin(10, 1)),
  execute_test AS 
    (TABLE test)
SELECT 1 ;     -- no, it doesn't do the update

et par conséquent, ma "règle d'exception" n'est pas correcte. Lorsqu'un CTE est référencé par un autre CTE et qu'aucun d'entre eux n'est référencé par la requête parent, la situation est plus compliquée et je ne sais pas exactement ce qui se passe et quand les CTE sont matérialisés. Je ne trouve pas non plus de référence pour de tels cas dans la documentation.


Je ne vois pas de meilleure solution que d'utiliser ce que vous avez déjà suggéré:

SELECT * FROM __post_users_id_coin(10, 1) ;

ou:

WITH test AS
    (SELECT * FROM __post_users_id_coin(10, 1))
SELECT *
FROM test ;

Si la fonction met à jour plusieurs lignes et que vous obtenez plusieurs lignes (avec 1) dans le résultat, vous pouvez agréger pour obtenir une seule ligne:

SELECT MAX(1) AS result FROM __post_users_id_coin(10, 1) ;

mais je préférerais que les résultats de la fonction qui effectue une mise à jour SELECT *soient retournés, avec comme exemple, donc tout ce qui appelle cette requête sait s'il y a eu des mises à jour et quelles ont été les modifications dans la table.

ypercubeᵀᴹ
la source
Continuons cette discussion dans le chat .
ypercubeᵀᴹ
4

Il s'agit d'un comportement attendu et documenté.

Tom Lane l'explique ici.

Documenté dans le manuel ici:

Les instructions de modification des données dans WITHsont exécutées exactement une fois, et toujours jusqu'à la fin , indépendamment du fait que la requête principale lit tout (ou même n'importe lequel) de leur sortie. Notez que ceci est différent de la règle pour SELECTdans WITH: comme indiqué dans la section précédente, l'exécution de a SELECTn'est effectuée que dans la mesure où la requête principale demande sa sortie .

Accentuation sur moi. "Modification des données" sont INSERT, UPDATEet les DELETErequêtes. (Par opposition à SELECT.). Le manuel une fois de plus:

Vous pouvez utiliser des instructions de modification de données ( INSERT, UPDATEou DELETE) dans WITH.

Fonction appropriée

CREATE OR REPLACE FUNCTION public.__post_users_id_coin (_coins integer, _userid integer)
  RETURNS TABLE (id integer) AS
$func$
UPDATE users u
SET    coin = u.coin + _coins  -- see below
WHERE  u.id = _userid
RETURNING u.id
$func$ LANGUAGE sql COST 100 ROWS 1000 STRICT;

J'ai supprimé les clauses par défaut (bruit) et STRICTest le synonyme court deRETURNS NULL ON NULL INPUT .

Assurez-vous que les noms de paramètres n'entrent pas en conflit avec les noms de colonnes. J'ai ajouté avant _, mais c'est juste ma préférence personnelle.

Si coinpossible, NULLje suggère:

SET    coin = CASE WHEN coin IS NULL THEN _coins ELSE coin + _coins END

Si users.idest la clé primaire, ni , RETURNS TABLEni ROWs 1000aucun sens. Une seule ligne peut être mise à jour / renvoyée. Mais c'est tout à côté du point principal.

Appel approprié

Cela n'a aucun sens d'utiliser la RETURNINGclause et de renvoyer des valeurs de votre fonction si vous voulez quand même ignorer les valeurs renvoyées dans l'appel. Il est également inutile de décomposer les lignes retournées SELECT * FROM ...si vous les ignorez de toute façon.

Il suffit de renvoyer une constante scalaire ( RETURNING 1), de définir la fonction comme RETURNS int(ou de la supprimer RETURNINGet de la créer RETURNS void) et de l'appeler avecSELECT my_function(...)

Solution

Depuis que vous ...

ne se soucient pas vraiment du résultat

.. juste SELECTune constante du CTE. Il est garanti d'être exécuté tant qu'il est référencé à l'extérieur SELECT(directement ou indirectement).

WITH test AS (SELECT __post_users_id_coin(10, 1))
SELECT 1 FROM test;

Si vous avez réellement une fonction de retour d'ensemble et que vous ne vous souciez toujours pas de la sortie:

WITH test AS (SELECT * FROM __post_users_id_coin(10, 1))
SELECT 1 FROM test LIMIT 1;

Pas besoin de retourner plus d'une ligne. La fonction est toujours appelée.

Enfin, on ne sait pas pourquoi vous avez besoin du CTE pour commencer. Probablement juste une preuve de concept.

Étroitement liés:

Réponse connexe sur SO:

Et considérez:

Erwin Brandstetter
la source
Super, grand fan et honoré d'avoir aussi votre réponse Erwin. J'utilise des CTE comme je le fais INSERTavant UPDATEdans la même fonction d'encapsulation - aucune transaction disponible.
Andy
Agréable. Juste aq: est la testdans WITH test AS (SELECT * FROM __post_users_id_coin(10, 1)) SELECT ... LIMIT 1;ou non considéré comme un CTE de modification?
ypercubeᵀᴹ
@ ypercubeᵀᴹ: A SELECTne "modifie pas les données" selon la terminologie CTE. J'ai ajouté quelques précisions ci-dessus. Il est de la responsabilité de l'utilisateur s'il ajoute du code à une fonction qui modifie les données derrière les rideaux.
Erwin Brandstetter