On this page
42.10. PL/pgSQL 深入研究
本节讨论一些实现细节,对于 PL/pgSQL 用户而言,这些细节通常很重要。
42 .10.1. 可变替代
PL/pgSQL 函数中的 SQL 语句和表达式可以引用函数的变量和参数。在后台,PL/pgSQL 将查询参数替换为此类引用。仅在语法上允许使用参数或列引用的位置替换参数。作为一个极端的情况,请考虑以下不良编程风格的示例:
INSERT INTO foo (foo) VALUES (foo);
从句法上讲,第一次出现的foo
必须是一个表名,因此即使该函数具有名为foo
的变量,它也不会被替换。第二次出现必须是表的列名,因此也不会被替换。只有第三次出现才可以引用该函数的变量。
Note
9.0 之前的 PostgreSQL 版本会在所有这三种情况下尝试替换该变量,从而导致语法错误。
由于变量的名称在语法上与表列的名称没有区别,因此在引用表的语句中可能存在歧义:给定名称是要引用表列还是变量?让我们将前面的示例更改为
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 42.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
的引用,以使其引用表列。但是我们不必将对comment
的引用限定为UPDATE
列表中的目标,因为从语法上讲,它必须是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 42.5.4所示。
当前,变量替换仅在SELECT
,INSERT
,UPDATE
和DELETE
命令中起作用,因为主 SQL 引擎仅在这些命令中允许查询参数。要在其他语句类型(通常称为 Util 语句)中使用非常量名称或值,必须将 Util 语句构造为字符串,并EXECUTE
。
42 .10.2. 计划缓存
PL/pgSQL 解释器解析函数的源文本,并在第一次调用函数时(在每个会话中)生成内部二进制指令树。指令树完全转换了 PL/pgSQL 语句结构,但是函数中使用的单个 SQL 表达式和 SQL 命令不会立即转换。
在函数中首先执行每个表达式和 SQL 命令时,PL/pgSQL 解释器将使用 SPIManagement 器的SPI_prepare
函数解析并分析该命令以创建准备好的语句。随后对该表达式或命令的访问将重用已准备好的语句。因此,具有很少访问的条件代码路径的函数将永远不会产生分析那些在当前会话中从未执行过的命令的开销。缺点是,直到执行中达到该功能的那一部分,才能检测到特定表达式或命令中的错误。 (在最初的解析过程中将检测到临时语法错误,但是直到执行之后,才会检测到更深的任何错误.)
PL/pgSQL(或更准确地说,SPIManagement 器)还可以尝试缓存与任何特定的准备好的语句关联的执行计划。如果未使用缓存的计划,则每次访问该语句时都会生成一个新的执行计划,并且可以使用当前参数值(即 PL/pgSQL 变量值)来优化所选计划。如果该语句没有参数,或者执行了多次,则 SPIManagement 器将考虑创建一个不依赖于特定参数值的“通用”计划,并将其缓存以供重用。通常,只有当执行计划对其中引用的 PL/pgSQL 变量的值不太敏感时,才会发生这种情况。如果是这样,那么每次生成计划都是一个净赢。有关预准备语句的行为的更多信息,请参见PREPARE。
因为 PL/pgSQL 用这种方法保存准备好的语句,有时还保存执行计划,所以直接出现在 PL/pgSQL 函数中的 SQL 命令在每次执行时都必须引用相同的表和列。也就是说,您不能在 SQL 命令中使用参数作为表或列的名称。要解决此限制,您可以使用 PL/pgSQL 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
的目标列就是该类型。因此,在分析INSERT
时,会将'now'
转换为timestamp
常量,然后在会话生存期内将其用于logfunc1
的所有调用中。不用说,这不是程序员想要的。一个更好的主意是使用now()
或current_timestamp
函数。
在logfunc2
的情况下,PostgreSQL 主解析器不知道'now'
应该变成哪种类型,因此它返回包含字符串now
的text
类型的数据值。在随后分配给局部变量curtime
的过程中,PL/pgSQL 解释器通过调用text_out
和timestamp_in
函数进行转换,将该字符串转换为timestamp
类型。因此,计算出的时间戳将按照程序员的期望在每次执行时进行更新。即使这可以按预期工作,但效率并不高,因此使用now()
函数仍然是一个更好的主意。