PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

41.11. PL/pgSQL 底层运行机制 #

41.11.1. 变量替换原理
41.11.2. 执行计划缓存

本节讨论一些对 PL/pgSQL 用户而言 经常很重要的实现细节。

41.11.1. 变量替换原理 #

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

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

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

从另一个角度理解:变量替换只能向 SQL 命令中插入数据值; 它无法动态改变该命令所引用的数据库对象。 (如果你想做到这一点,必须动态地构建命令字符串, 如 Section 41.5.4 中所解释的。)

由于变量名在语法上与表列名毫无区别, 在同时引用表的语句中可能产生歧义: 给定的名字到底指表列,还是指变量? 把前面的示例改成

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

这里,dest 和 src 必须是表名, col 必须是 dest 的一列, 而 foo 和 bar 既可能是函数的变量,也可能是 src 的列。

默认情况下,如果 SQL 语句中的某个名字既可能指变量 又可能指表列,PL/pgSQL 将报告一个错误。 你可以通过重命名变量或列、限定这个有歧义的引用, 或者告诉 PL/pgSQL 优先采用哪种解释来解决这个问题。

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

另一种办法是通过限定有歧义的引用来使其明确。 在上面的示例中,src.foo 就是对该表列的 无歧义引用。要创建对变量的无歧义引用, 可以把它声明在一个带标签的块中,并使用该块的标签 (见 Section 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 设置为 error、use_variable 或 use_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 命令中,无论 users 是否具有同名的列,curtime、comment 和 id 都将引用函数的变量和参数。 注意,我们必须在 WHERE 子句中限定对 users.id 的引用,使其指向表列。 但在 UPDATE 列表中作为目标出现的 comment 则不需要限定, 因为在语法上它必须是 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, 如 Section 41.5.4 中所示。

变量替换目前只对 SELECT、 INSERT、UPDATE、 DELETE、MERGE 以及包含其中之一的命令(例如 EXPLAIN 和 CREATE 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 语句构造动态命令 —— 代价是每次执行都要进行新的解析分析并构造新的执行计划。

记录变量的可变性质在这方面带来了另一个问题。 当记录变量的字段被用在表达式或语句中时, 这些字段的数据类型在函数的多次调用之间不得变化, 因为每个表达式都会使用该表达式第一次被到达时所 present 的数据类型进行分析。必要时可以使用 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' 应当变成什么类型, 因此它会返回一个包含字符串 now 的 text 类型的数据值。在随后对局部变量 curtime 的赋值过程中, PL/pgSQL 解释器通过调用 textout 和 timestamp_in 函数完成转换,把这个字符串转换为 timestamp 类型。 这样,计算出的时间戳就会像程序员期望的那样在每次执行时被更新。 尽管这恰好能按预期工作,但效率并不高, 所以使用 now() 函数仍然是更好的主意。