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


14

只是希望确认我的观察并获得关于为什么发生这种情况的解释。

我有一个函数定义为:

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

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

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

SELECT
1 -- Select 1 but update not performed

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

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

SELECT
*
FROM
test -- Select result and update performed

要么

SELECT * FROM __post_users_id_coin(10,1)

由于我不太在意函数的结果(只需要执行更新即可),是否有任何方法可以在不选择CTE结果的情况下使它起作用?

Answers:


11

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

如果父查询中未引用CTE,则根本不会实现。您可以尝试例如,它将运行良好:

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

从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

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


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

SELECT * FROM __post_users_id_coin(10, 1) ;

要么:

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

如果函数更新了多行,并且1结果中包含多行(带有),则可以合计得到单行:

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

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



4

这是预期的,已记录的行为。

汤姆·莱恩在这里解释。

在手册中记录在这里:

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

大胆强调我的。“数据改性”是INSERTUPDATE并且DELETE查询。(而不是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;

我删除了默认(噪声)子句,它 STRICT是的简短同义词RETURNS NULL ON NULL INPUT

确保参数名称不与列名称冲突。我以开头_,但这只是我个人的喜好。

如果coin可以,NULL我建议:

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

如果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;

如果您实际上有一个设置返回函数,但仍然不在乎输出:

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

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

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

密切相关:

SO的相关答案:

并考虑:


很棒,忠实的粉丝,也很荣幸也能回答您。我正在使用CTE,就像在同一个包装函数中INSERT先执行CTE一样UPDATE-没有可用的事务。
安迪

真好 只是阿Q:是testWITH test AS (SELECT * FROM __post_users_id_coin(10, 1)) SELECT ... LIMIT 1;被认为是修改CTE与否?
ypercubeᵀᴹ

@ypercubeᵀᴹ:SELECT根据CTE术语,A 不是“数据修改”。我在上面添加了一些说明。如果用户将代码添加到修改幕后数据的功能中,则这是用户的责任。
Erwin Brandstetter
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.