pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
本节讨论一些实现细节,了解这些细节对 PL/pgSQL 用户通常很重要。
在 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;
这里,dest 和 src 必须是表名, col 也必须是 dest 的一列,但 foo 和 bar 既可能是该函数的变量,也可能是 src 的列。
默认情况下,如果 SQL 语句中的名称既可能指变量,也可能指表列, PL/pgSQL 就会报告错误。可以通过重命名 变量或列、限定有歧义的引用,或者告诉 PL/pgSQL 优先采用哪种解释来解决这类 问题。
最简单的解决方案是重命名变量或列。一种常用的编码规则是为 PL/pgSQL 变量使用一种不同于列名的命名 习惯。例如,如果你将函数变量统一地命名为 v_,而你的列名不会 开始于 somethingv_,就不会发生冲突。
另外你可以限定有歧义的引用让它们变清晰。在上面的示例中, 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/pgSQL 在 PostgreSQL 9.0 之前的行为兼容)或 表列(这与某些其他系统兼容,例如 Oracle)解决。
要在系统范围内改变这种行为,将配置参数 plpgsql.variable_conflict 设置为 error、use_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 命令中, curtime、comment 和 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 或其变体之一的命令字符串中不会发生 变量替换。如果需要把变化的值插入到这样的命令中,应在构造字符 串值时完成,或者使用 USING,如 第 39.5.4 节 所示。
目前,变量替换只在 SELECT、 INSERT、 UPDATE 和 DELETE 命令中生效,因为主 SQL 引擎只允许在 这些命令中使用查询参数。若要在其他语句类型(统称为工具语句) 中使用非常量名称或值,就必须把该工具语句构造为字符串,再用 EXECUTE 执行。
在函数第一次被调用时(每个会话中都会如此), PL/pgSQL 解释器会解析函数源文本, 并生成一棵内部的二进制指令树。该指令树完整表示了 PL/pgSQL 语句结构,但函数中使用的 各个 SQL 表达式和 SQL 命令 并不会被立即翻译。
当每个表达式和 SQL 命令在函数中第一次被执行时, PL/pgSQL 解释器会创建一个预备执行 计划(使用 SPI 管理器的 SPI_prepare 和 SPI_saveplan 函数)。 之后对该表达式或命令的访问会重用该预备计划。 因此,一个包含条件代码、其中有大量可能需要执行计划的语句的 函数,只会准备并保存那些在数据库连接的生命周期内真正用到的 计划。这可以大幅减少为一个 PL/pgSQL 函数中的语句解析并生成 执行计划所需的总时间。一个缺点是,特定表达式或命令中的错误 只有在执行到函数的该部分时才能被发现。(琐碎的语法错误会在 初始解析阶段被发现,但更深层的问题要到执行时才会被发现。)
如果查询所用任何表的架构发生改变,或者查询中用到的任何用户 定义函数被重新定义,已保存的计划将被自动重新规划。这使预备 计划的重用在大多数情况下是透明的,但也存在可能重用过期计划 的边角情况。例如,删除并重新创建一个用户定义操作符不会影响 已缓存的计划;如果该操作符的底层函数没有被改变,这些计划会 继续调用原来的底层函数。必要时,可以通过启动一个新的数据库 会话来刷新缓存。
由于 PL/pgSQL 以这种方式保存执行 计划,直接出现在 PL/pgSQL 函数中的 SQL 命令在每次 执行时必须引用相同的表和列;也就是说,不能在 SQL 命令中把 参数用作表名或列名。要绕过这一限制,可以使用 PL/pgSQL 的 EXECUTE 语句构造动态命令 — 代价是 每次执行都要构造一个新的执行计划。
另一个要点是,预备计划是参数化的,允许 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_out 和 timestamp_in 函数,把这个字符串转换为 timestamp 类型。因此,计算得到的时间戳会按程序员 预期在每次执行时更新。
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。