2024 年 9 月 26 日: PostgreSQL 17 发布!
支持的版本:当前 (17) / 16 / 15 / 14 / 13 / 12
开发版本:devel
不支持的版本:11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3

41.11. PL/pgSQL 内部机制 #

本节讨论一些实现细节,这些细节对于 PL/pgSQL 用户来说经常很重要。

41.11.1. 变量替换 #

PL/pgSQL 函数内的 SQL 语句和表达式可以引用函数的变量和参数。在幕后,PL/pgSQL 会将查询参数替换为这些引用。查询参数仅在语法上允许的位置替换。作为一个极端的例子,考虑一下这个编程风格不佳的例子:

INSERT INTO foo (foo) VALUES (foo(foo));

foo 的第一次出现必须在语法上是表名,所以它不会被替换,即使函数有一个名为 foo 的变量。第二次出现必须是该表中列的名称,所以它也不会被替换。同样,第三次出现必须是函数名,所以它也不会被替换。只有最后一次出现才是 PL/pgSQL 函数的变量引用的候选。

另一种理解方式是,变量替换只能将数据值插入 SQL 命令中;它不能动态更改命令引用的数据库对象。(如果您想这样做,您必须动态构建命令字符串,如 第 41.5.4 节 所述。)

由于变量的名称在语法上与表列的名称没有什么区别,因此在也引用表的语句中可能会出现歧义:给定的名称是指表列还是变量?让我们将之前的例子改为:

INSERT INTO dest (col) SELECT foo + bar FROM src;

这里,destsrc 必须是表名,而 col 必须是 dest 的列,但 foobar 可以合理地认为是函数的变量或 src 的列。

默认情况下,如果 SQL 语句中的名称可能引用变量或表列,PL/pgSQL 将报告错误。您可以通过重命名变量或列,或限定模棱两可的引用,或告诉 PL/pgSQL 首选哪种解释来解决此问题。

最简单的解决方案是重命名变量或列。一个常见的编码规则是为 PL/pgSQL 变量使用与您为列名使用的命名约定不同的命名约定。例如,如果您始终将函数变量命名为 v_something,而您的列名都不以 v_ 开头,就不会发生冲突。

或者,您可以限定模棱两可的引用以使它们明确。在上面的例子中,src.foo 将是表列的明确引用。要创建对变量的明确引用,请在带标签的块中声明它并使用块的标签(参见 第 41.2 节)。例如,

<<block>>
DECLARE
    foo int;
BEGIN
    foo := ...;
    INSERT INTO dest (col) SELECT block.foo + bar FROM src;

这里 block.foo 指的是变量,即使 src 中存在列 foo。函数参数以及 FOUND 等特殊变量可以使用函数名限定,因为它们在用函数名标记的外部块中隐式声明。

有时,在大量的 PL/pgSQL 代码中修复所有模棱两可的引用是不切实际的。在这种情况下,您可以指定 PL/pgSQL 应该将模棱两可的引用解析为变量(这与 PostgreSQL 9.0 之前 PL/pgSQL 的行为兼容),或解析为表列(这与其他一些系统(如 Oracle)兼容)。

要在系统范围内更改此行为,请将配置参数 plpgsql.variable_conflict 设置为 erroruse_variableuse_column 之一(其中 error 是工厂默认值)。此参数影响 PL/pgSQL 函数中语句的后续编译,但不影响当前会话中已编译的语句。由于更改此设置可能会导致 PL/pgSQL 函数行为的意外更改,因此只能由超级用户更改。

您也可以在每个函数的基础上设置行为,方法是在函数文本的开头插入以下特殊命令之一:

#variable_conflict error
#variable_conflict use_variable
#variable_conflict use_column

这些命令仅影响它们所在的函数,并覆盖 plpgsql.variable_conflict 的设置。例如,

CREATE FUNCTION stamp_user(id int, comment text) RETURNS void AS $$
    #variable_conflict use_variable
    DECLARE
        curtime timestamp := now();
    BEGIN
        UPDATE users SET last_modified = curtime, comment = comment
          WHERE users.id = id;
    END;
$$ LANGUAGE plpgsql;

UPDATE 命令中,curtimecommentid 将引用函数的变量和参数,无论 users 是否具有这些名称的列。请注意,我们必须限定对 users.id 的引用,使其在 WHERE 子句中引用表列。但是,我们不必限定对 comment 的引用,使其成为 UPDATE 列表中的目标,因为在语法上,它必须是 users 的列。我们可以这样编写同一个函数,而不依赖于 variable_conflict 设置:

CREATE FUNCTION stamp_user(id int, comment text) RETURNS void AS $$
    <<fn>>
    DECLARE
        curtime timestamp := now();
    BEGIN
        UPDATE users SET last_modified = fn.curtime, comment = stamp_user.comment
          WHERE users.id = stamp_user.id;
    END;
$$ LANGUAGE plpgsql;

