选择 打开 改范围 完整检索页

pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。

受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
测试与开发版本: 19 / devel
不受支持的版本: 13 / 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0
历史版本PostgreSQL 9.1 已于 2016 年 10 月结束社区维护,本页译文保留供仍在使用旧版本的读者参考。新系统请看当前版本

39.10. PL/pgSQL 内部机制 #

本节讨论一些实现细节,了解这些细节对 PL/pgSQL 用户通常很重要。

39.10.1. 变量替换 #

PL/pgSQL函数中,SQL 语句和表达式可以引用函数的变量和参数。在内部,PL/pgSQL会用查询参数替换这些引用。只有在语法上允许参数或列引用的位置才会进行参数替换。作为一个极端例子,来看下面这种不良编程风格:

INSERT INTO foo (foo) VALUES (foo);

第一次出现的foo在语法上必须是表名,所以不会被替换,即使函数有一个变量也叫foo。第二次出现的位置必须是该表的列名,所以也不会被替换。只有第三次出现的位置才可能是对函数变量的引用。

注意

PostgreSQL 9.0 以前的版本会尝试在这三个位置都进行变量替换,从而导致语法错误。

由于变量名在语法上与表列名没有区别,所以在同时引用表的语句中就可能产生歧义:某个给定名称到底是指表列,还是变量?把前面的示例改成下面这样:

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将是对表列的一种无歧义的引用。要创建对一个变量的无歧义引用,在一个被标记的块中声明它并且使用块的标签(见第 39.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应该将有歧义的引用作为变量(这与PL/pgSQLPostgreSQL 9.0 之前的行为兼容)或表列(这与某些其他系统兼容,例如Oracle)解决。

要在系统范围内改变这种行为,将配置参数 plpgsql.variable_conflict 设置为 erroruse_variable 或者 use_column(这里 error 是出厂设置)之一。这个参数会影响 PL/pgSQL 函数中语句的后续编译,但是 不会影响在当前会话中已经编译过的语句。要在 PL/pgSQL 装载之前设置该参数,必须先把 plpgsql 加入 postgresql.conf 中的 custom_variable_classes 列表。因为改变这个设置 能够导致 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命令中,curtimecomment以及id将引用该函数的变量和参数,不管users有没有这些名称的列。注意,我们不得不在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 及其变体的命令字符串中,不会发生变量替换。如果你需要向这种命令中插入变化的值,应在构造字符串值时完成,或者像 第 39.5.4 节 所说明的那样使用 USING

目前,变量替换只在SELECTINSERTUPDATEDELETE命令中生效,因为主 SQL 引擎只允许在这些命令中使用查询参数。若要在其他语句类型(统称为工具语句)中使用非常量名称或值,就必须把该工具语句构造为字符串,再用EXECUTE执行。

39.10.2. 计划缓存 #

在函数第一次被调用时(每个会话中都会如此),PL/pgSQL解释器会解析函数源文本,并生成一棵内部的二进制指令树。该指令树完整表示了PL/pgSQL语句结构,但函数中使用的各个SQL表达式和SQL命令并不会立即被分析。

函数中的每个表达式和SQL命令首次执行时,PL/pgSQL解释器都会创建一个预备好的执行计划(使用SPI管理器的SPI_prepareSPI_saveplan函数)。之后对该表达式或命令的访问会复用该预备计划。因此,一个包含大量可能需要执行计划的语句的条件代码函数,在数据库连接的生存期内只会准备和保存真正用到的那些计划。这可以显著减少为PL/pgSQL函数中的语句解析和生成执行计划所需的总时间。一个不利之处是,特定表达式或命令中的错误要到函数执行到相应部分时才会被发现。(琐碎的语法错误会在初始解析阶段被发现,但更深层的错误要到执行时才会被发现。)

如果查询所用任何表的架构发生改变,或者查询中用到的任何用户 定义函数被重新定义,已保存的计划将被自动重新规划。这使预备 计划的重用在大多数情况下是透明的,但也存在可能重用过期计划 的边角情况。例如,删除并重新创建一个用户定义操作符不会影响 已缓存的计划;如果该操作符的底层函数没有被改变,这些计划会 继续调用原来的底层函数。必要时,可以通过启动一个新的数据库 会话来刷新缓存。

由于 PL/pgSQL 以这种方式保存执行 计划,直接出现在 PL/pgSQL 函数中的 SQL 命令在每次 执行时必须引用相同的表和列;也就是说,不能在 SQL 命令中把 参数用作表名或列名。要绕过这一限制,可以使用 PL/pgSQLEXECUTE 语句构造动态命令 — 代价是 每次执行都要构造一个新的执行计划。

另一个要点是,预备计划是参数化的,允许 PL/pgSQL 变量的值在一次使用与下一次 使用之间变化,如前文详细讨论的那样。有时这意味着计划不如按 特定变量值生成的计划高效。举例来说,考虑

SELECT * INTO myrec FROM dictionary WHERE word LIKE search_term;

其中 search_term 是一个 PL/pgSQL 变量。该查询的缓存计划永远 不会使用 word 上的索引,因为规划器不能假定 LIKE 模式在运行时是左锚定的。要使用索引, 必须在规划查询时提供具体的常量 LIKE 模式。这是另一种情况,可以用 EXECUTE 强制为每次执行生成新计划。

记录变量的可变特性在这里还会带来另一个问题。当记录变量的字段被 用于表达式或语句中时,这些字段的数据类型不能在函数的不同调用 之间发生变化,因为每个表达式都会按照第一次执行到它时所看到的 数据类型来规划。必要时,可以用 EXECUTE 绕过这个问题。

如果同一个函数被用作多个表的触发器, PL/pgSQL 会针对每个这样的表独立地 准备并缓存计划 — 也就是说,缓存是按 触发器函数 + 表的组合建立的,而不是每个函数只有一个缓存。 这缓解了数据类型变化带来的部分问题;例如,即使不同表中名为 key 的列类型不同,一个触发器函数也仍然能够 成功使用它。

同样,具有多态参数类型的函数也会为它们已经被调用的每一种实参 类型组合都保留一个独立的缓存,这样数据类型差异不会导致意想不 到的失败。

计划缓存有时可能在对时间敏感的值的解释上产生令人惊讶的效果。 例如这两个函数做的事情就有区别:

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

and:

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 被规划时转换为一个常量,然后在该 会话的生命周期内被用于所有对 logfunc1 的调用。不用说,这不是程序员 想要的。

logfunc2 中, PostgreSQL 的主解析器不知道 'now' 应该变成什么类型,因此返回一个 text 类型的数据值,其中包含字符串 now。在随后给局部变量 curtime 赋值时, PL/pgSQL 解释器通过调用用于该转换 的 text_outtimestamp_in 函数,把这个字符串转换为 timestamp 类型。因此,计算得到的时间戳会按程序员 预期在每次执行时更新。

提交更正

译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。