sql - 如果先前的 CTE 确实访问关系或错误字符串不包含参数,PostgreSQL 11 返回 null 而不是异常

标签 sql postgresql error-handling plpgsql

在 Postgres 11.4 中,我有一个用于抛出异常的简单函数。这个函数的目的是让我能够从普通 SQL SELECT 中抛出异常。 (如果这是最优雅的解决方案是另一回事)

CREATE OR REPLACE FUNCTION public.throw_error_wrapper("errorText" text)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
BEGIN
    RAISE EXCEPTION '%',$1;
END;
$function$

这个函数是从另一个函数中调用的,这个函数(简化)如下所示:
CREATE OR REPLACE FUNCTION public.my_function("myParam" integer)
 RETURNS void
 LANGUAGE sql
AS $function$

WITH my_cte AS (
  SELECT 'foo'
)
SELECT 
    throw_error_wrapper('my_function throws an error when called with parameter: ' || $1 || ' Please try again.')
FROM my_cte     

$function$

因此,它应该总是抛出一个错误消息,其中包含参数 $1 的值。 .现在,如果我将这个函数称为 SELECT my_function(42);一切都按预期工作。我得到了预期的错误

my_function throws an error when called with parameter: 42 Please try again. CONTEXT: PL/pgSQL function throw_error_wrapper(text) line 3 at RAISE SQL function "my_function" statement 1



现在让我们创建一个包含一列的虚拟表:
CREATE TABLE IF NOT EXISTS my_relation (my_column text);

然后替换虚拟 CTE SELECT 'foo'SELECT my_column FROM my_relation这样my_function现在看起来像这样:
CREATE OR REPLACE FUNCTION public.my_function("myParam" integer)
 RETURNS void
 LANGUAGE sql
AS $function$

WITH my_cte AS (
  SELECT my_column FROM my_relation
)
SELECT 
    throw_error_wrapper('my_function throws an error when called with parameter: ' || $1 || ' Please try again.')
FROM my_cte     

$function$

不过,我希望现在再次执行 SELECT my_function(42); 时会出现错误。
但是,我没有收到错误,而只是一个空结果。

现在如果我删除参数 $1从错误消息中,my_function现在由下面的代码组成
CREATE OR REPLACE FUNCTION public.my_function("myParam" integer)
 RETURNS void
 LANGUAGE sql
AS $function$

WITH my_cte AS (
  SELECT my_column FROM my_relation
)
SELECT 
    throw_error_wrapper('my_function throws an error when called with parameter: ' || ' Please try again.')
FROM my_cte     

$function$

我再次得到预期的错误(这一次,当然,没有 $1 的值)。

这真的让我很困惑。为什么 CTE 和参数连接会阻止函数按预期工作?为什么不应该抛出错误?

最佳答案

这只是我的猜测,如果有人给出更好的解释,将删除。我也认为这个问题更适合 https://dba.stackexchange.com/

PostgreSQL 在看到 IMMUTABLE 时会进行某种优化。在查询中使用具有常量参数的函数并在执行查询之前对其进行评估,然后将其结果视为不可变值。例如,当您在老式继承分区上使用 CHECK 约束并希望通过可能是函数结果的某个值过滤掉分区时,它可能会很有用 - 只有当该函数是不可变的时它才会起作用,很可能意味着它是在计划期间执行,以便 PostgreSQL 在实际搜索之前知道要搜索的分区。这就是为什么即使没有返回行也会引发异常,因此该函数应该永远不会执行。

尝试解释该查询抛出异常,它应该只输出查询计划,但它会抛出异常——因为它决定执行该函数,而不管只是为了获得该计划。

