PostgreSQL 似乎忽略了 CTE 中的 RAISE EXCEPTION

Chr*_* M. 5 postgresql cte

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

经过测试,我发现异常没有像我期望的那样抛出。例如,对于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$
;
Run Code Online (Sandbox Code Playgroud)

我希望调用SELECT testassert_buggy();会抛出异常,但结果是

 somevalue 
-----------

(1 row)
Run Code Online (Sandbox Code Playgroud)

(请注意,firstquery实际上返回 0 行;第 1 行是由于这是一个带out参数的函数。)

通过倒数第二行的以下小改动,抛出异常。

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

如果我重写最后一个查询切换表列表(即使用FROM firstquery, nonemptycheck),则再次没有例外。我很困惑。查询是否以某种方式进行了优化,忽略了异常等副作用?我试图IMMUTABLE从 的定义中删除assert,但这并没有什么区别。

Erw*_*ter 5

根本不执行未引用的 CTE 数据修改CTE除外!)
pgsql-bugs 上的相关线程,Tom Lane 解释了该行为。

在您的第一个示例中,您有:

SELECT * FROM firstquery;
Run Code Online (Sandbox Code Playgroud)

没有提及 CTE nonemptycheck。所以 CTE 永远不会被执行。

在第二个示例中,您有:

SELECT firstquery.* FROM nonemptycheck, firstquery;
Run Code Online (Sandbox Code Playgroud)

nonemptycheck 被引用,所以它被执行,导致异常。

没有行

您在评论中添加的测试用例因类似原因而失败。由于第一个 CTE 不返回任何行,因此外部 SELECT 不返回任何行。不需要执行第二个 CTE,因为不会显示结果。优化器的工作是避免徒劳无功的工作..

附加nonemptycheck跨接子查询(CROSS JOIN或逗号后所附),而不是第二CTE没有帮助,任一。一个类似的优化避免执行:由于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
Run Code Online (Sandbox Code Playgroud)

解决方案

您可以使用以下命令强制评估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
Run Code Online (Sandbox Code Playgroud)

副作用:NULL当不firstquery返回任何行时,这将返回一个填充了值的单行。但是,在这种特殊情况下并非如此,因为在这种情况下,您的断言会引发异常。