本节讨论一些对 PL/pgSQL 用户而言 经常很重要的实现细节。
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_,
而列名都不以 somethingv_ 开头,
就不会发生冲突。
另一种办法是通过限定有歧义的引用来使其明确。
在上面的示例中,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 它。
在函数第一次被调用时(每个会话内), 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() 函数仍然是更好的主意。