PostgreSQL-Funktion wird beim Aufruf aus CTE heraus nicht ausgeführt

14

Ich hoffe nur, meine Beobachtung zu bestätigen und eine Erklärung zu erhalten, warum dies geschieht.

Ich habe eine Funktion definiert als:

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

Wenn ich diese Funktion von einem CTE aus aufrufe, führt sie den SQL-Befehl aus, löst die Funktion jedoch nicht aus. Beispiel:

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

SELECT
1 -- Select 1 but update not performed

Wenn ich dagegen die Funktion von einem CTE aus aufrufe und dann das Ergebnis des CTE auswähle (oder die Funktion direkt ohne CTE aufrufe), führt sie den SQL-Befehl aus und löst die Funktion aus, zum Beispiel:

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

SELECT
*
FROM
test -- Select result and update performed

oder

SELECT * FROM __post_users_id_coin(10,1)

Da mir das Ergebnis der Funktion nicht wirklich wichtig ist (ich brauche es nur, um das Update durchzuführen), gibt es eine Möglichkeit, dies zum Laufen zu bringen, ohne das Ergebnis des CTE auszuwählen?

Andy
quelle

Antworten:

11

Das ist eine Art erwartetes Verhalten. CTEs werden materialisiert, aber es gibt eine Ausnahme.

Wenn ein CTE in der übergeordneten Abfrage nicht referenziert wird, wird er überhaupt nicht materialisiert. Sie können dies zum Beispiel versuchen und es wird gut laufen:

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

Code kopiert aus einem Kommentar in Craig
Ringers Blogpost: Die CTEs von PostgreSQL sind Optimierungszäune .


Bevor ich diese und mehrere ähnliche Abfragen ausprobiert habe, dachte ich, dass die Ausnahme lautete: "Wenn ein CTE in der übergeordneten Abfrage oder in einem anderen CTE nicht referenziert wird und sich nicht auf einen anderen CTE bezieht". Wenn Sie also möchten, dass der CTE ausgeführt wird, die Ergebnisse jedoch nicht im Abfrageergebnis angezeigt werden, ist dies eine Problemumgehung (Verweis auf einen anderen CTE).

Aber leider funktioniert es nicht wie erwartet:

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

und deshalb ist meine "Ausnahmeregel" nicht korrekt. Wenn ein CTE von einem anderen CTE referenziert wird und keiner von ihnen von der übergeordneten Abfrage referenziert wird, ist die Situation komplizierter und ich bin nicht sicher, was genau passiert und wann die CTEs materialisiert werden. Ich kann auch in der Dokumentation keinen Hinweis auf solche Fälle finden.


Ich sehe keine bessere Lösung als das, was Sie bereits vorgeschlagen haben:

SELECT * FROM __post_users_id_coin(10, 1) ;

oder:

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

Wenn die Funktion mehrere Zeilen aktualisiert und Sie 1im Ergebnis viele Zeilen (mit ) erhalten, können Sie diese zu einer einzelnen Zeile zusammenfassen:

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

Aber ich würde es vorziehen, die Ergebnisse der Funktion, die ein Update ausführt, SELECT *als Ihr Beispiel zurückzugeben, damit jeder Aufruf dieser Abfrage weiß, ob Updates vorhanden sind und welche Änderungen in der Tabelle vorgenommen wurden.

ypercubeᵀᴹ
quelle
Lassen Sie uns diese Diskussion im Chat fortsetzen .
Ypercubeᵀᴹ
4

Dies wird erwartet, dokumentiertes Verhalten.

Tom Lane erklärt es hier.

Hier im Handbuch dokumentiert:

Datenmodifizierende Anweisungen in WITHwerden genau einmal und immer vollständig ausgeführt , unabhängig davon, ob die primäre Abfrage alle (oder tatsächlich alle) Ausgaben liest. Beachten Sie, dass dies von der Regel für SELECTin WITHabweicht: Wie im vorherigen Abschnitt angegeben, wird die Ausführung von a SELECTnur so lange ausgeführt, wie die primäre Abfrage ihre Ausgabe erfordert .

Meine kühne Betonung. „Data-modifizierende“ sind INSERT, UPDATEund DELETEAbfragen. (Im Gegensatz zu SELECT.). Das Handbuch noch einmal:

Sie können Daten modifizierende Anweisungen (verwenden INSERT, UPDATEoder DELETE) in WITH.

Passende Funktion

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;

Ich habe Standardklauseln (Noise) fallen gelassen und STRICTist das kurze Synonym fürRETURNS NULL ON NULL INPUT .

Stellen Sie irgendwie sicher, dass Parameternamen nicht mit Spaltennamen in Konflikt stehen. Ich habe mit vorangestellt _, aber das ist nur meine persönliche Präferenz.

Wenn coinkann NULLich vorschlagen:

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

Wenn users.idist der Primärschlüssel, dann weder RETURNS TABLEnoch ROWs 1000Sinn. Es kann nur eine einzelne Zeile aktualisiert / zurückgegeben werden. Aber das ist alles neben dem Hauptpunkt.

Richtiger Anruf

Es macht keinen Sinn, das zu benutzen RETURNING Klausel zu verwenden und Werte aus Ihrer Funktion zurückzugeben, wenn Sie die zurückgegebenen Werte im Aufruf trotzdem ignorieren wollen. Es macht auch keinen Sinn, zurückgegebene Zeilen mit zu zerlegen, SELECT * FROM ...wenn Sie sie trotzdem ignorieren.

Geben Sie einfach eine Skalarkonstante ( RETURNING 1) zurück, definieren Sie die Funktion als RETURNS int(oder lassen Sie sie fallenRETURNING ganz und machen Sie sie RETURNS void) und rufen Sie sie mit aufSELECT my_function(...)

Lösung

Seit du ...

Das Ergebnis ist mir egal

.. nur SELECTeine konstante Form der CTE. Es ist garantiert ausgeführt, solange es in der äußeren SELECT(direkt oder indirekt) verwiesen wird .

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

Wenn Sie tatsächlich eine Set-Return- Funktion haben und sich trotzdem nicht um die Ausgabe kümmern:

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

Es muss nicht mehr als eine Zeile zurückgegeben werden. Die Funktion wird weiterhin aufgerufen.

Schließlich ist unklar, warum Sie den CTE benötigen. Wahrscheinlich nur ein Proof of Concept.

Eng verwandt:

Verwandte Antwort auf SO:

Und bedenken Sie:

Erwin Brandstetter
quelle
Großartiger, großer Fan und es ist eine Ehre, Ihre Antwort auch Erwin zu haben. Ich verwende CTEs, wie ich es INSERTvor UPDATEder gleichen Wrapping-Funktion mache - keine Transaktionen verfügbar.
Andy
Nett. Aq nur: ist die testin WITH test AS (SELECT * FROM __post_users_id_coin(10, 1)) SELECT ... LIMIT 1;einer Modifizierung CTE oder nicht in Betracht gezogen?
Ypercubeᵀᴹ
@ ypercubeᵀᴹ: A SELECTist gemäß CTE-Terminologie nicht " datenmodifizierend ". Ich habe oben einige Klarstellungen hinzugefügt. Es liegt in der Verantwortung des Benutzers, wenn er einer Funktion, die Daten hinter den Kulissen ändert, Code hinzufügt.
Erwin Brandstetter