Redrock Postgres 搜索 英文
版本: 9.3 / 9.4 / 9.5 / 9.6 / 10 / 11 / 12 / 13 / 14 / 15 / 16 / 17

41.5 基本语句 #

41.5.1 赋值
41.5.2 执行 SQL 命令
41.5.3 执行带单行结果的命令
41.5.4 执行动态命令
41.5.5 获取结果状态
41.5.6 什么也不做

在本节及其后续章节中,我们将介绍 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,或者 INSERT/UPDATE/DELETE/MERGE 带有 RETURNING),有两种方法可以继续操作。当命令最多返回一行,或者您只关心输出的第一行时,像往常一样编写命令,但添加一个 INTO 子句来捕获输出,如 第 41.5.3 节 所述。要处理所有输出行,像 第 41.6.6 节 所述那样,将命令作为 FOR 循环的数据源编写。

通常,只执行静态定义的 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 将被设置为真;如果没有生成任何行,则将其设置为假(参见第 41.5.5 节)。

注意

人们可能会期望直接写入 SELECT 会达成这一结果,但目前唯一可以做到这一点的方法是 PERFORM。诸如 SELECT 的可以返回行的 SQL 命令会被拒绝为错误,除非它具有 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 子句之外的所有内容),就像上面描述的那样,并且将以相同的方式缓存该计划。这适用于带有 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 解析器在未来版本中变得更加严格。

如果 STRICT 未在 INTO 子句中指定,则 目标 将设置为该命令返回的第一行,如果没有行返回,则设置为 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/MERGEPL/pgSQL 将对返回的多于一行报告一个错误,即使未指定 STRICT。这是因为没有 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 [, ... ] ];

其中 命令字符串 是产生一个字符串的表达式(文本类型 )包含要执行的命令。可选 目标 是记录变量、行变量或简单变量的逗号分隔列表和记录/行字段,执行结果将存储在其中。可选 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,以及包含其中某一个的特定命令)。在其他语句类型(泛称为实用程序语句)中,即使只是数据值,你也必须文本化地插入值。

EXECUTE 带有一个简单的常量命令字符串,还有一些 USING 参数(如上文中第一个示例所示),在功能上等效于在 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 值不会产生意外结果。例如,如果 keyvalue 为 null,WHERE 子句

'WHERE key = ' || quote_nullable(keyvalue)

将永远无法成功,因为使用相等运算符 = 和 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

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

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 的特殊变量,其类型为 booleanFOUND 在每个 PL/pgSQL 函数调用中初始值都为 false。它由以下类型的每个语句设置:

  • 当分配了一行时,SELECT INTO 语句将 FOUND 设置为真,如果没有返回行,则设置为假。

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

  • UPDATEINSERTDELETEMERGE 语句会将 FOUND 设置为真(如果影响了至少一行);如果没有影响行,则设置为假。

  • 如果返回一行,则 FETCH 语句会将 FOUND 设置为真;如果没有返回行,则设置为假。

  • 如果成功重新定位了游标,则 MOVE 语句会将 FOUND 设置为真;否则设置为假。

  • 如果一个 FORFOREACH 语句迭代了一次或多次,则会将 FOUND 设置为真;否则设置为假。当循环退出时,FOUND 将以这种方式设置;在循环执行过程中,循环语句不会修改 FOUND,但循环主体中其他语句的执行可能会更改它。

  • RETURN QUERYRETURN QUERY EXECUTE 语句会将 FOUND 设置为真(如果查询返回了至少一行);如果没有返回行,则设置为假。

其他 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 则允许你什么都不写。