test=# EXPLAIN
test-# WITH my_cte AS (
test(#   SELECT my_column FROM my_relation
test(# )
test-# SELECT
test-#     throw_error_wrapper('my_function throws an error when called with parameter: ' || 42 || ' Please try again.')
test-# FROM my_cte;
ERROR:  my_function throws an error when called with parameter: 42 Please try again.
CONTEXT:  PL/pgSQL function throw_error_wrapper(text) line 3 at RAISE

我要求优化,因为如果您更改功能 public.throw_error_wrapper("errorText" text)来自 IMMUTABLESTABLEVOLATILE当没有返回行时,它将停止抛出错误。

使用常量参数的函数不应该以相同的方式运行吗? PostgreSQL 应该知道它会一直执行 public.throw_error_wrapper(42) ,所以它应该以同样的方式优化它。 PL/pgSQL 也是如此语言,但在 SQL 的情况下没有那么多.可以使用分区和外部表来说明。在下面的示例中,您将看到分区创建的方式无法访问,原因有两个:未定义用户映射和外部服务器不存在。如果尝试访问,它总是会失败。
CREATE SERVER test_srv FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (dbname 'test', host 'fake_host');

CREATE TABLE IF NOT EXISTS my_relation (dat date, my_column text);

CREATE FOREIGN TABLE my_relation_partition_1
(constraint dat_chk check(dat between '2019-06-01' and '2019-06-30'))
INHERITS (my_relation) SERVER test_srv;

没有扫描外表:
test=# explain SELECT * FROM my_relation WHERE dat = '2019-05-04';
                            QUERY PLAN
------------------------------------------------------------------
 Append  (cost=0.00..0.01 rows=1 width=36)
   ->  Seq Scan on my_relation  (cost=0.00..0.00 rows=1 width=36)
         Filter: (dat = '2019-05-04'::date)
(3 rows)

test=# SELECT * FROM my_relation WHERE dat = '2019-05-04';
 dat | my_column
-----+-----------
(0 rows)

尝试扫描外部表:
test=# explain SELECT * FROM my_relation WHERE dat = '2019-06-04';
                                      QUERY PLAN
--------------------------------------------------------------------------------------
 Append  (cost=0.00..127.24 rows=8 width=36)
   ->  Seq Scan on my_relation  (cost=0.00..0.00 rows=1 width=36)
         Filter: (dat = '2019-06-04'::date)
   ->  Foreign Scan on my_relation_partition_1  (cost=100.00..127.20 rows=7 width=36)
(4 rows)

test=# SELECT * FROM my_relation WHERE dat = '2019-06-04';
ERROR:  user mapping not found for "postgres"

如果您以不是 IMMUTABLE 的方式通过该日期,例如作为非不可变函数的结果,则它不会用于通过 CHECK 约束过滤掉分区。所以在这里,尽管我们知道它不应该触及 June 2019 分区,但它仍然如此,因为 date_trunc(..) 的结果不是不可变/恒定的。
test=# explain SELECT * FROM my_relation WHERE dat = date_trunc('month', '2019-05-04'::date);
                                            QUERY PLAN
---------------------------------------------------------------------------------------------------
 Append  (cost=0.00..161.23 rows=8 width=36)
   ->  Seq Scan on my_relation  (cost=0.00..0.00 rows=1 width=36)
         Filter: (dat = date_trunc('month'::text, ('2019-05-04'::date)::timestamp with time zone))
   ->  Foreign Scan on my_relation_partition_1  (cost=100.00..161.19 rows=7 width=36)
         Filter: (dat = date_trunc('month'::text, ('2019-05-04'::date)::timestamp with time zone))
(5 rows)

好的,现在您的查询使用不可变值进行连接:
test=# WITH my_cte AS (
test(#   SELECT my_column FROM my_relation WHERE dat = '2019-05-04'
test(# )
test-# SELECT
test-#     throw_error_wrapper('my_function throws an error when called with parameter: ' || random()::int2 || ' Please try again.')
test-# FROM my_cte;
 throw_error_wrapper
---------------------
(0 rows)

没有抛出异常。现在我们有了这个,让我们看看函数的行为方式。
CREATE OR REPLACE FUNCTION public.my_function(adat date, "myParam" integer)
 RETURNS void
 LANGUAGE sql
 SECURITY DEFINER
AS $function$
WITH my_cte AS (
  SELECT my_column FROM my_relation where dat = $1
)
SELECT 
    throw_error_wrapper('my_function throws an error when called with parameter: ' || $2 || ' Please try again.')
FROM my_cte     
$function$;

test=# SELECT * FROM public.my_function('2019-05-03', 42);
ERROR:  user mapping not found for "postgres"
CONTEXT:  SQL function "my_function" statement 1

正如怀疑的那样,它没有发现函数参数是不可变的,并试图访问外部表。就像那个抛出异常的函数在你的尝试中没有得到(在 PostgreSQL 规划师的眼中)不可变的值(value)。

现在这是我前段时间发现的问题,由于访问太多表而导致分区和函数变慢 - 如果您从语言 SQL 更改函数至plpgsql它会突然将函数参数视为不可变的。

稍微改变定义:
CREATE OR REPLACE FUNCTION public.my_function2(adat date, "myParam" integer)
 RETURNS table(t text)
 LANGUAGE plpgsql
 SECURITY DEFINER
AS $function$
begin
WITH my_cte AS (
  SELECT my_column FROM my_relation where dat = $1
)
SELECT 
    throw_error_wrapper('my_function throws an error when called with parameter: ' || $2 || ' Please try again.')
FROM my_cte;
END;
$function$;

test=# SELECT * FROM public.my_function2('2019-05-03', 42);
ERROR:  my_function throws an error when called with parameter: 42 Please try again.
CONTEXT:  PL/pgSQL function throw_error_wrapper(text) line 3 at RAISE
SQL statement "WITH my_cte AS (
  SELECT my_column FROM my_relation where dat = $1
)
SELECT
    throw_error_wrapper('my_function throws an error when called with parameter: ' || $2 || ' Please try again.')
FROM my_cte"
PL/pgSQL function my_function2(date,integer) line 3 at SQL statement

你知道吗,它不仅忽略了外部表/分区,还通过了42作为异常抛出函数的不可变值。

至于功能和语言SQL我认为这很可能是有限的。不久前,您甚至无法使用参数名称,并且只有占位符 $1、$2、$3 [..] 可用,因此这些参数可能存在一些错误,或者就像这样,查询计划程序可以更多轻松地将这些函数的内容集成到执行它们的查询中。

关于sql - 如果先前的 CTE 确实访问关系或错误字符串不包含参数,PostgreSQL 11 返回 null 而不是异常,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57160493/

相关文章:

java - 比较oracle中的两个日期

postgresql - 如何使用 Flink 读取 Postgresql 中的表

C# (429) 请求过多

sql - 如何停止 Sql Server 的用户实例? (Sql Express 用户实例数据库文件被锁定,即使在停止 Sql Express 服务后)

sql - 如何将 (sql) 数据库添加到我的 sintra 应用程序?

sql - MYSQL Select语句Order By和Group By

Postgresql - 一个 ini4 与两个 int2 用于索引

postgresql - 在终端显示Postgres服务器日志输出,同时记录到日志中

走。接口(interface)中单值上下文中的多值

ruby-on-rails - Rails中的管理功能: Error: Couldn't find User with 'id' =