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

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 / 8.2 / 8.1 / 8.0 / 7.4 / 7.3 / 7.2
测试版PostgreSQL 19 Beta 4 尚未正式发布,内容与行为在正式发布前可能变化。正式内容请看当前版本。

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,或者带 RETURNING 的 INSERT/UPDATE/DELETE/MERGE),有两种方式处理。当命令最多返回一行,或者只关心第一行的输出时,可照常编写该命令,但要添加一个 INTO 子句来捕获输出,如第 41.5.3 节中所述。要处理所有输出行,可将该命令写成 FOR 循环的数据源,如第 41.6.6 节中所述。

仅仅执行静态定义的 SQL 命令通常是不够的。通常,你会希望一条命令使用可变的数据值,甚至还希望以更根本的方式变化,例如在不同时间使用不同的表名。根据具体情况,同样也有两种处理方式。

PL/pgSQL 变量值可以自动插入可优化的 SQL 命令中,这些命令包括 SELECT、INSERT、UPDATE、DELETE、MERGE 以及某些包含其中之一的工具命令,比如 EXPLAIN 和 CREATE TABLE ... AS SELECT。在这些命令中,命令文本中出现的任何 PL/pgSQL 变量名都会被查询参数替换,然后变量的当前值会在运行时作为参数值提供。这与前面描述的表达式处理完全相同;详情请参见第 41.11.1 节。

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

不可优化的 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 会被设置为真;如果不产生行,则设置为假(见第 41.5.5 节)。

注意

我们可能期望直接写 SELECT 能实现这个结果,但是当前唯一被接受的方式是 PERFORM。一个能返回行的 SQL 命令(例如 SELECT)将被当成一个错误拒绝,除非它像下一节中讨论的有一个 INTO 子句。

一个示例:

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 子句之外的所有部分),并且计划也会以同样的方式缓存。这适用于 SELECT、带有 RETURNING 的 INSERT/UPDATE/DELETE/MERGE,以及某些返回行集的工具命令,例如 EXPLAIN。除了 INTO 子句之外,该 SQL 命令的写法与在 PL/pgSQL 之外完全相同。

提示

注意带 INTO 的 SELECT 的这种解释和 PostgreSQL 常规的 SELECT INTO 命令有很大的不同,后者的 INTO 目标是一个新创建的表。如果你想要在一个 PL/pgSQL 函数中从一个 SELECT 的结果创建一个表,请使用语法 CREATE TABLE ... AS SELECT。

如果一个行变量或一个变量列表被用作目标,该命令的结果列必须完全匹配该目标的结构,包括数量和数据类型,否则会发生一个运行时错误。当一个记录变量是目标时,它会自动地把自身配置成命令的结果列组成的行类型。

INTO 子句几乎可以出现在 SQL 命令中的任何位置。通常它被写成刚好在 SELECT 命令中的 select_expressions 列表之前或之后,或者在其他命令类型的命令最后。我们推荐你遵循这种惯例,以防 PL/pgSQL 的解析器在未来的版本中变得更严格。

