Skip to content

43.11.PL/pgSQL的底层#

43.11.1. 变量替换
43.11.2. 计划缓存

本节讨论一些实现细节,这些细节对于PL/pgSQL用户来说通常很重要。

43.11.1. 变量替换#

在PL/pgSQL函数中的 SQL 语句和表达式可以引用函数的变量和参数。在后台,PL/pgSQL用查询参数替换此类引用。查询参数仅在语法允许的情况下替换。极端情况下,请考虑以下糟糕的编程风格示例

INSERT INTO foo (foo) VALUES (foo(foo));

foo的第一次出现语法上必须是表名,因此不会替换,即使函数有一个名为foo的变量。第二次出现必须是该表的列名,因此也不会替换。同样,第三次出现必须是函数名,因此也不会替换。只有最后一次出现是PL/pgSQL函数变量引用的候选对象。

理解这一点的另一种方法是,变量替换只能将数据值插入 SQL 命令;它不能动态更改命令引用的数据库对象。(如果您想这样做,您必须动态构建一个命令字符串,如第 43.5.4 节中所述。)

由于变量的名称在语法上与表列的名称没有区别,因此在也引用表的语句中可能会出现歧义:给定的名称是指表列还是变量?我们把之前的示例更改为

INSERT INTO dest (col) SELECT foo + bar FROM src;

此处,destsrc必须是表名,col必须是dest的列,但foobar可能是函数的变量或src的列。

默认情况下,如果 SQL 语句中的名称可以引用变量或表列,PL/pgSQL将报告错误。您可以通过重命名变量或列、限定不明确的引用或告诉PL/pgSQL优先选择哪种解释来解决此类问题。

最简单的解决方案是重命名变量或列。一个常见的编码规则是为PL/pgSQL变量使用与列名不同的命名约定。例如,如果你始终将函数变量命名为v_*something*,而你的列名均不以v_开头,则不会发生冲突。

或者,你可以限定模棱两可的引用以使其清晰。在上例中,src.foo将是对表列的明确引用。要创建对变量的明确引用,请在标记的块中声明它并使用块的标签(请参见第 43.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在PostgreSQL9.0 之前的行为兼容),或解析为表列(与Oracle等其他一些系统兼容)。

要在系统范围内更改此行为,请将配置参数plpgsql.variable_conflict设置为erroruse_variableuse_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命令中,curtimecommentid将引用函数的变量和参数,无论users是否具有这些名称的列。请注意,我们必须在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,如第 43.5.4 节所示。

变量替换当前仅适用于SELECTINSERTUPDATEDELETE以及包含其中一个命令的命令(例如EXPLAINCREATE TABLE ... AS SELECT),因为主 SQL 引擎仅允许在这些命令中使用查询参数。要在其他语句类型(通常称为实用程序语句)中使用非常量名称或值,您必须将实用程序语句构造为字符串并EXECUTE它。

43.11.2. 计划缓存#

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/pgSQLEXECUTE语句构造动态命令——代价是在每次执行时执行新的解析分析并构建新的执行计划。

记录变量的可变特性在此连接中提出了另一个问题。当记录变量的字段用于表达式或语句中时,字段的数据类型不能从函数的一次调用更改为下一次调用,因为每个表达式都将使用在首次到达表达式时存在的数据类型进行分析。必要时,可以使用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'应变为哪种类型,因此它返回包含字符串now的类型为text的数据值。在随后的对局部变量curtime的赋值期间,PL/pgSQL解释器通过调用textouttimestamp_in函数进行转换,将此字符串强制转换为timestamp类型。因此,计算出的时间戳会在每次执行时更新,就像程序员期望的那样。尽管这恰好按预期工作,但效率不高,因此仍然建议使用now()函数。