Redrock Postgres 搜索 英文
版本: 9.3 / 9.4 / 9.5 / 9.6 / 10 / 11 / 12 / 13 / 14 / 15 / 16 / 17

41.11. PL/pgSQL 的内部机制 #

41.11.1. 变量替换
41.11.2. 计划缓存

本节讨论一些对于 PL/pgSQL 用户而言通常很重要的实现细节。

41.11.1. 变量替换 #

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;

在此处,destsrc 必须是表名,并且 col 必须是 dest 的列,但 foobar 可能合理的作为函数的变量或 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设置为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是否具有这些名称的列。请注意,我们必须限定对users.id的引用在WHERE子句中,以使其引用表列。但是,我们不必将对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,如 第 41.5.4 节 中所述。

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

41.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/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 的情况下,分析 INSERTPostgreSQL 主要解析器知道字符串 'now' 应解释为 timestamp,因为 logtable 目标栏即为此类型。因此,分析 INSERT'now' 将转换为 timestamp 常量,然后在会话期间 logfunc1 的所有调用中使用。不用说,这不是程序员想要的。一个更好的方法是使用 now()current_timestamp 函数。

logfunc2 的情况下,PostgreSQL 主要解析器不知道 'now' 将变成什么类型,因此它返回一个包含字符串 now 的类型为 text 的数据值。在随后的赋值到局部变量 curtime 中,PL/pgSQL 解释器通过调用 textouttimestamp_in 函数进行转换,将此字符串转换为 timestamp 类型。因此,计算出的小时标记在每次执行时将按照程序员的期望更新。即使这恰好按照预期工作,但它并非特别有效,因此使用 now() 函数仍然会是一个好方法。