↑↓ 选择 ↵ 打开 ⌫ 改范围 完整检索页

pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。

受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
测试与开发版本: 19 / devel
不受支持的版本: 13 / 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3
历史版本PostgreSQL 8.3 已于 2013 年 2 月结束社区维护,本页译文保留供仍在使用旧版本的读者参考。新系统请看当前版本。

38.10. PL/pgSQL Under the Hood #

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

38.10.1. 变量替换 #

当 PL/pgSQL 为执行准备一条 SQL 语句或表达式时,语句或表达式中出现的任何 PL/pgSQL 变量名都会被替换为参数符号 $n。此后每当该语句或表达式被执行时,变量的当前值就会作为该参数的值提供。作为示例,考虑下面的函数:

CREATE FUNCTION logfunc(logtxt text) RETURNS void AS $$
    DECLARE
        curtime timestamp := now();
    BEGIN
        INSERT INTO logtable VALUES (logtxt, curtime);
    END;
$$ LANGUAGE plpgsql;

这条 INSERT 语句实际会被处理为

PREPARE statement_name(text, timestamp) AS
  INSERT INTO logtable VALUES ($1, $2);

然后在每次执行时用两个变量的当前实际值执行 EXECUTE。(注意:这里说的是主 SQL 引擎的 EXECUTE 命令,不是 PL/pgSQL 的 EXECUTE。)

替换机制会替换任何与已知变量名匹配的记号。这给粗心的人设置了各种陷阱。例如,把变量名起成与需要在函数内查询中引用的任何表名或列名相同,是很糟糕的做法,因为你以为是表名或列名的地方仍然会被替换。在上面的示例中,假定 logtable 有列名 logtxt 和 logtime,而我们试图把 INSERT 写成

        INSERT INTO logtable (logtxt, logtime) VALUES (logtxt, curtime);

这会被送进主 SQL 解析器,变成

        INSERT INTO logtable ($1, logtime) VALUES ($1, $2);

从而产生类似这样的语法错误:

ERROR:  syntax error at or near "$1"
LINE 1: INSERT INTO logtable ( $1 , logtime) VALUES ( $1 ,  $2 )
                               ^
QUERY:  INSERT INTO logtable ( $1 , logtime) VALUES ( $1 ,  $2 )
CONTEXT:  SQL statement in PL/PgSQL function "logfunc2" near line 5

这个示例相当容易诊断,因为它导致明显的语法错误。更麻烦的是替换在语法上被允许的情形,此时唯一的症状可能是函数行为异常。有一个案例,一位用户写下了这样的代码:

    DECLARE
        val text;
        search_key integer;
    BEGIN
        ...
        FOR val IN SELECT val FROM table WHERE key = search_key LOOP ...

并奇怪为什么他所有的表条目似乎都是 NULL。当然,这里实际发生的是查询变成了

        SELECT $1 FROM table WHERE key = $2

因而这对每一行来说,只是把 val 的当前值重新赋给它自身的一种昂贵方式。

避免这类陷阱的一条常用编码规则,是为 PL/pgSQL 变量使用不同于表名和列名的命名约定。例如,如果你所有变量都命名为 v_something,而你的表名或列名都不以 v_ 开头,那就相当安全。

另一种变通办法是对 SQL 实体使用限定(带点)名。例如,上面的示例可以安全地写成

        FOR val IN SELECT table.val FROM table WHERE key = search_key LOOP ...

因为 PL/pgSQL 不会用变量替换限定名的尾部成分。 但这种办法并非处处适用 — 例如,你不能限定 INSERT 列名列表中的名字。 还有一点,记录和行变量名会与限定名的第一部分匹配,因此限定的 SQL 名在某些情况下仍然有风险。 在这类情况下,选择一个不冲突的变量名是唯一的办法。

你可以使用的另一种技巧是,在声明变量的块上附加一个标签,然后在 SQL 命令中限定变量名(见 第 38.2 节)。例如:

    <<pl>>
    DECLARE
        val text;
    BEGIN
        ...
        UPDATE table SET col = pl.val WHERE ...

这本身并不能解决冲突问题,因为 SQL 命令中未限定的名字仍有被“错误”解释的风险。但它有助于澄清可能含糊的代码的意图。

给 EXECUTE 或其变体之一的命令字符串中不会发生变量替换。如果需要把变化的值插入这样的命令,应在构造字符串值时进行,如 第 38.5.4 节 中所示。

