本节讨论了一些 PL/pgSQL 用户经常需要了解的实现细节。
在 PL/pgSQL 函数中的 SQL 语句和表达式可以引用该函数的变量和参数。在底层,PL/pgSQL 会将查询参数替换为这些引用。查询参数只会在语法允许的地方进行替换。作为一个极端的例子,请看这个糟糕的编程风格示例
INSERT INTO foo (foo) VALUES (foo(foo));
第一个出现的 foo
在语法上必须是一个表名,因此它不会被替换,即使该函数有一个名为 foo
的变量。第二个出现必须是该表的一个列的名称,因此它也不会被替换。同样,第三个出现必须是一个函数名,因此它也不会被替换。只有最后一次出现才是 PL/pgSQL 函数的变量引用的候选。
理解这一点的另一种方式是,变量替换只能将数据值插入到 SQL 命令中;它不能动态更改命令引用的数据库对象。(如果想这样做,必须动态构建命令字符串,如第 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_
,而没有任何列名以 something
v_
开头,则不会发生冲突。
或者,可以限定歧义引用以使其清晰。在上面的示例中,src.foo
将是对表列的明确引用。要创建对变量的明确引用,请在带标签的块中声明它并使用该块的标签(请参阅第 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
,如第 41.5.4 节所示。
变量替换目前仅在 SELECT
、INSERT
、UPDATE
、DELETE
以及包含其中之一的命令(例如 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
语句来构造动态命令 —— 但代价是每次执行都需要进行新的解析分析并构建新的执行计划。
记录变量的可变性也带来了一个相关的问题。当在表达式或语句中使用记录变量的字段时,字段的数据类型在函数的每次调用之间不能改变,因为每个表达式都将使用首次到达该表达式时存在的数据类型进行分析。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
的目标列是该类型。因此,当分析 INSERT
时,'now'
将被转换为 timestamp
常量,然后在会话的生命周期内用于 logfunc1
的所有调用。不用说,这不是程序员想要的。更好的方法是使用 now()
或 current_timestamp
函数。
在 logfunc2
的情况下,PostgreSQL 主解析器不知道 'now'
应该变成什么类型,因此它返回一个数据类型为 text
的值,其中包含字符串 now
。在随后赋值给局部变量 curtime
的过程中,PL/pgSQL 解释器通过调用 textout
和 timestamp_in
函数进行转换,将此字符串强制转换为 timestamp
类型。因此,计算出的时间戳会在每次执行时根据程序员的期望进行更新。尽管这恰好按预期工作,但效率不高,因此使用 now()
函数仍然是更好的选择。
如果您发现文档中有任何不正确的内容、与您对特定功能的体验不符或需要进一步澄清的地方,请使用此表单报告文档问题。