如果 STRICT 没有在 INTO 子句中被指定,那么 target 将被设置为该命令返回的第一个行,或者在该命令不返回行时设置为空值(注意除非使用了 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 置为真。

对于带有 RETURNING 的 INSERT/UPDATE/DELETE/MERGE,即使没有指定 STRICT,PL/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 命令(SELECT、INSERT、UPDATE、DELETE、MERGE 以及包含其中之一的某些命令)。在其他语句类型(通称为工具语句)中,即使它们只是数据值,你也必须以文本方式插入值。

在上面第一个示例中,带有一个简单的常量命令字符串和一些 USING 参数的 EXECUTE 命令在功能上等效于直接用 PL/pgSQL 写的命令,并且允许自动发生 PL/pgSQL 变量替换。重要的不同之处在于,EXECUTE 会在每一次执行时根据当前的参数值重新计划该命令,而 PL/pgSQL 则可能创建一个通用计划并且将其缓存以便重用。在最佳计划强依赖于参数值的情况中,使用 EXECUTE 来明确地保证不会选择一个通用计划是很有帮助的。

EXECUTE 目前不支持 SELECT INTO。但是可以执行一个纯的 SELECT 命令并且指定 INTO 作为 EXECUTE 本身的一部分。

注意

PL/pgSQL 的 EXECUTE 语句与 PostgreSQL 服务器支持的 SQL EXECUTE 语句无关。服务器的 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_ident 和 quote_literal 函数的用法(见第 9.4 节)。为了安全起见,在插入动态查询之前,包含列名或表名标识符的表达式应先传给 quote_ident。在构造出的命令中应作为字符串字面量出现的值,则应传给 quote_literal。这两个函数都会采取适当措施,分别返回用双引号或单引号括起来的输入文本,并正确转义其中嵌入的特殊字符。

由于 quote_literal 被标记为 STRICT,因此用 null 参数调用时它总会返回 null。在上面的示例中,如果 newvalue 或 keyvalue 为 null,整个动态查询字符串都会变成 null,进而导致 EXECUTE 报错。可以通过使用 quote_nullable 函数来避免这个问题;它与 quote_literal 的工作方式相同,只是在用 null 参数调用时会返回字符串 NULL。例如:

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

如果正在处理的参数值可能为空值,那么通常应该用 quote_nullable 来代替 quote_literal。

通常,必须小心地确保查询中的空值不会产生意料之外的结果。例如如果 keyvalue 为空值,下面的 WHERE 子句

'WHERE key = ' || quote_nullable(keyvalue)

永远不会成功,因为在=操作符中使用空值操作数得到的结果总是空值。如果想让空值像普通键值一样工作,你应该将上面的命令重写成

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

(目前,IS NOT DISTINCT FROM 的处理效率不如=,因此只有在非常必要时才这样做。关于空值和 IS DISTINCT 的详细信息请见第 9.2 节)。

请注意美元引用只对引用固定文本有用。尝试写出下面这个示例是一个非常糟糕的主意:

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

因为如果 newvalue 的内容碰巧含有$$,那么这段代码就会出问题。同样的问题也适用于你选择的任何其他美元引用定界符。因此,要想安全地引用事先不知道的文本,必须恰当地使用 quote_literal、quote_nullable 或 quote_ident。

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

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

%I 等效于 quote_ident 并且 %L 等效于 quote_nullable。format 函数可以和 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 的特殊变量,类型为 boolean。在每次 PL/pgSQL 函数调用中,FOUND 的初始值都是 false。它由以下类型的语句设置:

  • SELECT INTO 语句在为目标赋上一行值时将 FOUND 设置为 true,如果没有返回行则设置为 false。

  • PERFORM 语句在生成(和丢弃)一个或多个行时将 FOUND 设置为 true,如果没有生成行则设置为 false。

  • UPDATE、INSERT、DELETE 和 MERGE 语句在至少影响一行时将 FOUND 设置为 true,如果没有影响行则设置为 false。

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

  • MOVE 语句在成功重新定位游标时将 FOUND 设置为 true,否则设置为 false。

  • FOR 或 FOREACH 语句在迭代一次或多次时将 FOUND 设置为 true,否则设置为 false。当循环退出时,FOUND 会按上述方式设置;在循环执行过程中,FOUND 不会被循环语句修改,尽管它可能会被循环体内的其他语句执行修改。

  • RETURN QUERY 和 RETURN 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;  -- 忽略错误
END;
BEGIN
    y := x / 0;
EXCEPTION
    WHEN division_by_zero THEN  -- 忽略错误
END;

究竟使用哪一种取决于各人的喜好。

注意

在 Oracle 的 PL/SQL 中,不允许出现空语句列表,并且因此在这种情况下必须使用 NULL 语句。而 PL/pgSQL 允许你什么也不写。

提交更正

译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游会在正式发布前持续修订。