La funzione PostgreSQL non viene eseguita quando viene chiamata dall'interno di CTE


14

Spero solo di confermare la mia osservazione e ottenere una spiegazione sul perché questo sta accadendo.

Ho una funzione definita come:

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

Quando chiamo questa funzione da un CTE, esegue il comando SQL ma non attiva la funzione, ad esempio:

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

SELECT
1 -- Select 1 but update not performed

D'altra parte, se chiamo la funzione da un CTE e quindi seleziono il risultato del CTE (o chiamo la funzione direttamente senza CTE) esegue il comando SQL e attiva la funzione, ad esempio:

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

SELECT
*
FROM
test -- Select result and update performed

o

SELECT * FROM __post_users_id_coin(10,1)

Dal momento che non mi interessa davvero il risultato della funzione (ne ho solo bisogno per eseguire l'aggiornamento) esiste un modo per farlo funzionare senza selezionare il risultato del CTE?

Risposte:


11

È un tipo di comportamento previsto. I CTE sono materializzati ma c'è un'eccezione.

Se un CTE non fa riferimento nella query principale, non si materializza affatto. Puoi provare questo per esempio e funzionerà benissimo:

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

Codice copiato da un commento nel post sul blog di Craig Ringer:
i CTE di PostgreSQL sono recinzioni di ottimizzazione .


Prima di provare questa e diverse query simili, ho pensato che l'eccezione fosse: "quando un CTE non è referenziato nella query principale o in un altro CTE e non fa riferimento a se stesso un altro CTE". Quindi, se si desidera eseguire il CTE ma i risultati non sono mostrati nel risultato della query, ho pensato che questo sarebbe stato un modo per aggirare il problema (facendo riferimento a esso in un altro CTE).

Ma ahimè, non funziona come mi aspettavo:

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

e quindi la mia "regola delle eccezioni" non è corretta. Quando un CTE fa riferimento a un altro CTE e nessuno di essi fa riferimento alla query principale, la situazione è più complicata e non sono sicuro esattamente cosa succede e quando i CTE sono materializzati. Non riesco a trovare alcun riferimento per tali casi nella documentazione.


Non vedo alcuna soluzione migliore dell'utilizzo di ciò che hai già suggerito:

SELECT * FROM __post_users_id_coin(10, 1) ;

o:

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

Se la funzione aggiorna più righe e si ottengono molte righe (con 1) nel risultato, è possibile aggregare per ottenere una singola riga:

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

ma preferirei che vengano restituiti i risultati della funzione che esegue un aggiornamento, SELECT *come ad esempio, quindi qualunque sia la chiamata a questa query sa se c'erano aggiornamenti e quali erano le modifiche nella tabella.



4

Ciò è previsto, comportamento documentato.

Tom Lane lo spiega qui.

Documentato nel manuale qui:

Le istruzioni che modificano i dati in WITHvengono eseguite esattamente una volta e sempre fino al completamento , indipendentemente dal fatto che la query primaria legga tutto (o effettivamente qualsiasi) del loro output. Si noti che questo è diverso dalla regola per SELECTin WITH: come indicato nella sezione precedente, l'esecuzione di a SELECTviene eseguita solo nella misura in cui la query primaria richiede il suo output .

Enorme enfasi sulla mia. "Data modificanti" sono INSERT, UPDATEe DELETEle query. (Al contrario di SELECT.). Il manuale ancora una volta:

È possibile utilizzare le istruzioni di dati modificanti ( INSERT, UPDATE, o DELETE) in WITH.

Funzione corretta

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;

Ho abbandonato le clausole predefinite (rumore) ed STRICTè il breve sinonimo diRETURNS NULL ON NULL INPUT .

Assicurarsi in qualche modo che i nomi dei parametri non siano in conflitto con i nomi delle colonne. Ho anteposto _, ma questa è solo la mia preferenza personale.

Se coinpossibile, NULLsuggerisco:

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

Se users.idè la chiave primaria, allora né ha RETURNS TABLEROWs 1000senso. È possibile aggiornare / restituire una sola riga. Ma questo è tutto a parte il punto principale.

Chiamata corretta

Non ha senso utilizzare la RETURNINGclausola e restituire i valori dalla propria funzione se si desidera ignorare comunque i valori restituiti nella chiamata. Inoltre, non ha senso scomporre le righe restituite SELECT * FROM ...se le ignori comunque.

Restituisci semplicemente una costante scalare ( RETURNING 1), definisci la funzione come RETURNS int(o rilasciala del RETURNINGtutto e creala RETURNS void) e chiamala conSELECT my_function(...)

Soluzione

Dal momento che tu ...

non importa davvero del risultato

.. solo SELECTuna forma costante del CTE. È garantito che venga eseguito purché sia ​​referenziato all'esterno SELECT(direttamente o indirettamente).

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

Se in realtà hai una funzione di restituzione set e ancora non ti interessa l'output:

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

Non è necessario restituire più di 1 riga. La funzione è ancora chiamata.

Infine, non è chiaro il motivo per cui è necessario il CTE per cominciare. Probabilmente solo una prova di concetto.

Strettamente correlato:

Risposta correlata su SO:

E considera:


Fantastico, grande fan e onorato di avere anche la tua risposta Erwin. Sto usando CTE mentre sto facendo un INSERTprecedente UPDATEall'interno della stessa funzione di wrapping - nessuna transazione disponibile.
Andy,

Bello. Solo aq: il testin è WITH test AS (SELECT * FROM __post_users_id_coin(10, 1)) SELECT ... LIMIT 1;considerato un CTE modificante o no?
ypercubeᵀᴹ

@ ypercubeᵀᴹ: A SELECTnon sta "modificando i dati" secondo la terminologia CTE. Ho aggiunto alcuni chiarimenti sopra. È responsabilità dell'utente se aggiunge il codice a una funzione che modifica i dati dietro le tende.
Erwin Brandstetter,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.