选择 打开 改范围 完整检索页

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
历史版本PostgreSQL 9.2 已于 2017 年 11 月结束社区维护,本页译文保留供仍在使用旧版本的读者参考。新系统请看当前版本

39.5. 基本语句 #

在本节和后续各节中,我们描述PL/pgSQL能够明确理解的所有语句类型。凡是不被识别为这些类型之一的内容,都被视为 SQL 命令并发送给主数据库引擎执行,详见第 39.5.2 节第 39.5.3 节

39.5.1. 赋值 #

PL/pgSQL变量赋值的写法为:

variable := expression;

如前所述,这种语句中的表达式通过发送给主数据库引擎的 SQLSELECT命令求值。表达式必须产生单个值(如果变量是行变量或记录变量,也可以是行值)。目标变量可以是简单变量(可以用块名限定)、行变量或记录变量的字段,或者由简单变量或字段表示的数组中的元素。

如果该表达式的结果数据类型不匹配变量的数据类型,或者变量具有特定 的大小/精度(例如 char(20)),结果值将由 PL/pgSQL解释器使用结果类型的输出函数和变量 类型的输入函数隐式转换。注意如果结果值的字符串形式无法被输入函数 所接受,这可能会导致由输入函数产生的运行时错误。

例如:

tax := subtotal * 0.06;
my_record.user_id := 20;

39.5.2. 执行没有结果的命令 #

对于不返回行的 SQL 命令,例如没有RETURNING子句的INSERT,只需写出命令,就可以在PL/pgSQL函数中执行它。

命令文本中出现的任何PL/pgSQL变量名都被视为参数,然后在运行时将变量的当前值作为参数值提供。这与前面描述的表达式处理完全相同;详情见第 39.10.1 节

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

有时,对表达式或SELECT查询求值但丢弃结果会很有用,例如调用有副作用但没有有用结果值的函数时。要在PL/pgSQL中这样做,可以使用PERFORM语句:

PERFORM query;

这会执行query并丢弃结果。编写query的方式与编写 SQLSELECT命令相同,只需把开头的关键字SELECT替换为PERFORM。对于WITH查询,使用PERFORM,然后用圆括号括住查询。(此时查询只能返回一行。)PL/pgSQL变量会被替换到查询中,方式与不返回结果的命令相同,计划也会以相同方式缓存。另外,特殊变量FOUND在查询至少产生一行时设为真,没有产生行时设为假(见第 39.5.5 节)。

注意

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

一个示例:

PERFORM create_mv('cs_session_page_requests_mv', my_query);

39.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;

其中,target可以是记录变量、行变量,或逗号分隔的简单变量及记录/行字段列表。PL/pgSQL变量会被替换到查询的其余部分,计划也会被缓存,正如上面描述的不返回行的命令一样。这适用于SELECTINSERT/UPDATE/DELETE带有RETURNING的情况,以及返回行集结果的工具命令(例如EXPLAIN)。除了INTO子句以外,SQL 命令的写法与在PL/pgSQL之外的写法相同。

提示

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

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

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

如果STRICT没有在INTO子句中指定,那么target会被设为查询返回的第一行;如果查询没有返回行,则设为空值。(注意,第一行并没有明确定义,除非使用了ORDER BY。)第一行之后的所有结果行都会被丢弃。可以检查特殊的FOUND变量(见第 39.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设为真。

对于带有RETURNINGINSERT/UPDATE/DELETE,即使没有指定STRICTPL/pgSQL也会针对多于一个返回行的情况报告一个错误。这是因为没有类似于ORDER BY的选项可以用来决定应该返回哪个被影响的行。

注意

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

对于需要处理 SQL 查询的多个结果行的情况,请参见第 39.6.4 节

39.5.4. 执行动态命令 #

很多时候你将想要在PL/pgSQL函数中产生动态命令,也就是每次执行中会涉及到不同表或不同数据类型的命令。PL/pgSQL通常对于命令所做的缓存计划尝试(如第 39.10.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 '
    || tabname::regclass
    || ' WHERE inserted_by = $1 AND inserted <= $2'
   INTO c
   USING checked_user, checked_date;

参数符号的另一个限制是,它们只能用于SELECTINSERTUPDATE以及DELETE命令。在其他语句类型(统称为工具语句)中,即使只是数据值,也必须以文本形式插入。

带简单常量命令字符串和一些 USING 参数的 EXECUTE(如上面第一个示例那样),在功能上等价于 直接在 PL/pgSQL 中写出该命令,并让 PL/pgSQL 变量的替换自动发生。重要的 区别在于:EXECUTE 会在每次执行时重新规划命令,生成 针对当前参数值的特定计划;而 PL/pgSQL 通常创建的是通用计划并将其 缓存以供重用。在最佳计划强烈依赖参数值的情况下, EXECUTE 可能明显更快;而当计划对参数值不敏感时, 重新规划就是浪费。

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

注意

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

例 39.1. 在动态查询中为值加引号

在使用动态命令时,经常需要处理单引号的转义。我们推荐在函数体中使用美元引用来引用固定文本。(如果你有未使用美元引用的旧代码,请参阅第 39.11.1 节中的概述;在把这类代码转换成更合理的写法时,它会帮你省下一些工夫。)

将被插入到构造出的查询中的动态值需要被小心地处理,因为它们 自身可能包含引号字符。一个示例(这假设你对函数整体使用了美元 引用,因此引号标记不需要被双写):

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);

如果正在处理的参数值可能为空值,那么通常应该用 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_literalquote_nullablequote_ident

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

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

format函数可以和 USING子句一起使用:

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

这种形式更高效,因为参数newvaluekeyvalue 不会被转换为文本。


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

39.5.5. 获取结果状态 #

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

GET [ CURRENT ] DIAGNOSTICS variable = item [ , ... ];

这条命令允许检索系统状态指示符。每个 item 是一个关键字,它标识一个要赋予 给指定变量(该变量应具有正确的数据类型来接收它)的状态值。 当前可用的状态项是 ROW_COUNT(发送到 SQL 引擎的最后一条 SQL 命令所处理的行数)和 RESULT_OID(最近一条 SQL 命令插入的最后一行的 OID)。注意, RESULT_OID 只有在向包含 OID 的表执行 INSERT 命令之后才有用。

例如:

GET DIAGNOSTICS integer_var = ROW_COUNT;

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

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

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

  • UPDATEINSERTDELETE语句在至少影响一行时将FOUND设置为真,如果没有影响行则设置为假。

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

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

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

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

Other PL/pgSQL statements do not change the state of FOUND. Note in particular that EXECUTE changes the output of GET DIAGNOSTICS, but does not change FOUND.

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

39.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 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。