2025年9月25日: PostgreSQL 18 发布!
支持的版本: 当前 (18) / 17 / 16 / 15 / 14 / 13
开发版本: devel
不支持的版本: 12 / 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;

在这里,即使 src 中有一个名为 foo 的列,block.foo 也指的是变量。函数参数以及像 FOUND 这样的特殊变量,可以通过函数名来限定,因为它们隐式地声明在一个以函数名标记的外部块中。

有时,修复大量 PL/pgSQL 代码中的所有歧义引用是不切实际的。在这种情况下,您可以指定 PL/pgSQL 应将歧义引用解析为变量(这与 PL/pgSQLPostgreSQL 9.0 之前的行为兼容),或解析为表列(这与其他系统如 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 是否具有这些名称的列。请注意,我们在 WHERE 子句中必须限定对 users.id 的引用,以使其引用表列。但是,我们不需要限定对 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管理器(manager)的 SPI_prepare 函数来创建准备好的语句。后续访问该表达式或命令时会重用准备好的语句。因此,具有很少访问的代码路径的函数将永远不会承担分析当前会话中从不执行的命令的开销。一个缺点是特定表达式或命令中的错误直到执行到该部分时才能被检测到。(细微的语法错误将在初始解析过程中被检测到,但更深层次的问题直到执行时才能被检测到。)

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

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

记录变量的可变性在这个方面带来了另一个问题。当记录变量的字段用在表达式或语句中时,字段的数据类型从函数的一次调用到下一次调用都不能改变,因为每个表达式都将使用在表达式首次到达时存在的数据类型进行分析。必要时可以使用 EXECUTE 来解决此问题。

如果同一个函数用作多个表的触发器,PL/pgSQL 会为每个这样的表独立地准备和缓存语句 — 也就是说,对于每个触发器函数和表的组合都有一个缓存,而不仅仅是每个函数。这缓解了数据类型变化带来的一些问题;例如,即使 key 列在不同表中具有不同的类型,触发器函数也能够成功地处理名为 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 的目标列就是该类型。因此,当 INSERT 被解析时,'now' 将被转换为一个 timestamp 常量,然后在会话的整个生命周期内用于 logfunc1 的所有调用。不用说,这不是程序员想要的。更好的方法是使用 now()current_timestamp 函数。

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

提交更正

如果您在文档中发现任何不正确、与您对特定功能的体验不符或需要进一步澄清的内容,请使用 此表单 报告文档问题。