是否可以使用 RECORD 参数编写多态 Postgres 函数?

Eri*_*tta 6 postgresql

我想编写一个 PL/pgSQL 函数,它可以获取不同类型的记录,检查提供的记录类型,然后对记录执行某些操作。例子:

CREATE FUNCTION polymorphic_input(arg_rec RECORD) RETURNS TEXT LANGUAGE plpgsql AS
$plpgsql$
BEGIN
  IF pg_typeof(arg_rec)::text = 'information_schema.tables' THEN
    RETURN (arg_rec::information_schema.tables).table_name;
  ELSIF pg_typeof(arg_rec)::text = 'information_schema.columns' THEN
    RETURN (arg_rec::information_schema.columns).column_name;
  ELSE
    RETURN 'unknown';
  END IF;
END;
$plpgsql$;
Run Code Online (Sandbox Code Playgroud)

当您使用表中的一行调用该函数时information_schema.tables,它应该返回表的名称,当您像这样调用它时它会返回表的名称:

-- this returns table name "pg_type"
SELECT polymorphic_input((SELECT t FROM information_schema.tables t WHERE table_name = 'pg_type' LIMIT 1));
Run Code Online (Sandbox Code Playgroud)

当您使用表中的一行调用该函数时information_schema.columns,它应该返回列的名称,当您像这样调用它时它会返回列的名称:

-- this returns column name "objsubid"
SELECT polymorphic_input((SELECT t FROM information_schema.columns t WHERE t.column_name = 'objsubid' LIMIT 1));
Run Code Online (Sandbox Code Playgroud)

问题是您不能使用不同的行类型连续两次调用该函数。例如,如果您使用 row form 调用它information_schema.columns,那么当您使用 row form 调用它时information_schema.tables,您会收到如下错误:

参数1(information_schema.tables)的类型与准备计划(information_schema.columns)时的类型不匹配

“准备计划时”这个词给了我一个暗示,Postgres 正在缓存计划,所以我认为DISCARD PLANS;在每次调用该函数之前运行是可行的,而且当您运行整个查询时确实如此:

DISCARD PLANS; SELECT polymorphic_input((SELECT t FROM information_schema.tables t WHERE table_name = 'pg_type' LIMIT 1));
DISCARD PLANS; SELECT polymorphic_input((SELECT t FROM information_schema.columns t WHERE t.column_name = 'objsubid' LIMIT 1));
Run Code Online (Sandbox Code Playgroud)

运行DISCARD PLANS;似乎是核心选项,毫无疑问会影响现实场景中的性能。经过一些实验,我发现使用该pg_typeof函数会强制缓存计划。我们可以pg_typeof通过添加一个指定所需记录类型的参数来重写该函数来避免:

CREATE FUNCTION polymorphic_input2(arg_rec RECORD, arg_type text) RETURNS TEXT LANGUAGE plpgsql AS
$plpgsql$
BEGIN
  IF arg_type = 'tables' THEN
    RETURN (arg_rec::information_schema.tables).table_name;
  ELSIF arg_type = 'columns' THEN
    RETURN (arg_rec::information_schema.columns).column_name;
  ELSE
    RETURN 'unknown';
  END IF;
END;
$plpgsql$;
Run Code Online (Sandbox Code Playgroud)

然后,您可以使用不同的行类型连续多次调用polymorphic_input2而不会出现错误,如下所示:

-- no need for DISCARD PLANS here...these calls work fine.
SELECT polymorphic_input2((SELECT t FROM information_schema.tables t WHERE table_name = 'pg_type' LIMIT 1), 'tables');
SELECT polymorphic_input2((SELECT t FROM information_schema.columns t WHERE t.column_name = 'objsubid' LIMIT 1), 'columns');
Run Code Online (Sandbox Code Playgroud)

问题polymorphic_input2是您必须手动给出有关所需记录类型的提示。我的问题:是否可以实现一个多态函数,该函数可以找出传递给它的记录类型,而不会出现缓存的计划错误?

文档提到了plan_cache_mode设置:

准备好的语句(显式准备或隐式生成,例如通过 PL/pgSQL)可以使用自定义或通用计划执行。自定义计划是使用其特定的参数值集为每次执行重新制定的,而通用计划不依赖于参数值并且可以在执行之间重复使用......允许的值为 auto(默认值)、force_custom_plan 和强制_通用_计划...

