支持的版本:当前 (17) / 16 / 15 / 14 / 13
开发版本: devel
不支持的版本:12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0 / 7.4 / 7.3 / 7.2

41.5. 基本语句 #

在本节和接下来的章节中,我们将描述 PL/pgSQL 明确理解的所有语句类型。任何未被识别为这些语句类型之一的内容都被认为是 SQL 命令,并被发送到主数据库引擎执行,如 第 41.5.2 节中所述。

41.5.1. 赋值 #

将值赋值给 PL/pgSQL 变量的写法如下:

variable { := | = } expression;

如前所述,此类语句中的表达式通过发送到主数据库引擎的 SQL SELECT 命令进行评估。表达式必须产生单个值(如果变量是行或记录变量,则可能是一个行值)。目标变量可以是简单变量(可以选择使用块名称限定),行或记录目标的字段,或者数组目标的一个元素或切片。可以使用等于号 (=) 代替符合 PL/SQL 标准的 :=

如果表达式的结果数据类型与变量的数据类型不匹配,则该值将如同通过赋值强制转换一样进行强制转换(请参见 第 10.4 节)。如果对于所涉及的数据类型对没有已知的赋值强制转换,则 PL/pgSQL 解释器将尝试以文本方式转换结果值,即应用结果类型的输出函数,然后应用变量类型的输入函数。请注意,如果结果值的字符串形式不被输入函数接受,则可能导致输入函数生成运行时错误。

示例

tax := subtotal * 0.06;
my_record.user_id := 20;
my_array[j] := 20;
my_array[1:3] := array[1,2,3];
complex_array[n].realpart = 12.3;

41.5.2. 执行 SQL 命令 #

通常,任何不返回行的 SQL 命令都可以通过简单地编写该命令在 PL/pgSQL 函数中执行。例如,您可以编写以下代码来创建并填充一个表:

CREATE TABLE mytable (id int primary key, data text);
INSERT INTO mytable VALUES (1,'one'), (2,'two');

如果命令确实返回行(例如,SELECT,或者带有 RETURNINGINSERT/UPDATE/DELETE/MERGE),则有两种处理方式。当命令最多返回一行时,或者您只关心第一行输出时,像往常一样编写该命令,但添加一个 INTO 子句来捕获输出,如 第 41.5.3 节中所述。要处理所有输出行,请将命令编写为 FOR 循环的数据源,如 第 41.6.6 节中所述。

通常,仅仅执行静态定义的 SQL 命令是不够的。通常,您需要命令使用不同的数据值,甚至在更基本的方式上进行变化,例如在不同的时间使用不同的表名。同样,根据情况,有两种处理方法。

PL/pgSQL 变量值可以自动插入到可优化的 SQL 命令中,这些命令是 SELECTINSERTUPDATEDELETEMERGE 以及某些包含其中一个命令的实用程序命令,例如 EXPLAINCREATE TABLE ... AS SELECT。在这些命令中,命令文本中出现的任何 PL/pgSQL 变量名都将替换为查询参数,然后在运行时将该变量的当前值作为参数值提供。这与之前描述的表达式处理方式完全相同;有关详细信息,请参见 第 41.11.1 节

以这种方式执行可优化的 SQL 命令时,PL/pgSQL 可能会缓存并重用该命令的执行计划,如 第 41.11.2 节中所述。

不可优化的 SQL 命令(也称为实用程序命令)无法接受查询参数。因此,PL/pgSQL 变量的自动替换在这些命令中不起作用。要在从 PL/pgSQL 执行的实用程序命令中包含非常量文本,您必须将实用程序命令构建为字符串,然后 EXECUTE 它,如 第 41.5.4 节中所述。

如果您想以某种方式修改命令(而不是提供数据值),例如通过更改表名,也必须使用 EXECUTE

有时,评估表达式或 SELECT 查询但放弃结果会很有用,例如在调用具有副作用但没有有用结果值的函数时。要在 PL/pgSQL 中执行此操作,请使用 PERFORM 语句

PERFORM query;