目前,变量替换只在 SELECT、 INSERT、UPDATE 和 DELETE 命令中生效,因为主 SQL 引擎只允许这些命令中出现参数符号。要在其他语句类型(统称为工具语句)中使用非常量名或值,必须把该工具语句构造为字符串并 EXECUTE 它。

38.10.2. 计划缓存 #

在函数第一次被调用时(每个会话中都会如此), PL/pgSQL 解释器会解析函数源文本, 并生成一棵内部的二进制指令树。该指令树完整表示了 PL/pgSQL 语句结构,但函数中使用的 各个 SQL 表达式和 SQL 命令 并不会被立即翻译。

当每个表达式和 SQL 命令在函数中第一次被执行时, PL/pgSQL 解释器会创建一个预备执行 计划(使用 SPI 管理器的 SPI_prepare 和 SPI_saveplan 函数)。 之后对该表达式或命令的访问会重用该预备计划。 因此,一个包含条件代码、其中有大量可能需要执行计划的语句的 函数,只会准备并保存那些在数据库连接的生命周期内真正用到的 计划。这可以大幅减少为一个 PL/pgSQL 函数中的语句解析并生成 执行计划所需的总时间。一个缺点是,特定表达式或命令中的错误 只有在执行到函数的该部分时才能被发现。(琐碎的语法错误会在 初始解析阶段被发现,但更深层的问题要到执行时才会被发现。)

一旦 PL/pgSQL 为函数中的某个特定命令生成了执行 计划,它就会在数据库连接的整个生命周期内重用该计划。这通常有 利于性能,但如果你动态地更改数据库架构,就可能引发一些问题。 例如:

CREATE FUNCTION populate() RETURNS integer AS $$
DECLARE
    -- declarations
BEGIN
    PERFORM my_function();
END;
$$ LANGUAGE plpgsql;

如果你执行上面的函数,它会在为 PERFORM 语句 生成的执行计划中引用 my_function() 的 OID。 之后,如果你删除并重新创建 my_function(), 那么 populate() 将无法再找到 my_function()。此时你必须启动一个新的数据库 会话,让 populate() 重新编译,它才能重新正常 工作。在更新 my_function() 的定义时,可以使 用 CREATE OR REPLACE FUNCTION 来避免这个问题, 因为函数被“替换”时其 OID 不会改变。

注意

在 PostgreSQL 8.3 及之后的版本中,只要已保存计划所引用的任何表发生了任何架构改变,这些计划就会被替换。这消除了保存计划的一个主要缺点。但对函数引用没有这样的机制,因此上面涉及引用已删除函数的示例仍然成立。

由于 PL/pgSQL 以这种方式保存执行 计划,直接出现在 PL/pgSQL 函数中的 SQL 命令在每次 执行时必须引用相同的表和列;也就是说,不能在 SQL 命令中把 参数用作表名或列名。要绕过这一限制,可以使用 PL/pgSQL 的 EXECUTE 语句构造动态命令 — 代价是 每次执行都要构造一个新的执行计划。

另一个要点是,预备计划是参数化的,允许 PL/pgSQL 变量的值在一次使用与下一次 使用之间变化,如前文详细讨论的那样。有时这意味着计划不如按 特定变量值生成的计划高效。举例来说,考虑

SELECT * INTO myrec FROM dictionary WHERE word LIKE search_term;

其中 search_term 是一个 PL/pgSQL 变量。该查询的缓存计划永远 不会使用 word 上的索引,因为规划器不能假定 LIKE 模式在运行时是左锚定的。要使用索引, 必须在规划查询时提供具体的常量 LIKE 模式。这是另一种情况,可以用 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 的目标列是这种类型。因此, 'now' 将在 INSERT 被规划时转换为一个常量,然后在该 会话的生命周期内被用于所有对 logfunc1 的调用。不用说,这不是程序员 想要的。

在 logfunc2 中, PostgreSQL 的主解析器不知道 'now' 应该变成什么类型,因此返回一个 text 类型的数据值,其中包含字符串 now。在随后给局部变量 curtime 赋值时, PL/pgSQL 解释器通过调用用于该转换 的 text_out 和 timestamp_in 函数,把这个字符串转换为 timestamp 类型。因此,计算得到的时间戳会按程序员 预期在每次执行时更新。

提交更正

译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。