web-dev-qa-db-ja.com

PostgreSQLはCTEのRAISE EXCEPTIONを無視しているようです

PostgreSQL 9.3を使用して、空のクエリ結果や次のようなものをチェックするためにassertヘルパー関数を定義しようとしています。

_CREATE FUNCTION public.assert (
    in_assertion boolean,
    in_errormessage text
)
RETURNS boolean
IMMUTABLE
LANGUAGE plpgsql
SECURITY INVOKER
AS $function$
    BEGIN
        IF NOT in_assertion THEN
            RAISE EXCEPTION 'assertion failed: %', in_errormessage;
        END IF;

        RETURN in_assertion;
    END;
$function$
;
_

テストの結果、期待したとおりに例外がスローされないことがわかりました。たとえば、CREATE TABLE emptytable (somecolumn text);および

_CREATE FUNCTION public.testassert_buggy (
    out somevalue text
)
LANGUAGE sql
SECURITY DEFINER
AS $function$
    WITH firstquery AS (
            SELECT * FROM emptytable
    ), nonemptycheck AS (
            SELECT assert(count(*) = 42, 'nonemptycheck failed') FROM firstquery
    ) SELECT * FROM firstquery;
$function$
;
_

SELECT testassert_buggy();のような呼び出しが例外をスローすることを期待しますが、代わりに結果は

_ somevalue 
-----------

(1 row)
_

firstqueryは実際には0行を返すことに注意してください。1行は、これがoutパラメーターを持つ関数であるためです。)

最後から2行目が次のように少し変更されたため、例外ISがスローされました。

_CREATE FUNCTION public.testassert (
    out somevalue text
)
LANGUAGE sql
SECURITY DEFINER
AS $function$
    WITH firstquery AS (
            SELECT * FROM emptytable
    ), nonemptycheck AS (
            SELECT assert(count(*) = 42, 'nonemptycheck failed') FROM firstquery
    ) SELECT firstquery.* FROM nonemptycheck, firstquery;
$function$
;
_

テーブルリストを切り替える最後のクエリを書き直した場合(つまり、_FROM firstquery, nonemptycheck_を使用)、再び例外はありません。私は困惑しています。クエリは、例外などの副作用を無視する方法で最適化されていますか? IMMUTABLEの定義からassertを削除しようとしましたが、違いはありませんでした。

4
Chris M.

参照されていないCTEはまったく実行されませんdata-modifyingCTEを除く!)
動作を説明するTom Laneとのpgsql-bugsの関連スレッド

最初の例では、次のようになります。

SELECT * FROM firstquery;

CTE nonemptycheckへの参照はありません。したがって、CTEは実行されません。

2番目の例では、次のようになります。

SELECT firstquery.* FROM nonemptycheck, firstquery;

nonemptycheckが参照されるため、実行され、例外が発生します。

行がありません

コメントに追加したテストケースは、同様の理由で失敗します。最初のCTEはno rowを返すため、外側のSELECTはnoを返します行。結果が表示されないため、2番目のCTEを実行する必要はありません。オプティマイザの仕事は、無駄な作業を避けることです。

2番目のCTEではなくクロス結合サブクエリ(CROSS JOINまたはコンマの後に追加)としてnonemptycheckを追加すると、は行われませんヘルプ、どちらか同様の最適化は実行を回避します:firstqueryは行を返さないため、nonemptycheckを評価しても意味がありませんサブクエリ内:

WITH firstquery AS (
   SELECT *
   FROM   emptytable
   WHERE  FALSE
   )
SELECT f.*     -- even if you append ", n.*" to SELECT list
FROM   firstquery f
    , (
   SELECT assert(count(*) = 42, 'check failed')
   FROM   firstquery
   ) n         -- not executed

解決

FULL OUTER JOINで評価を強制できます:

WITH firstquery AS (
   SELECT *
   FROM   emptytable
   WHERE  false
   )
SELECT f.*
FROM   firstquery f
FULL   JOIN (
   SELECT assert(count(*) = 42, 'check failed')
   FROM   firstquery
   ) nonemptycheck ON TRUE;  -- always executed

副作用:NULLが行を返さない場合、これはfirstquery値で満たされた単一の行を返します。ただし、この特定のケースではありません。この場合、アサートによって例外が発生するためです。

4