这将执行 query 并放弃结果。编写 query 的方式与您编写 SQL SELECT 命令的方式相同,但将初始关键字 SELECT 替换为 PERFORM。对于 WITH 查询,请使用 PERFORM,然后将查询放在括号中。(在这种情况下,查询只能返回一行。)PL/pgSQL 变量将像上面描述的那样替换到查询中,并且以相同的方式缓存计划。此外,如果查询生成了至少一行,则特殊变量 FOUND 将设置为 true;如果未生成任何行,则设置为 false(请参见 第 41.5.5 节)。

注意

人们可能会期望直接编写 SELECT 会达到此结果,但目前唯一接受的方式是 PERFORM。除非 SQL 命令具有 INTO 子句(如下一节中所述),否则可能会返回行的 SQL 命令(例如 SELECT)将被拒绝,并显示错误。

一个例子

PERFORM create_mv('cs_session_page_requests_mv', my_query);

41.5.3. 执行单行结果的命令 #

产生单行(可能包含多列)的 SQL 命令的结果可以赋值给记录变量、行类型变量或标量变量列表。这是通过编写基本 SQL 命令并添加 INTO 子句来完成的。例如:

SELECT select_expressions INTO [STRICT] target FROM ...;
INSERT ... RETURNING expressions INTO [STRICT] target;
UPDATE ... RETURNING expressions INTO [STRICT] target;
DELETE ... RETURNING expressions INTO [STRICT] target;
MERGE ... RETURNING expressions INTO [STRICT] target;

其中 target 可以是记录变量、行变量或以逗号分隔的简单变量和记录/行字段列表。PL/pgSQL 变量将像上面描述的那样替换到命令的其余部分(即,除了 INTO 子句之外的所有内容)中,并且以相同的方式缓存计划。这适用于带有 RETURNINGSELECTINSERT/UPDATE/DELETE/MERGE 以及某些返回行集的实用程序命令,例如 EXPLAIN。除了 INTO 子句外,SQL 命令与在 PL/pgSQL 外部编写的方式相同。

提示

请注意,SELECTINTO 的这种解释与 PostgreSQL 的常规 SELECT INTO 命令完全不同,在常规命令中,INTO 目标是新创建的表。如果要在 PL/pgSQL 函数内部从 SELECT 结果创建表,请使用语法 CREATE TABLE ... AS SELECT

如果将行变量或变量列表用作目标,则命令的结果列必须在数量和数据类型上与目标的结构完全匹配,否则会发生运行时错误。当记录变量是目标时,它会自动将其配置为命令的结果列的行类型。

INTO 子句几乎可以出现在 SQL 命令中的任何位置。通常,它写在 SELECT 命令中 select_expressions 列表之前或之后,或者对于其他命令类型写在命令的末尾。建议您遵循此约定,以防 PL/pgSQL 解析器在未来版本中变得更加严格。

如果 INTO 子句中未指定 STRICT,则 target 将设置为命令返回的第一行,如果命令未返回任何行,则设置为 null。(请注意,除非你使用了 ORDER BY,否则 第一行 的定义不明确。)第一行之后的任何结果行都将被丢弃。你可以检查特殊的 FOUND 变量(请参阅第 41.5.5 节)以确定是否返回了一行。

SELECT * INTO myrec FROM emp WHERE empname = myname;
IF NOT FOUND THEN
    RAISE EXCEPTION 'employee % not found', myname;
END IF;

如果指定了 STRICT 选项,则命令必须只返回一行,否则将报告运行时错误,即 NO_DATA_FOUND(无行)或 TOO_MANY_ROWS(多于一行)。如果你想捕获错误,可以使用异常块,例如

BEGIN
    SELECT * INTO STRICT myrec FROM emp WHERE empname = myname;
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            RAISE EXCEPTION 'employee % not found', myname;
        WHEN TOO_MANY_ROWS THEN
            RAISE EXCEPTION 'employee % not unique', myname;
END;

成功执行带有 STRICT 的命令始终会将 FOUND 设置为 true。

对于带有 RETURNINGINSERT/UPDATE/DELETE/MERGE,即使未指定 STRICTPL/pgSQL 也会报告返回多行的错误。这是因为没有像 ORDER BY 这样的选项来确定应该返回哪个受影响的行。

