受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发版本: devel

41.11. PL/pgSQL 内部机制 #

这一节讨论了一些PL/pgSQL用户应该知道的一些重要的实现细节。

41.11.1. 变量替换 #

PL/pgSQL 函数中的 SQL 语句和表达式可以引用该函数的变量和参数。在幕后,PL/pgSQL 会把这类引用替换为查询参数。只有在语法上允许使用查询参数的位置,才会发生这种替换。作为一个极端示例,考虑下面这个编程风格很差的例子:

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

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

换一种说法,变量替换只能把数据值插入到 SQL 命令中;它不能动态改变命令所引用的数据库对象。(如果你想这样做,就必须像 Section 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将是对表列的一种无歧义的引用。要创建对一个变量的无歧义引用,在一个被标记的块中声明它并且使用块的标签(见Section 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应该将有歧义的引用作为变量(这与PL/pgSQLPostgreSQL 9.0 之前的行为兼容)或表列(这与某些其他系统兼容,例如Oracle)解决。

要在系统范围内改变这种行为,将配置参数plpgsql.variable_conflict设置为erroruse_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命令中,curtimecomment以及id将引用该函数的变量和参数,不管users有没有这些名称的列。注意,我们不得不在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 及其变体的命令字符串中,不会发生变量替换。如果你需要向这种命令中插入变化的值,应在构造字符串值时完成,或者像 Section 41.5.4 所说明的那样使用 USING

目前,变量替换只在 SELECTINSERTUPDATEDELETEMERGE 以及包含其中之一的命令中生效(例如 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中,PostgreSQL的主解析器在分析INSERT时就知道字符串'now'应该被解释为timestamp,因为logtable的目标列是这种类型。因此,在INSERT被分析时'now'将被转换为一个timestamp常量,并且在该会话的生命周期内被用于所有对logfunc1的调用。不用说,这不是程序员想要的。一个更好的主意是使用now()current_timestamp函数。

logfunc2中,PostgreSQL的主解析器不知道应该把'now'解释成什么类型,因此返回一个text类型的数据值,其中包含字符串now。在确定对本地变量curtime的赋值时,PL/pgSQL解释器通过调用用于该转换的textouttimestamp_in函数,把这个字符串转换为timestamp类型。因此,计算得到的时间戳会按程序员预期在每次执行时更新。虽然这恰好能得到预期结果,但效率并不高,因此使用now()函数仍然是更好的主意。

提交更正

如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。