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 及其变体的命令字符串中,不会发生变量替换。如果你需要向这种命令中插入变化的值,应在构造字符串值时完成,或者像 第 39.5.4 节 所说明的那样使用 USING。
目前,变量替换只在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 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。