如果为函数启用了 print_strict_params,则当因为不满足 STRICT 的要求而抛出错误时,错误消息的 DETAIL 部分将包含有关传递给命令的参数的信息。你可以通过设置 plpgsql.print_strict_params 来更改所有函数的 print_strict_params 设置,但只有后续的函数编译才会受到影响。你也可以通过使用编译器选项在每个函数的基础上启用它,例如

CREATE FUNCTION get_userid(username text) RETURNS int
AS $$
#print_strict_params on
DECLARE
userid int;
BEGIN
    SELECT users.userid INTO STRICT userid
        FROM users WHERE users.username = get_userid.username;
    RETURN userid;
END;
$$ LANGUAGE plpgsql;

失败时,此函数可能会生成如下错误消息

ERROR:  query returned no rows
DETAIL:  parameters: username = 'nosuchuser'
CONTEXT:  PL/pgSQL function get_userid(text) line 6 at SQL statement

注意

STRICT 选项与 Oracle PL/SQL 的 SELECT INTO 和相关语句的行为相匹配。

41.5.4. 执行动态命令 #

通常,你希望在你的 PL/pgSQL 函数内部生成动态命令,也就是说,每次执行时涉及不同的表或不同的数据类型的命令。PL/pgSQL 尝试缓存命令计划的正常方式(如第 41.11.2 节中所述)在这种情况下将不起作用。为了处理这类问题,提供了 EXECUTE 语句。

EXECUTE command-string [ INTO [STRICT] target ] [ USING expression [, ... ] ];

其中 command-string 是一个表达式,生成一个字符串(text 类型),其中包含要执行的命令。可选的 target 是一个记录变量、一个行变量或一个逗号分隔的简单变量和记录/行字段列表,命令的结果将存储在其中。可选的 USING 表达式提供要插入到命令中的值。

不会对计算的命令字符串执行 PL/pgSQL 变量的替换。任何需要的变量值都必须在构造命令字符串时插入到其中;或者你可以使用如下所述的参数。

此外,通过 EXECUTE 执行的命令没有计划缓存。相反,每次运行语句时都会对命令进行计划。因此,可以在函数中动态创建命令字符串,以便对不同的表和列执行操作。

INTO 子句指定应将返回行的 SQL 命令的结果分配到哪里。如果提供了行变量或变量列表,则它必须与命令结果的结构完全匹配;如果提供了记录变量,它将自动配置自身以匹配结果结构。如果返回多行,则只有第一行将被分配给 INTO 变量。如果没有返回任何行,则将 NULL 分配给 INTO 变量。如果未指定 INTO 子句,则将丢弃命令结果。

如果给定了 STRICT 选项,则除非命令只生成一行,否则会报告错误。

命令字符串可以使用参数值,这些参数值在命令中被引用为 $1$2 等。这些符号引用在 USING 子句中提供的值。此方法通常比将数据值作为文本插入到命令字符串中更可取:它避免了将值转换为文本再转换回来的运行时开销,并且由于无需引用或转义,因此更不容易遭受 SQL 注入攻击。一个例子是

EXECUTE 'SELECT count(*) FROM mytable WHERE inserted_by = $1 AND inserted <= $2'
   INTO c
   USING checked_user, checked_date;

请注意,参数符号只能用于数据值 — 如果你想使用动态确定的表或列名称,则必须以文本形式将它们插入到命令字符串中。例如,如果需要针对动态选择的表执行前面的查询,则可以这样做

EXECUTE 'SELECT count(*) FROM '
    || quote_ident(tabname)
    || ' WHERE inserted_by = $1 AND inserted <= $2'
   INTO c
   USING checked_user, checked_date;

一种更简洁的方法是使用 format()%I 规范来插入带有自动引号的表或列名称

EXECUTE format('SELECT count(*) FROM %I '
   'WHERE inserted_by = $1 AND inserted <= $2', tabname)
   INTO c
   USING checked_user, checked_date;

(此示例依赖于 SQL 规则,即由换行符分隔的字符串文字是隐式连接的。)

参数符号的另一个限制是它们仅在可优化的 SQL 命令(SELECTINSERTUPDATEDELETEMERGE 以及包含其中一个的某些命令)中起作用。在其他语句类型(通常称为实用程序语句)中,即使它们只是数据值,也必须以文本形式插入值。

