# 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));

第一次出现在语法上必须是表名,因此即使函数有一个名为。第二次出现的必须是该表中某列的名称,因此也不会被替换。同样,第三次出现必须是函数名,因此它也不会被替换。只有最后一个匹配项是PL/pgSQL函数变量的引用。

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

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

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

在这里目的地src必须是表名,并且上校一定是一列目的地但是酒吧可以是函数的变量,也可以是函数的列src.

默认情况下,如果SQL语句中的名称可能引用变量或表列,PL/pgSQL将报告错误。您可以通过重命名变量或列,或者限定不明确的引用,或者告诉PL/pgSQL更喜欢哪种解释,来解决这个问题。

最简单的解决方案是重命名变量或列。一个常见的编码规则是对PL/pgSQL变量使用不同于列名的命名约定。例如,如果您始终命名函数变量五_*某物*而你的列名都不是以五_,不会发生冲突。

或者,您可以限定不明确的引用以使其清晰。在上面的例子中,src。福将是对表列的明确引用。要创建对变量的明确引用,请在带标签的块中声明它,并使用块的标签(请参见第43.2节).例如,

<<block>>
DECLARE
    foo int;
BEGIN
    foo := ...;
    INSERT INTO dest (col) SELECT block.foo + bar FROM src;

在这里块福表示变量,即使有列在里面src.函数参数,以及特殊变量,如建立,可以通过函数名限定,因为它们是在用函数名标记的外部块中隐式声明的。

有时,修复大量PL/pgSQL代码中所有不明确的引用是不切实际的。在这种情况下,您可以指定PL/pgSQL应该将不明确的引用解析为变量(与PostgreSQL 9.0之前的PL/pgSQL行为兼容),或表列(与Oracle等其他系统兼容)。

要在系统范围内更改此行为,请设置配置参数plpgsql。变量冲突给其中一个错误, 使用_变量使用_列(在哪里错误是出厂默认设置)。此参数影响PL/pgSQL函数中语句的后续编译,但不影响当前会话中已编译的语句。由于更改此设置可能会导致PL/pgSQL函数的行为发生意外更改,因此只能由超级用户更改。

还可以通过在函数文本开头插入以下特殊命令之一,逐个函数设置行为:

#variable_conflict error
#variable_conflict use_variable
#variable_conflict use_column

这些命令只影响写入它们的函数,并覆盖plpgsql。变量冲突例如

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;

使现代化命令短时间, 议论身份证件将引用函数的变量和参数,无论用户有这些名字的列。注意,我们必须限定对的引用用户。身份证件哪里子句使其引用表列。但我们不必限定提及议论作为未来的目标使现代化列表,因为从语法上讲,它必须是用户.我们可以编写相同的函数,而不依赖于变量冲突以这种方式设置:

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;

变量替换不会发生在给定给处决或者它的一个变种。如果需要在这样的命令中插入可变值,可以在构造字符串值的过程中插入,也可以使用使用,如中所示第43.5.4节.

变量替换目前只适用于选择, 插入, 使现代化, 删去,以及包含其中一个的命令(例如解释创建表格。。。作为选择),因为主SQL引擎只允许在这些命令中使用查询参数。要在其他语句类型(通常称为实用程序语句)中使用非常量名称或值,必须将实用程序语句构造为字符串和处决信息技术

# 43.11.2.计划缓存

PL/pgSQL解释器解析函数的源文本,并在第一次调用函数时(在每个会话中)生成一个内部二进制指令树。指令树完全转换PL/pgSQL语句结构,但函数中使用的单个SQL表达式和SQL命令不会立即转换。

在函数中首先执行每个表达式和SQL命令时,PL/pgSQL解释器使用SPI管理器的准备好了吗作用对该表达式或命令的后续访问将重用准备好的语句。因此,具有很少访问的条件代码路径的函数永远不会产生分析当前会话中从未执行过的命令的开销。一个缺点是,在执行函数的该部分之前,无法检测特定表达式或命令中的错误。(在初始解析过程中会检测到轻微的语法错误,但在执行之前不会检测到更深层次的语法错误。)

PL/pgSQL(或者更准确地说,SPI管理器)还可以尝试缓存与任何特定准备语句关联的执行计划。如果未使用缓存的计划,则每次访问语句时都会生成一个新的执行计划,并且可以使用当前参数值(即PL/pgSQL变量值)来优化所选计划。如果语句没有参数,或多次执行,SPI管理器将考虑创建一个通用的不依赖于特定参数值的计划,并将其缓存以供重用。通常,只有当执行计划对其中引用的PL/pgSQL变量的值不太敏感时,才会发生这种情况。如果是的话,每次制定一个计划就是一个净胜利。看见准备有关准备好的语句的行为的更多信息。

由于PL/pgSQL以这种方式保存准备好的语句,有时还保存执行计划,因此直接出现在PL/pgSQL函数中的SQL命令在每次执行时都必须引用相同的表和列;也就是说,不能在SQL命令中使用参数作为表或列的名称。为了绕过这个限制,可以使用PL/pgSQL构造动态命令处决语句——代价是执行新的解析分析,并在每次执行时构建新的执行计划。

记录变量的易变性在这方面提出了另一个问题。在表达式或语句中使用记录变量的字段时,字段的数据类型不得从函数的一次调用更改为下一次调用,因为每个表达式都将使用首次到达表达式时出现的数据类型进行分析。处决必要时可以用来解决这个问题。

如果同一个函数用作多个表的触发器,PL/pgSQL会为每个这样的表独立地准备和缓存语句——也就是说,每个触发器函数和表组合都有一个缓存,而不仅仅是每个函数。这缓解了不同数据类型的一些问题;例如,触发器函数将能够成功地处理名为钥匙即使它碰巧在不同的表中有不同的类型。

同样,具有多态参数类型的函数对于它们被调用的实际参数类型的每个组合都有一个单独的语句缓存,这样数据类型差异就不会导致意外的失败。

语句缓存有时会对时间敏感值的解释产生令人惊讶的影响。例如,这两个函数的功能不同:

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主解析器在分析插入那是绳子吗“现在”应该解释为时间戳,因为日志表是那种类型的。因此“现在”将被转换为时间戳插入分析,然后用于logfunc1在会话的生命周期内。不用说,这不是程序员想要的。更好的办法是使用现在()当前时间戳作用

logfunc2,PostgreSQL主解析器不知道是什么类型“现在”应该变成,因此它返回类型为的数据值文本包含字符串现在.在随后分配给局部变量的过程中短时间,PL/pgSQL解释器将该字符串转换为时间戳通过调用发短信时间戳_in用于转换的函数。因此,计算出的时间戳在每次执行时都会按照程序员的预期进行更新。尽管这恰好如预期的那样工作,但它的效率并不太高,所以使用现在()功能仍然是一个更好的主意。