web-dev-qa-db-ja.com

CTE内から呼び出されたときにPostgreSQL関数が実行されない

私の観察を確認し、これがなぜ起こっているのかについての説明を得たいと思っています。

次のように定義された関数があります。

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コマンドが実行され、does trigger関数が実行されます。 :

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の結果を選択せず​​にこれを機能させる方法はありますか?

16
Andy

それは一種の予想される行動です。 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 *を例として返されるようにしたいので、このクエリを呼び出すと、更新があったかどうかと、テーブルの変更が何であるかがわかります。

12
ypercubeᵀᴹ

これは予想される、文書化された動作です。

Tom Laneがここで説明しています。

このマニュアルに記載されています:

WITHのデータ変更ステートメントは1回だけ実行され、常に完了します、プライマリクエリが出力のすべて(または実際にすべて)を読み取るかどうかに関係なく。これはSELECTWITHのルールとは異なることに注意してください。前のセクションで説明したように、SELECTの実行が行われますプライマリクエリがその出力を要求する限り

大胆な強調鉱山。 「データ変更」は、INSERTUPDATEDELETEクエリです。 (SELECTとは対照的です。)。もう一度マニュアル:

INSERTでは、データ変更ステートメント(UPDATEDELETE、または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;

デフォルト(ノイズ)句を削除しました STRICTRETURNS NULL ON NULL INPUT の短い同義語です。

パラメータ名が列名と競合しないようにしてください。先頭に_を付けましたが、それは私の好みです。

coinNULLの場合は、次のことをお勧めします。

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

users.idが主キーの場合、RETURNS TABLEROWs 1000も意味がありません。更新/返される行は1つだけです。しかし、それはすべて要点の外です。

適切な呼び出し

とにかく呼び出しで返された値を無視する場合は、RETURNING句を使用して関数から値を返すことは意味がありません。とにかく無視した場合、返された行をSELECT * FROM ...で分解しても意味がありません。

スカラー定数(RETURNING 1)を返し、関数をRETURNS intとして定義します(またはRETURNINGを完全に削除してRETURNS voidにします)。SELECT my_function(...)

解決

あなたから...

結果を本当に気にしない

.. CTEを構成する定数SELECTだけ。外部SELECTで(直接または間接に)参照されている限り、実行されることが保証されています。

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

実際にset-returning関数があり、それでも出力を気にしない場合:

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

複数の行を返す必要はありません。関数はまだ呼び出されています。

最後に、そもそもCTEが必要な理由は明らかではありません。おそらく単なるコンセプトの証明です。

密接に関連:

SOの関連回答:

そして考慮してください:

5