如上面的第一个示例中所示,具有简单常量命令字符串和一些 USING 参数的 EXECUTE 在功能上等同于直接在 PL/pgSQL 中编写命令并允许自动替换 PL/pgSQL 变量。重要的区别在于,EXECUTE 将在每次执行时重新计划命令,生成一个特定于当前参数值的计划;而 PL/pgSQL 可能会创建通用计划并将其缓存以供重用。在最佳计划强烈依赖于参数值的情况下,使用 EXECUTE 可以有助于确保不选择通用计划。

SELECT INTO 目前在 EXECUTE 中不受支持;相反,请执行普通的 SELECT 命令,并将 INTO 指定为 EXECUTE 本身的一部分。

注意

PL/pgSQL EXECUTE 语句与 PostgreSQL 服务器支持的 EXECUTE SQL 语句无关。服务器的 EXECUTE 语句不能直接在 PL/pgSQL 函数中使用(并且不需要)。

示例 41.1. 动态查询中的值引用

在使用动态命令时,你经常必须处理单引号的转义。在函数体中引用固定文本的推荐方法是美元引用。(如果你有未使用美元引用的遗留代码,请参考第 41.12.1 节中的概述,这可以帮助你在将所述代码转换为更合理的方案时节省一些精力。)

动态值需要小心处理,因为它们可能包含引号字符。一个使用 format() 的示例(这假设你正在美元引用函数体,因此无需将引号加倍)

EXECUTE format('UPDATE tbl SET %I = $1 '
   'WHERE key = $2', colname) USING newvalue, keyvalue;

也可以直接调用引用函数

EXECUTE 'UPDATE tbl SET '
        || quote_ident(colname)
        || ' = '
        || quote_literal(newvalue)
        || ' WHERE key = '
        || quote_literal(keyvalue);

此示例演示了 quote_identquote_literal 函数的用法(请参阅第 9.4 节)。为了安全起见,包含列或表标识符的表达式在插入动态查询之前应通过 quote_ident 传递。包含应为构造命令中的文字字符串的值的表达式应通过 quote_literal 传递。这些函数会采取适当的步骤来分别返回用双引号或单引号括起来的输入文本,并正确转义任何嵌入的特殊字符。

因为 quote_literal 被标记为 STRICT,因此当使用 null 参数调用时,它将始终返回 null。在上面的示例中,如果 newvaluekeyvalue 为 null,则整个动态查询字符串将变为 null,从而导致 EXECUTE 出现错误。你可以通过使用 quote_nullable 函数来避免此问题,该函数的工作方式与 quote_literal 相同,只是当使用 null 参数调用时,它会返回字符串 NULL。例如,

EXECUTE 'UPDATE tbl SET '
        || quote_ident(colname)
        || ' = '
        || quote_nullable(newvalue)
        || ' WHERE key = '
        || quote_nullable(keyvalue);

如果你正在处理可能为 null 的值,则通常应使用 quote_nullable 代替 quote_literal

与往常一样,必须注意确保查询中的 null 值不会传递意外的结果。例如 WHERE 子句

'WHERE key = ' || quote_nullable(keyvalue)

如果 keyvalue 为 null,则永远不会成功,因为将相等运算符 = 与 null 操作数一起使用得到的结果始终为 null。如果你希望 null 像普通的键值一样工作,则需要将上述内容重写为

'WHERE key IS NOT DISTINCT FROM ' || quote_nullable(keyvalue)

(目前,IS NOT DISTINCT FROM 的处理效率远低于 =,因此除非必须这样做,否则不要这样做。有关 null 和 IS DISTINCT 的更多信息,请参阅第 9.2 节。)

请注意,美元引用仅对引用固定文本有用。尝试将此示例写成如下形式是一个非常糟糕的主意

EXECUTE 'UPDATE tbl SET '
        || quote_ident(colname)
        || ' = $$'
        || newvalue
        || '$$ WHERE key = '
        || quote_literal(keyvalue);

因为如果 newvalue 的内容恰好包含 $$,就会出错。同样的反对意见也适用于您可能选择的任何其他美元符号引用分隔符。因此,要安全地引用事先未知的文本,您必须使用 quote_literalquote_nullablequote_ident,视情况而定。

