PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

41.5. 基础语句 #

41.5.1. 变量赋值
41.5.2. 执行普通 SQL 命令
41.5.3. 执行单行结果查询
41.5.4. 执行动态 SQL 命令
41.5.5. 获取命令执行状态
41.5.6. 空语句

在本节及后续各节中,我们描述 PL/pgSQL 显式理解的所有语句类型。未被识别为这些语句类型之一的任何内容 都会被视为 SQL 命令并发送给主数据库引擎执行, 如 Section 41.5.2 中所述。

41.5.1. 变量赋值 #

给 PL/pgSQL 变量赋值可以写成:

variable { := | = } expression;

如前所述,这类语句中的表达式通过向主数据库引擎发送一条 SQL SELECT 命令来求值。 表达式必须产生单个值(如果变量是行变量或记录变量, 则可以是行值)。目标变量可以是简单变量(可选地带块名限定)、 行或记录目标的字段,或者数组目标的元素或切片。 等号(=)可以用来替代符合 PL/SQL 习惯的 :=。

如果表达式的结果数据类型与变量的数据类型不匹配, 该值会像通过赋值转换那样被强制转换 (见 Section 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 子句来捕获输出, 如 Section 41.5.3 中所述。 要处理所有输出行,可以把该命令写成一个 FOR 循环的数据源,如 Section 41.6.6 中所述。

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

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

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

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

如果你想以提供数据值之外的其他方式修改命令(例如更改表名), 也必须使用 EXECUTE。

有时求值一个表达式或 SELECT 查询但丢弃其结果 会很有用,例如调用一个有副作用但没有有用返回值的函数时。 要在 PL/pgSQL 中做到这一点, 请使用 PERFORM 语句:

PERFORM query;

它执行 query 并丢弃结果。 query 的写法与 SQL SELECT 命令相同,只需把开头的关键字 SELECT 替换为 PERFORM。 对于 WITH 查询,使用 PERFORM 并把查询放在圆括号中。(在这种情况下,查询只能返回一行。) PL/pgSQL 变量会像前面描述的那样 被替换进查询,计划也以相同的方式缓存。此外, 如果查询产生了至少一行,特殊变量 FOUND 会被置为 true;如果未产生行,则置为 false (见 Section 41.5.5)。

Note

你可能期望直接书写 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 之外书写时完全相同。

Tip

注意,这种对带 INTO 的 SELECT 的解释与 PostgreSQL 常规的 SELECT INTO 命令差别很大, 后者的 INTO 目标是一张新建的表。 如果你想在 PL/pgSQL 函数内部 从 SELECT 结果创建一张表, 请使用 CREATE TABLE ... AS SELECT 语法。

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

INTO 子句几乎可以出现在 SQL 命令中的任何位置。 惯例是在 SELECT 命令中把它写在 select_expressions 列表紧前或紧后, 或在其他命令类型的命令末尾书写。建议你遵循这一惯例, 以防 PL/pgSQL 解析器在未来的版本中 变得更加严格。

如果 INTO 子句中没有指定 STRICT, 则 target 将被设置为命令返回的第一行, 或者在命令没有返回行时被设置为空值。 (注意,除非你使用了 ORDER BY, 否则“第一行”并没有明确定义。) 第一行之后的所有结果行都会被丢弃。 你可以检查特殊变量 FOUND(见 Section 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。

对于带 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

Note

STRICT 选项与 Oracle PL/SQL 的 SELECT INTO 及相关语句的行为一致。

41.5.4. 执行动态 SQL 命令 #

你常常需要在 PL/pgSQL 函数内部生成动态命令, 也就是每次执行时涉及不同表或不同数据类型的命令。 PL/pgSQL 为命令缓存计划的常规尝试 (如 Section 41.11.2 中所讨论的) 在这类场景中行不通。为处理这类问题, PL/pgSQL 提供了 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 本身的一部分。

Note

PL/pgSQL 的 EXECUTE 语句与 PostgreSQL 服务器支持的 EXECUTE SQL 语句没有关系。服务器的 EXECUTE 语句 不能直接在 PL/pgSQL 函数中使用 (而且也没有必要)。

Example 41.1. 动态查询中的数值转义引用

处理动态命令时,你常常不得不处理单引号的转义。 在函数体中引用固定文本的推荐方法是美元符引用。 (如果你有不使用美元符引用的遗留代码,请参阅 Section 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 函数的用法 (见 Section 9.4)。为安全起见, 包含列或表标识符的表达式在插入动态查询之前应该先经过 quote_ident。包含应该在构造的命令中 成为字面字符串的值的表达式应该先经过 quote_literal。这些函数会采取适当的步骤, 返回分别用双引号或单引号括起的输入文本, 并对其中的特殊字符进行正确的转义。

由于 quote_literal 被标记为 STRICT,用空参数调用它时总是返回空值。 在上面的例子中,如果 newvalue 或 keyvalue 为空,整个动态查询字符串就会变成空, 从而导致 EXECUTE 出错。 你可以用 quote_nullable 函数避免这个问题, 它与 quote_literal 的作用相同, 只是在用空参数调用时返回字符串 NULL。 例如,

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

如果你要处理的值可能为空,通常应该用 quote_nullable 替代 quote_literal。

与往常一样,必须小心确保查询中的空值不会带来意外的结果。 例如,WHERE 子句

'WHERE key = ' || quote_nullable(keyvalue)

在 keyvalue 为空时永远不会成功, 因为让相等操作符 = 作用在空操作数上的结果 总是空。如果你希望空值表现得像一个普通的键值, 就需要把上面的写法改写为

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

(目前,IS NOT DISTINCT FROM 的处理效率 远低于 =,所以除非必要请不要这样做。 有关空值与 IS DISTINCT 的更多信息, 见 Section 9.2。)

注意,美元符引用只对引用固定文本有用。 试图像下面这样写这个例子是一个非常糟糕的主意:

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

因为如果 newvalue 的内容恰好包含 $$,它就会失效。 同样的反对意见适用于你可能挑选的任何其他美元符引用定界符。 因此,要安全地引用事先未知的文本,你必须酌情使用 quote_literal、quote_nullable 或 quote_ident。

动态 SQL 语句也可以使用 format 函数安全地构造 (见 Section 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 引用。 它的效率也更高。


在 Example 41.10 中可以看到一个 大得多的动态命令与 EXECUTE 的例子, 它构建并执行一条 CREATE FUNCTION 命令 来定义一个新函数。

41.5.5. 获取命令执行状态 #

有几种方法可以确定一条命令的影响。第一种方法是使用 GET DIAGNOSTICS 命令,其形式为:

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

该命令允许检索系统状态指示值。 CURRENT 是一个无意义的修饰词 (但另见 Section 41.6.8.1 中的 GET STACKED DIAGNOSTICS)。 每个 item 是一个关键字, 标识要赋给指定 variable (其数据类型应适合接收该值)的状态值。 当前可用的状态项见 Table 41.1。 冒号等号(:=)可以用来替代 SQL 标准的 = 记号。一个例子:

GET DIAGNOSTICS integer_var = ROW_COUNT;

Table 41.1. 可用诊断状态项清单

NameTypeDescription
ROW_COUNTbigint最近一条 SQL 命令 处理的行数
PG_CONTEXTtext描述当前调用栈的文本行 (见 Section 41.6.9)
PG_ROUTINE_OIDoid当前函数的 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。

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;

哪种写法更好纯属个人口味。

Note

在 Oracle 的 PL/SQL 中,不允许空的语句列表, 因此在这类情况下必须使用 NULL 语句。PL/pgSQL 允许你什么都不写。