La función PostgreSQL no se ejecuta cuando se llama desde CTE


14

Solo espero confirmar mi observación y obtener una explicación sobre por qué sucede esto.

Tengo una función definida como:

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

Cuando llamo a esta función desde un CTE, ejecuta el comando SQL pero no activa la función, por ejemplo:

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

SELECT
1 -- Select 1 but update not performed

Por otro lado, si llamo a la función desde un CTE y luego selecciono el resultado del CTE (o llamo a la función directamente sin CTE), ejecuta el comando SQL y activa la función, por ejemplo:

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)

Como realmente no me importa el resultado de la función (solo la necesito para realizar la actualización), ¿hay alguna forma de hacer que esto funcione sin seleccionar el resultado del CTE?

Respuestas:


11

Ese es el tipo de comportamiento esperado. Los CTE se materializan pero hay una excepción.

Si no se hace referencia a un CTE en la consulta principal, entonces no se materializa en absoluto. Puedes probar esto por ejemplo y funcionará bien:

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

Código copiado de un comentario en la publicación del blog de Craig Ringer:
los CTE de PostgreSQL son vallas de optimización .


Antes de probar esta y varias consultas similares, pensé que la excepción era: "cuando un CTE no está referenciado en la consulta principal o en otro CTE y no hace referencia a otro CTE". Entonces, si desea que se ejecute el CTE pero los resultados no se muestran en el resultado de la consulta, pensé que esto sería una solución alternativa (haciendo referencia a él en otro CTE).

Pero, por desgracia, no funciona como esperaba:

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

y por lo tanto, mi "regla de excepción" no es correcta. Cuando otro CTE hace referencia a un CTE y la consulta principal no hace referencia a ninguno de ellos, la situación es más complicada y no estoy seguro de qué sucede exactamente y cuándo se materializan los CTE. Tampoco puedo encontrar ninguna referencia para tales casos en la documentación.


No veo una solución mejor que usar lo que ya sugirió:

SELECT * FROM __post_users_id_coin(10, 1) ;

o:

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

Si la función actualiza varias filas y obtiene muchas filas (con 1) en el resultado, puede agregar para obtener una sola fila:

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

pero preferiría que se devuelvan los resultados de la función que hace una actualización, con SELECT *su ejemplo, por lo que cualquier llamada a esta consulta sabe si hubo actualizaciones y cuáles fueron los cambios en la tabla.



4

Esto se espera, comportamiento documentado.

Tom Lane lo explica aquí.

Documentado en el manual aquí:

Declaraciones de datos modificadores en WITHse ejecutan exactamente una vez, y siempre hasta el final , independientemente de si la consulta primaria lee todos (o cualquier) de su producción. Tenga en cuenta que esto es diferente de la regla para SELECTin WITH: como se indicó en la sección anterior, la ejecución de SELECTa solo se lleva a cabo hasta que la consulta primaria exija su salida .

El énfasis audaz es mío. "Datos modificadores" son INSERT, UPDATEy DELETEconsultas. (A diferencia de SELECT). El manual una vez más:

Puede utilizar las declaraciones de datos modificadores ( INSERT, UPDATEo DELETE) en WITH.

Función adecuada

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;

Eliminé las cláusulas predeterminadas (ruido) y STRICTes el sinónimo corto deRETURNS NULL ON NULL INPUT .

Asegúrese de alguna manera de que los nombres de los parámetros no entren en conflicto con los nombres de las columnas. Prefiero _, pero esa es solo mi preferencia personal.

Si coinpuede ser NULLsugiero:

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

Si users.ides la clave principal, entonces RETURNS TABLEni ROWs 1000tiene sentido. Solo se puede actualizar / devolver una sola fila. Pero eso está al lado del punto principal.

Llamada adecuada

No tiene sentido usar la RETURNINGcláusula y devolver los valores de su función si va a ignorar los valores devueltos en la llamada de todos modos. Tampoco tiene sentido descomponer filas devueltas SELECT * FROM ...si las ignora de todos modos.

Simplemente devuelva una constante escalar ( RETURNING 1), defina la función como RETURNS int(o suelte por RETURNINGcompleto y hágala RETURNS void) y llámela conSELECT my_function(...)

Solución

Desde que tu ...

realmente no me importa el resultado

.. solo SELECTuna forma constante del CTE. Se garantiza que se ejecutará siempre que se haga referencia en el exterior SELECT(directa o indirectamente).

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

Si realmente tiene una función de retorno de conjunto y aún no le importa la salida:

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

No es necesario devolver más de 1 fila. La función todavía se llama.

Finalmente, no está claro por qué necesita el CTE para empezar. Probablemente solo una prueba de concepto.

Estrechamente relacionada:

Respuesta relacionada sobre SO:

Y considere:


Fantástico, gran admirador y honrado de tener su respuesta también Erwin. Estoy usando CTE como lo estoy haciendo INSERTantes de UPDATEdentro de la misma función de ajuste : no hay transacciones disponibles.
Andy

Agradable. Sólo aq: es la testde WITH test AS (SELECT * FROM __post_users_id_coin(10, 1)) SELECT ... LIMIT 1;considerar un CTE o no la modificación?
ypercubeᵀᴹ

@ ypercubeᵀᴹ: A SELECTno está "modificando datos" de acuerdo con la terminología CTE. Agregué algunas aclaraciones arriba. Es responsabilidad del usuario si agrega código a una función que modifica los datos detrás de las cortinas.
Erwin Brandstetter
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.