动态 SQL 语句也可以使用 format 函数安全地构造(参见 第 9.4.1 节)。例如:

EXECUTE format('UPDATE tbl SET %I = %L '
   'WHERE key = %L', colname, newvalue, keyvalue);

%I 等同于 quote_ident,而 %L 等同于 quote_nullableformat 函数可以与 USING 子句一起使用。

EXECUTE format('UPDATE tbl SET %I = $1 WHERE key = $2', colname)
   USING newvalue, keyvalue;

这种形式更好,因为变量以其原生数据类型格式处理,而不是无条件地将它们转换为文本并通过 %L 引用。它也更有效率。


一个更大的动态命令和 EXECUTE 的示例可以在 示例 41.10 中看到,该示例构建并执行一个 CREATE FUNCTION 命令来定义一个新函数。

41.5.5. 获取结果状态 #

有几种方法可以确定命令的效果。第一种方法是使用 GET DIAGNOSTICS 命令,其形式如下:

GET [ CURRENT ] DIAGNOSTICS variable { = | := } item [ , ... ];

此命令允许检索系统状态指示符。CURRENT 是一个噪音词(但另请参见 第 41.6.8.1 节中的 GET STACKED DIAGNOSTICS)。每个 item 都是一个关键字,用于标识要分配给指定 variable 的状态值(该变量应该具有正确的数据类型来接收它)。当前可用的状态项显示在 表 41.1 中。可以使用冒号等号(:=)代替 SQL 标准的 = 标记。一个例子:

GET DIAGNOSTICS integer_var = ROW_COUNT;

表 41.1. 可用的诊断项

名称 类型 描述
ROW_COUNT bigint 最近一次处理的行数SQL命令
PG_CONTEXT text 描述当前调用堆栈的文本行(请参见第 41.6.9 节
PG_ROUTINE_OID oid 当前函数的 OID

确定命令效果的第二种方法是检查名为 FOUND 的特殊变量,其类型为 booleanFOUND 在每个 PL/pgSQL 函数调用中都以 false 开始。它由以下每种类型的语句设置:

  • SELECT INTO 语句在分配行时将 FOUND 设置为 true,如果没有返回行,则设置为 false。

  • 如果 PERFORM 语句产生(并丢弃)一行或多行,则将 FOUND 设置为 true,如果没有产生行,则设置为 false。

  • 如果 UPDATEINSERTDELETEMERGE 语句至少影响一行,则将 FOUND 设置为 true,如果没有影响任何行,则设置为 false。

  • 如果 FETCH 语句返回一行,则将 FOUND 设置为 true,如果没有返回行,则设置为 false。

  • 如果 MOVE 语句成功地重新定位光标,则将 FOUND 设置为 true,否则设置为 false。

  • 如果 FORFOREACH 语句迭代一次或多次,则将 FOUND 设置为 true,否则设置为 false。FOUND 在循环退出时以这种方式设置;在循环执行期间,循环语句不会修改 FOUND,尽管它可能会被循环体中其他语句的执行更改。

  • 如果 RETURN QUERYRETURN QUERY EXECUTE 语句返回至少一行,则将 FOUND 设置为 true,如果没有返回行,则设置为 false。

其他 PL/pgSQL 语句不会更改 FOUND 的状态。特别注意,EXECUTE 会更改 GET DIAGNOSTICS 的输出,但不会更改 FOUND

FOUND 是每个 PL/pgSQL 函数中的局部变量;对其的任何更改只会影响当前函数。

41.5.6. 什么都不做 #

有时,一个什么都不做的占位符语句很有用。例如,它可以指示 if/then/else 链的一个分支是有意为空的。为此,请使用 NULL 语句。

NULL;

例如,以下两段代码是等效的:

BEGIN
    y := x / 0;
EXCEPTION
    WHEN division_by_zero THEN
        NULL;  -- ignore the error
END;
BEGIN
    y := x / 0;
EXCEPTION
    WHEN division_by_zero THEN  -- ignore the error
END;

哪一个更好取决于个人喜好。

注意

在 Oracle 的 PL/SQL 中,不允许空的语句列表,因此在这种情况必须使用 NULL 语句。而 PL/pgSQL 允许您直接不写任何东西。

提交更正

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