从 CTE 内部调用时未执行 PostgreSQL 函数

And*_*ndy 16 postgresql cte

只是希望证实我的观察并得到有关为什么会发生这种情况的解释。

我有一个函数定义为:

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
Run Code Online (Sandbox Code Playgroud)

当我从 CTE 调用此函数时,它执行 SQL 命令但不触发该函数,例如:

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

SELECT
1 -- Select 1 but update not performed
Run Code Online (Sandbox Code Playgroud)

另一方面,如果我从 CTE 调用该函数,然后选择 CTE 的结果(或在没有 CTE 的情况下直接调用该函数),它将执行 SQL 命令并触发该函数,例如:

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

SELECT
*
FROM
test -- Select result and update performed
Run Code Online (Sandbox Code Playgroud)

或者

SELECT * FROM __post_users_id_coin(10,1)
Run Code Online (Sandbox Code Playgroud)

由于我并不真正关心函数的结果(只需要它来执行更新),是否有任何方法可以在不选择 CTE 结果的情况下使其工作?

ype*_*eᵀᴹ 12

这是一种预期的行为。CTE 已实现,但有一个例外。

如果父查询中未引用 CTE,则它根本不会具体化。例如,您可以试试这个,它会运行良好:

WITH not_executed AS (SELECT 1/0),
     executed AS (SELECT 1)
SELECT * FROM executed ;
Run Code Online (Sandbox Code Playgroud)

从 Craig Ringer 的博客文章中的评论中复制的代码:
PostgreSQL 的 CTE 是优化栅栏


在尝试这个和几个类似的查询之前,我认为例外是:“当父查询或另一个 CTE 中没有引用 CTE 并且没有引用另一个 CTE 时”。因此,如果您希望执行 CTE 但查询结果中未显示结果,我认为这将是一种解决方法(在另一个 CTE 中引用它)。

但可惜,它不像我预期的那样工作

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
Run Code Online (Sandbox Code Playgroud)

因此,我的“例外规则”是不正确的。当一个 CTE 被另一个 CTE 引用而父查询没有引用它们时,情况会更加复杂,我不确定会发生什么以及 CTE 何时具体化。我也无法在文档中找到此类案例的任何参考。


我没有看到比使用您已经建议的更好的解决方案:

SELECT * FROM __post_users_id_coin(10, 1) ;
Run Code Online (Sandbox Code Playgroud)

或者:

WITH test AS
    (SELECT * FROM __post_users_id_coin(10, 1))
SELECT *
FROM test ;
Run Code Online (Sandbox Code Playgroud)

如果该函数更新多行并且您1在结果中获得多行(带有),您可以聚合以获得单行:

SELECT MAX(1) AS result FROM __post_users_id_coin(10, 1) ;
Run Code Online (Sandbox Code Playgroud)

但我更希望返回执行更新的函数的结果,以SELECT *您的示例为例,因此无论调用此查询,都知道是否有更新以及表中的更改是什么。


Erw*_*ter 5

这是预期的、记录在案的行为。

汤姆·莱恩 (Tom Lane) 在这里进行了解释。

此处的手册中记录:

修改数据的报表WITH都只执行一次,并且 总是完成,独立的是否主要查询读取所有(或任何)的输出。请注意,这与SELECTin的规则不同WITH:如上一节所述, aSELECT的执行仅在主查询需要其输出时进行

大胆强调我的。“数据修改”是INSERT,UPDATEDELETE查询。(相对于SELECT.)。手册再次:

你可以使用(修改数据的语句INSERTUPDATEDELETE)的WITH

适当的功能

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;
Run Code Online (Sandbox Code Playgroud)

我删除了默认(噪音)子句, STRICTRETURNS NULL ON NULL INPUT.

以某种方式确保参数名称与列名称不冲突。我在前面加上_,但这只是我个人的喜好。

如果coin可以,NULL我建议:

SET    coin = CASE WHEN coin IS NULL THEN _coins ELSE coin + _coins END
Run Code Online (Sandbox Code Playgroud)

如果users.id是主键,那么既没有RETURNS TABLE也没有ROWs 1000任何意义。只能更新/返回一行。但这都不是重点。

正确调用

RETURNING如果您无论如何都要忽略调用中的返回值,那么使用子句并从函数中返回值是没有意义的。SELECT * FROM ...如果无论如何都忽略它们,分解返回的行也是没有意义的。

只需返回一个标量常量 ( RETURNING 1),将函数定义为RETURNS int(或RETURNING完全删除并使其成为RETURNS void)并使用SELECT my_function(...)

解决方案

自从你 ...

不在乎结果

.. 只是SELECT一个常数形式的 CTE。只要在外部SELECT(直接或间接)中引用它,它就可以保证被执行。

WITH test AS (SELECT __post_users_id_coin(10, 1))
SELECT 1 FROM test;
Run Code Online (Sandbox Code Playgroud)

如果您确实有一个设置返回函数并且仍然不关心输出:

WITH test AS (SELECT * FROM __post_users_id_coin(10, 1))
SELECT 1 FROM test LIMIT 1;
Run Code Online (Sandbox Code Playgroud)

无需返回超过 1 行。该函数仍被调用。

最后,不清楚为什么您需要 CTE 开始。可能只是一个概念证明。

密切相关:

SO的相关答案:

并考虑: