Função PostgreSQL não executada quando chamada de dentro da CTE


14

Apenas esperando confirmar minha observação e obter uma explicação sobre por que isso está acontecendo.

Eu tenho uma função 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

Quando eu chamo essa função de um CTE, ele executa o comando SQL, mas não aciona a função, por exemplo:

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

SELECT
1 -- Select 1 but update not performed

Por outro lado, se eu chamar a função de um CTE e selecionar o resultado do CTE (ou chamar a função diretamente sem CTE), ele executará o comando SQL e acionará a função, por exemplo:

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)

Como eu realmente não me importo com o resultado da função (só preciso executar a atualização), existe alguma maneira de fazer isso funcionar sem selecionar o resultado da CTE?

Respostas:


11

Esse é o tipo de comportamento esperado. CTEs são materializadas, mas há uma exceção.

Se um CTE não for mencionado na consulta pai, ele não será materializado. Você pode tentar isso por exemplo e ele funcionará bem:

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

Código copiado de um comentário no post de Craig Ringer:
Os CTEs do PostgreSQL são barreiras de otimização .


Antes de tentar essa e várias consultas semelhantes, pensei que a exceção fosse: "quando um CTE não é referenciado na consulta pai ou em outro CTE e não se refere a outro CTE". Portanto, se você queria que o CTE fosse executado, mas os resultados não mostrados no resultado da consulta, pensei que seria uma solução alternativa (referenciando-o em outro CTE).

Mas, infelizmente, não funciona como eu esperava:

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, portanto, minha "regra de exceção" não está correta. Quando um CTE é referenciado por outro CTE e nenhum deles é referenciado pela consulta pai, a situação é mais complicada e não sei exatamente o que acontece e quando os CTEs são materializados. Também não encontro nenhuma referência para esses casos na documentação.


Não vejo solução melhor do que usar o que você já sugeriu:

SELECT * FROM __post_users_id_coin(10, 1) ;

ou:

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

Se a função atualizar várias linhas e você obter muitas linhas (com 1) no resultado, poderá agregar para obter uma única linha:

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

mas eu preferiria retornar os resultados da função que faz uma atualização, SELECT *como no seu exemplo, de modo que o que chama essa consulta saiba se houve atualizações e quais foram as alterações na tabela.



4

Isso é esperado, comportamento documentado.

Tom Lane explica aqui.

Documentado no manual aqui:

Declarações de dados modificando em WITHsão executadas exatamente uma vez, e sempre para a conclusão , independentemente da consulta principal lê tudo (ou mesmo qualquer) de sua produção. Observe que isso é diferente da regra para SELECTem WITH: como declarado na seção anterior, a execução de a SELECTé realizada apenas na medida em que a consulta primária exigir sua saída .

Negrito ênfase minha. "Data-modificando" são INSERT, UPDATEe DELETEconsultas. (Ao contrário de SELECT.). O manual mais uma vez:

Você pode usar as declarações que modifique os dados ( INSERT, UPDATEou DELETE) em WITH.

Função adequada

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;

Larguei as cláusulas padrão (ruído) e STRICTé o sinônimo curto deRETURNS NULL ON NULL INPUT .

Certifique-se de alguma forma que os nomes dos parâmetros não entrem em conflito com os nomes das colunas. Eu comecei com _, mas essa é apenas a minha preferência pessoal.

Se coinpuder ser NULL, sugiro:

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

Se users.idé a chave primária, então RETURNS TABLEnem ROWs 1000faz sentido. Somente uma única linha pode ser atualizada / retornada. Mas isso não vem ao caso principal.

Chamada adequada

Não faz sentido usar a RETURNINGcláusula e retornar valores de sua função se você for ignorar os valores retornados na chamada. Também não faz sentido decompor as linhas retornadas SELECT * FROM ...se você as ignorar de qualquer maneira.

Apenas retorne uma constante escalar ( RETURNING 1), defina a função como RETURNS int(ou solte-a RETURNINGcompletamente e faça-a RETURNS void) e chame-a comSELECT my_function(...)

Solução

Desde que você ...

realmente não me importo com o resultado

.. apenas SELECTuma forma constante do CTE. É garantido que será executado desde que seja referenciado no exterior SELECT(direta ou indiretamente).

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

Se você realmente tem uma função de retorno de conjunto e ainda não se importa com a saída:

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

Não há necessidade de retornar mais de uma linha. A função ainda é chamada.

Por fim, não está claro por que você precisa do CTE para começar. Provavelmente apenas uma prova de conceito.

Intimamente relacionado:

Resposta relacionada no SO:

E considere:


Fantástico, grande fã e honrado por ter sua resposta também, Erwin. Estou usando CTEs como INSERTantes, UPDATEdentro da mesma função de quebra automática - nenhuma transação disponível.
28416 Andy

Agradável. Apenas aq: é o testno WITH test AS (SELECT * FROM __post_users_id_coin(10, 1)) SELECT ... LIMIT 1;considerado um CTE modificando ou não?
ypercubeᵀᴹ

@ ypercubeᵀᴹ: A SELECTnão é "modificador de dados" de acordo com a terminologia CTE. Eu adicionei alguns esclarecimentos acima. É de responsabilidade do usuário se ele adicionar código a uma função que modifica os dados atrás das cortinas.
Erwin Brandstetter
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.