我尝试通过运行来消除错误SET plan_cache_mode = force_custom_plan;,但这没有帮助(这可能是一个错误,因为文档暗示它应该在每次调用中强制执行自定义计划,但 Postgres 仍在缓存该计划并导致错误)。只DISCARD PLANS工作了。

关于计划缓存的文档似乎认识到这个问题并说:

记录变量的可变性质在这方面提出了另一个问题。当在表达式或语句中使用记录变量的字段时,字段的数据类型在函数的一次调用与下一次调用之间不得更改,因为将使用第一个表达式时存在的数据类型来分析每个表达式到达。必要时可以使用 EXECUTE 来解决这个问题。

...文档的进一步说明表明这种情况不应该发生:

同样,具有多态参数类型的函数对于调用它们的实际参数类型的每个组合都有一个单独的语句缓存,因此数据类型差异不会导致意外失败

文档EXECUTE进一步证实了这一点:

此外,通过 EXECUTE 执行的命令没有计划缓存。相反,每次运行语句时都会计划该命令。因此,可以在函数内动态创建命令字符串,以对不同的表和列执行操作。

pg_typeof所以我尝试了另一个尝试通过以下方式运行的变体EXECUTE

CREATE FUNCTION polymorphic_input3(arg_rec RECORD) RETURNS TEXT LANGUAGE plpgsql AS
$plpgsql$
DECLARE
  rec_type text;
BEGIN
  EXECUTE 'SELECT pg_typeof($1)' INTO rec_type USING arg_rec;
  IF rec_type = 'information_schema.tables' THEN
    RETURN (arg_rec::information_schema.tables).table_name;
  ELSIF rec_type = 'information_schema.columns' THEN
    RETURN (arg_rec::information_schema.columns).column_name;
  ELSE
    RETURN 'unknown';
  END IF;
END;
$plpgsql$;
Run Code Online (Sandbox Code Playgroud)

...但这仍然会产生与pg_typeof直接调用的变体相同的错误。

我的问题再次出现:是否有可能(在 Postgres 14 中)实现一个多态函数,该函数可以找出传递给它的记录类型,而不会出现缓存计划错误?

更新(2023 年 8 月 25 日)一年多后,答案似乎是肯定的!感谢康纳下面的建议,看来您可以用作ANYCOMPATIBLE参数类型,并且pg_typeof现在可以正常工作而不会出现错误。这是修改后的函数:

CREATE FUNCTION polymorphic_input2(arg_rec ANYCOMPATIBLE) RETURNS TEXT LANGUAGE plpgsql AS
$plpgsql$
BEGIN
    IF pg_typeof(arg_rec)::text = 'tables' THEN
        RETURN (arg_rec::information_schema.tables).table_name;
    ELSIF pg_typeof(arg_rec)::text = 'columns' THEN
        RETURN (arg_rec::information_schema.columns).column_name;
    ELSE
        RETURN 'unknown: ' || pg_typeof(arg_rec);
    END IF;
END;
$plpgsql$;
Run Code Online (Sandbox Code Playgroud)

您现在可以调用上面的方法,向其传递不同的记录类型,如下所示:

/* The query below will return the name of the table which is "pg_type" */
SELECT polymorphic_input2((SELECT t FROM information_schema.tables t WHERE table_name = 'pg_type' LIMIT 1));

/* The query below will return the name of the column which is "objsubid" */
SELECT polymorphic_input2((SELECT t FROM information_schema.columns t WHERE t.column_name = 'objsubid' LIMIT 1));
Run Code Online (Sandbox Code Playgroud)

Con*_*nor 2

不确定这是否回答了问题,但如果您ANYCOMPATIBLE在函数定义中使用类型,则可以避免一些错误:


DROP FUNCTION polymorphic_input(arg_rec ANYCOMPATIBLE);
CREATE FUNCTION polymorphic_input(arg_rec ANYCOMPATIBLE) RETURNS TEXT
  LANGUAGE plpgsql AS
$plpgsql$
BEGIN
  IF pg_typeof(arg_rec) IN ('integer', 'numeric') THEN
    RETURN 'number';
  ELSIF pg_typeof(arg_rec)::TEXT IN ('date') THEN
    RETURN 'date';
  ELSE
    RETURN 'unknown';
  END IF;
END;
$plpgsql$;

SELECT polymorphic_input(5); -- number
SELECT polymorphic_input(date('2022-01-01')); -- date
SELECT polymorphic_input('2022-01-01'); -- unknown
Run Code Online (Sandbox Code Playgroud)