变量替换不会发生在提供给 EXECUTE 或其变体的命令字符串中。如果您需要将不同的值插入这样的命令,请在构造字符串值时进行,或使用 USING,如 第 41.5.4 节 所示。

变量替换目前仅在 SELECTINSERTUPDATEDELETE 以及包含其中之一的命令(如 EXPLAINCREATE TABLE ... AS SELECT)中起作用,因为主 SQL 引擎仅在这些命令中允许查询参数。要在其他语句类型(通常称为实用程序语句)中使用非常量名称或值,您必须将实用程序语句构造为字符串并 EXECUTE 它。

41.11.2. 计划缓存 #

PL/pgSQL 解释器会解析函数的源文本并在函数第一次调用时(在每个会话中)生成一个内部二进制指令树。指令树完全转换 PL/pgSQL 语句结构,但函数中使用的单个SQL表达式和SQL命令不会立即转换。

随着每个表达式和SQL命令在函数中第一次执行,PL/pgSQL 解释器会解析并分析命令以使用SPI管理器的 SPI_prepare 函数创建准备好的语句。后续访问该表达式或命令会重用准备好的语句。因此,具有很少访问的条件代码路径的函数永远不会产生分析从未在当前会话中执行的命令的开销。缺点是,直到执行函数的该部分时,才能检测到特定表达式或命令中的错误。(在初始解析阶段会检测到微不足道的语法错误,但任何更深层的错误都不会在执行之前被检测到。)

PL/pgSQL(更准确地说是 SPI 管理器)还可以尝试缓存与任何特定准备好的语句关联的执行计划。如果未使用缓存的计划,则每次访问语句时都会生成一个新的执行计划,并且当前参数值(即 PL/pgSQL 变量值)可用于优化所选计划。如果语句没有参数,或多次执行,则 SPI 管理器会考虑创建一个通用计划,该计划不依赖于特定参数值,并将其缓存以供重用。通常,只有当执行计划对其中引用的 PL/pgSQL 变量的值不敏感时才会发生这种情况。如果是,则每次生成一个计划是一个净收益。有关准备好的语句行为的更多信息,请参见 PREPARE

由于 PL/pgSQL 以这种方式保存准备好的语句,有时还保存执行计划,因此直接出现在 PL/pgSQL 函数中的 SQL 命令必须在每次执行时都引用相同的表和列;也就是说,您不能在 SQL 命令中使用参数作为表或列的名称。要绕过此限制,您可以使用 PL/pgSQL EXECUTE 语句构造动态命令——代价是每次执行都要执行新的解析分析并构造新的执行计划。

记录变量的可变性质在此方面提出了另一个问题。当记录变量的字段用于表达式或语句时,字段的数据类型在函数的每次调用之间不得更改,因为每个表达式将使用第一次到达表达式时存在的数据类型进行分析。如果需要,可以使用 EXECUTE 绕过此问题。

如果同一个函数被用作多个表的触发器,PL/pgSQL 会为每个表独立地准备和缓存语句 - 也就是说,每个触发器函数和表组合都有一个缓存,而不仅仅是每个函数。这缓解了一些数据类型不同的问题;例如,一个触发器函数能够成功地处理一个名为key的列,即使它恰好在不同的表中具有不同的类型。

同样,具有多态参数类型的函数会为它们被调用的每个实际参数类型组合创建一个单独的语句缓存,这样数据类型差异不会导致意外的失败。

语句缓存有时会对时间敏感值的解释产生意想不到的影响。例如,这两个函数的功能存在差异

CREATE FUNCTION logfunc1(logtxt text) RETURNS void AS $$
    BEGIN
        INSERT INTO logtable VALUES (logtxt, 'now');
    END;
$$ LANGUAGE plpgsql;

CREATE FUNCTION logfunc2(logtxt text) RETURNS void AS $$
    DECLARE
        curtime timestamp;
    BEGIN
        curtime := 'now';
        INSERT INTO logtable VALUES (logtxt, curtime);
    END;
$$ LANGUAGE plpgsql;

logfunc1的情况下,PostgreSQL 主解析器在分析INSERT时知道字符串'now'应该被解释为timestamp,因为logtable的目标列是这种类型。因此,'now'将在分析INSERT时被转换为timestamp常量,然后在会话生命周期内所有调用logfunc1时使用。不用说,这不是程序员想要的。一个更好的主意是使用now()current_timestamp函数。

logfunc2的情况下,PostgreSQL 主解析器不知道'now'应该变成什么类型,因此它返回一个包含字符串nowtext类型的数据值。在随后对局部变量curtime的赋值中,PL/pgSQL 解释器通过调用textouttimestamp_in函数来将此字符串转换为timestamp类型进行转换。因此,计算出的时间戳在每次执行时都会更新,如程序员所预期的那样。即使这碰巧按预期工作,但它效率并不高,因此使用now()函数仍然是更好的选择。

提交更正

如果您在文档中看到任何不正确的地方,与您对特定功能的体验不符或需要进一步澄清,请使用 此表格 报告文档问题。