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

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

39.6. 控制结构 #

控制结构可能是PL/pgSQL中最有用的(以及最重要)的部分了。利用PL/pgSQL的控制结构,你可以以非常灵活而且强大的方法操纵PostgreSQL的数据。

39.6.1. 从函数返回 #

有两个命令让我们能够从函数中返回数据:RETURNRETURN NEXT

39.6.1.1. RETURN

RETURN expression;

带有一个表达式的RETURN用于终止函数并把expression的值返回给调用者。这种形式被用于不返回集合的PL/pgSQL函数。

如果函数返回的是标量类型,任何表达式都可以使用。表达式结果会 按照赋值部分的说明自动转换为函数的返回类型。但要返回一个 复合(行)值,你必须把记录变量或行变量写为 expression

如果你声明带输出参数的函数,那么就只需要写不带表达式的RETURN。输出参数变量的当前值将被返回。

如果你声明函数返回void,一个RETURN语句可以被用来提前退出函数;但是不要在RETURN后面写一个表达式。

一个函数的返回值不能是未定义。如果控制到达了函数最顶层块的末尾而没有碰到一个RETURN语句,那么会发生一个运行时错误。不过,这个限制不适用于带输出参数的函数以及返回void的函数。在这些情况中,如果顶层的块结束,将自动执行一个RETURN语句。

39.6.1.2. RETURN NEXTRETURN QUERY

RETURN NEXT expression;
RETURN QUERY query;
RETURN QUERY EXECUTE command-string [ USING expression [, ... ] ];

PL/pgSQL 函数被声明为返回 SETOF sometype 时,返回过程会略有不同。在这种情况下,要返回的各个项通过一系列 RETURN NEXTRETURN QUERY 命令指定,最后再用一个不带参数的 RETURN 命令表明函数已经执行完毕。RETURN NEXT 可用于标量和复合数据类型;对于复合结果类型,会返回完整的结果RETURN QUERY 会把查询执行结果追加到函数的结果集中。在同一个集合返回函数中,RETURN NEXTRETURN QUERY 可以自由混用,此时它们的结果会被串接起来。

RETURN NEXTRETURN QUERY实际上不会从函数中返回 — 它们简单地向函数的结果集中追加零或多行。然后会继续执行PL/pgSQL函数中的下一条语句。随着后继的RETURN NEXTRETURN QUERY命令的执行,结果集就建立起来了。最后一个RETURN(应该没有参数)会导致控制退出该函数(或者你可以让控制到达函数的结尾)。

RETURN QUERY有一种变体RETURN QUERY EXECUTE,它可以动态指定要被执行的查询。可以通过USING向计算出的查询字符串插入参数表达式,这和在EXECUTE命令中的方式相同。

如果函数是用输出参数声明的,只需写不带表达式的 RETURN NEXT。每次执行时,输出参数变量的 当前值都会被保存,最终作为结果的一行返回。注意,为了创建带 输出参数的返回集合函数,当有多个输出参数时,必须把函数声明 为返回 SETOF record;当只有一个类型为 sometype 的输出参数时,必须声明为返回 SETOF sometype

下面是一个使用 RETURN NEXT 的函数示例:

CREATE TABLE foo (fooid INT, foosubid INT, fooname TEXT);
INSERT INTO foo VALUES (1, 2, 'three');
INSERT INTO foo VALUES (4, 5, 'six');

CREATE OR REPLACE FUNCTION getAllFoo() RETURNS SETOF foo AS
$BODY$
DECLARE
    r foo%rowtype;
BEGIN
    FOR r IN SELECT * FROM foo
    WHERE fooid > 0
    LOOP
        -- can do some processing here
        RETURN NEXT r; -- return current row of SELECT
    END LOOP;
    RETURN;
END
$BODY$
LANGUAGE 'plpgsql' ;

SELECT * FROM getallfoo();

注意

如上所述,目前RETURN NEXTRETURN QUERY的实现在从函数返回之前会把整个结果集都保存起来。这意味着如果一个PL/pgSQL函数生成一个非常大的结果集,性能可能会很差:数据将被写到磁盘上以避免内存耗尽,但是函数本身在整个结果集都生成之前不会退出。将来的PL/pgSQL版本可能会允许用户定义没有这种限制的集合返回函数。目前,数据开始被写入到磁盘的时机由配置变量work_mem控制。拥有足够内存来存储大型结果集的管理员可以考虑增大这个参数。

39.6.2. 条件语句 #

IFCASE 语句让你可以根据条件执行不同的命令。PL/pgSQL 有三种形式的 IF

  • IF ... THEN

  • IF ... THEN ... ELSE

  • IF ... THEN ... ELSIF ... THEN ... ELSE

and two forms of CASE:

  • CASE ... WHEN ... THEN ... ELSE ... END CASE

  • CASE WHEN ... THEN ... ELSE ... END CASE

39.6.2.1. IF-THEN

IF boolean-expression THEN
    statements
END IF;

IF-THEN 语句是 IF 的最简单形式。如果条件为真,在 THENEND IF 之间的 语句将被执行。否则,将忽略它们。

示例:

IF v_user_id <> 0 THEN
    UPDATE users SET email = v_email WHERE user_id = v_user_id;
END IF;

39.6.2.2. IF-THEN-ELSE

IF boolean-expression THEN
    statements
ELSE
    statements
END IF;

IF-THEN-ELSE 语句对 IF-THEN 进行了增加,它让你能够指定一组在 条件不为真时应该被执行的语句(注意这也包括条件求值为 NULL 的情况)。

示例:

IF parentid IS NULL OR parentid = ''
THEN
    RETURN fullname;
ELSE
    RETURN hp_true_filename(parentid) || '/' || fullname;
END IF;
IF v_count > 0 THEN
    INSERT INTO users_count (count) VALUES (v_count);
    RETURN 't';
ELSE
    RETURN 'f';
END IF;

39.6.2.3. IF-THEN-ELSIF

IF boolean-expression THEN
    statements
[ ELSIF boolean-expression THEN
    statements
[ ELSIF boolean-expression THEN
    statements
    ...]]
[ ELSE
    statements ]
END IF;

有时会有多于两种选择。IF-THEN-ELSIF 则提供了一个简便的方法来检查多个条件。 IF 条件会被一个接一个测试,直到找到第一个为真的。 然后执行相关语句,之后控制会被交给 END IF 之后的下一个语句(后续的任何 IF 条件不会被测试)。如果 没有一个 IF 条件为真,那么 ELSE 块(如果有)将被执行。

这里有一个示例:

IF number = 0 THEN
    result := 'zero';
ELSIF number > 0 THEN
    result := 'positive';
ELSIF number < 0 THEN
    result := 'negative';
ELSE
    -- hmm, the only other possibility is that number is null
    result := 'NULL';
END IF;

关键词 ELSIF 也可以被拼写成 ELSEIF

另一个可以完成相同任务的方法是嵌套 IF-THEN-ELSE 语句,如下例:

IF demo_row.sex = 'm' THEN
    pretty_sex := 'man';
ELSE
    IF demo_row.sex = 'f' THEN
        pretty_sex := 'woman';
    END IF;
END IF;

However, 这种方法要求为每个 IF 编写匹配的 END IF,因此当有许多备选方案时,它比使用 ELSIF 笨拙得多。

39.6.2.4. 简单 CASE

CASE search-expression
    WHEN expression [, expression [ ... ]] THEN
      statements
  [ WHEN expression [, expression [ ... ]] THEN
      statements
    ... ]
  [ ELSE
      statements ]
END CASE;

CASE 的简单形式提供基于操作数相等的条件执行。 search-expression 被求值(一次),并依次与 WHEN 子句中的每个 expression 比较。如果找到匹配,则执行相应的 statements,然后控制传递到 END CASE 之后的下一个语句。(后续的 WHEN 表达式不会被求值。)如果没有找到匹配,则执行 ELSEstatements; 但如果没有 ELSE,则会引发 CASE_NOT_FOUND 异常。

这里有一个简单示例:

CASE x
    WHEN 1, 2 THEN
        msg := 'one or two';
    ELSE
        msg := 'other value than one or two';
END CASE;

39.6.2.5. 搜索 CASE

CASE
    WHEN boolean-expression THEN
      statements
  [ WHEN boolean-expression THEN
      statements
    ... ]
  [ ELSE
      statements ]
END CASE;

CASE 的搜索形式提供基于布尔表达式真实性的条件 执行。每个 WHEN 子句的 boolean-expression 依次被求值,直到 找到产生 true 的那一个。然后执行相应的 statements,之后控制传递到 END CASE 之后的下一个语句。(后续的 WHEN 表达式不会被求值。)如果没有找到为真的结果, 则执行 ELSEstatements;但如果没有 ELSE,则会引发 CASE_NOT_FOUND 异常。

Here is an example:

CASE
    WHEN x BETWEEN 0 AND 10 THEN
        msg := 'value is between zero and ten';
    WHEN x BETWEEN 11 AND 20 THEN
        msg := 'value is between eleven and twenty';
END CASE;

这种形式的 CASE 完全等价于 IF-THEN-ELSIF,唯一的规则差异是:到达被省略 的 ELSE 子句时会报错,而不是什么也不做。

39.6.3. 简单循环 #

使用 LOOPEXITCONTINUEWHILEFOR 语句,可以让你的 PL/pgSQL 函数重复执行一系列命令。

39.6.3.1. LOOP

[ <<label>> ]
LOOP
    statements
END LOOP [ label ];

LOOP 定义了一个无条件循环,它会无限重复,直到被 EXITRETURN 语句终止。可选的 label 可以供嵌套循环中的 EXITCONTINUE 语句使用, 以指定这些语句作用的是哪一个循环。

39.6.3.2. EXIT

EXIT [ label ] [ WHEN boolean-expression ];

如果没有给出 label,那么最内层的循环会被终止, 然后跟在 END LOOP 后面的语句会被执行。如果给出了 label,那么它必须是当前或者更高 层的嵌套循环或者语句块的标签。然后该命名循环或块就会被终止, 并且控制会转移到该循环/块相应的 END 之后的语句上。

如果指定了 WHEN,则只有在 boolean-expression 为真时才会退出循环。否则, 控制传递到 EXIT 之后的语句。

EXIT 可以与所有类型的循环一起使用;它并 不限于在无条件循环中使用。

当与 BEGIN 块一起使用时, EXIT 会把控制转交给该块结束后的下一条 语句。需要注意的是,为此必须使用标签;未加标签的 EXIT 永远不会被视为匹配某个 BEGIN 块。这与 PostgreSQL 8.4 之前的版本不同, 旧版本允许未加标签的 EXIT 匹配 BEGIN 块。)

Examples:

LOOP
    -- some computations
    IF count > 0 THEN
        EXIT;  -- exit loop
    END IF;
END LOOP;

LOOP
    -- some computations
    EXIT WHEN count > 0;  -- same result as previous example
END LOOP;

<<ablock>>
BEGIN
    -- some computations
    IF stocks > 100000 THEN
        EXIT ablock;  -- causes exit from the BEGIN block
    END IF;
    -- computations here will be skipped when stocks > 100000
END;

39.6.3.3. CONTINUE

CONTINUE [ label ] [ WHEN boolean-expression ];

如果没有给出 label,最内层循环的下一次迭代会 开始。也就是,循环体中剩余的所有语句将被跳过,并且控制会 返回到循环控制表达式(如果有)来决定是否需要另一次循环迭代。 如果 label 存在,它指定应该继续 执行的循环的标签。

如果指定了 WHEN,则只有在 boolean-expression 为真时才会开始循环的下 一次迭代。否则,控制传递到 CONTINUE 之后的语句。

CONTINUE 可以与所有类型的循环一起使用; 它并不限于在无条件循环中使用。

示例:

LOOP
    -- some computations
    EXIT WHEN count > 100;
    CONTINUE WHEN count < 50;
    -- some computations for count IN [50 .. 100]
END LOOP;

39.6.3.4. WHILE

[ <<label>> ]
WHILE boolean-expression LOOP
    statements
END LOOP [ label ];

只要 boolean-expression 被计算为真,WHILE 语句就会重复一个语句 序列。在每次进入到循环体之前都会检查该表达式。

例如:

WHILE amount_owed > 0 AND gift_certificate_balance > 0 LOOP
    -- some computations here
END LOOP;

WHILE NOT done LOOP
    -- some computations here
END LOOP;

39.6.3.5. FOR(整型变体) #

[ <<label>> ]
FOR name IN [ REVERSE ] expression .. expression [ BY expression ] LOOP
    statements
END LOOP [ label ];

这种形式的 FOR 会创建一个在一个整数范围 上迭代的循环。变量 name 会自动定义 为类型 integer 并且只在循环内存在(任何该变量名 的现有定义在此循环内都将被忽略)。给出范围上下界的两个表达式 在进入循环的时候计算一次。如果没有指定 BY 子句,迭代步长为 1,否则步长是 BY 中指定的值,该值也只在循环进入时计算 一次。如果指定了 REVERSE,那么在每次迭代 后会减去步长,而不是加上步长。

整数 FOR 循环的一些示例:

FOR i IN 1..10 LOOP
    -- i will take on the values 1,2,3,4,5,6,7,8,9,10 within the loop
END LOOP;

FOR i IN REVERSE 10..1 LOOP
    -- i will take on the values 10,9,8,7,6,5,4,3,2,1 within the loop
END LOOP;

FOR i IN REVERSE 10..1 BY 2 LOOP
    -- i will take on the values 10,8,6,4,2 within the loop
END LOOP;

如果下界大于上界(或者在 REVERSE 情况下是小于),循环体根本不会被执行。而且不会抛出任何错误。

如果 FOR 循环附加了 label,则整数循环变量可以用该 label 限定的名称来引用。

39.6.4. 遍历查询结果 #

使用另一种形式的 FOR 循环,可以遍历查询的 结果并相应地处理这些数据。语法是:

[ <<label>> ]
FOR target IN query LOOP
    statements
END LOOP [ label ];

target 可以是记录变量、行变量,或 逗号分隔的标量变量列表。target 会被依次赋予 query 产生的每一行, 并且循环体对每一行都执行一次。这里有一个示例:

CREATE FUNCTION cs_refresh_mviews() RETURNS integer AS $$
DECLARE
    mviews RECORD;
BEGIN
    PERFORM cs_log('Refreshing materialized views...');

    FOR mviews IN SELECT * FROM cs_materialized_views ORDER BY sort_key LOOP

        -- Now "mviews" has one record from cs_materialized_views

        PERFORM cs_log('Refreshing materialized view '
                   || quote_ident(mviews.mv_name) || ' ...');
        EXECUTE 'TRUNCATE TABLE ' || quote_ident(mviews.mv_name);
        EXECUTE 'INSERT INTO '
                   || quote_ident(mviews.mv_name) || ' ' 
                   || mviews.mv_query;
    END LOOP;

    PERFORM cs_log('Done refreshing materialized views.');
    RETURN 1;
END;
$$ LANGUAGE plpgsql;

如果循环被 EXIT 语句终止,最后一次赋予的 行值在循环之后仍然可以访问。

这种形式的 FOR 语句所使用的 query 可以是任何向调用者返回行的 SQL 命令:SELECT 是最常见的情况,但也可以 使用带 RETURNING 子句的 INSERTUPDATEDELETE。某些工具命令(如 EXPLAIN)也可以。

PL/pgSQL 变量会被替换到查询文本中, 并且查询计划会被缓存以备可能的重用,详见 第 39.10.1 节第 39.10.2 节

FOR-IN-EXECUTE 语句是另一种遍历行的方式:

[ <<label>> ]
FOR target IN EXECUTE text_expression [ USING expression [, ... ] ] LOOP
    statements
END LOOP [ label ];

它与前一种形式类似,只是源查询被指定为一个字符串表达式, 在每次进入 FOR 循环时都会重新求值和重新规划。 这让程序员可以在预先规划的查询的速度与动态查询的灵活性之间 做出选择,就像使用普通的 EXECUTE 语句一样。 与 EXECUTE 一样,可以通过 USING 向动态命令中插入参数值。

指定要遍历其结果的查询的另一种方式,是把它声明为游标。这 在 第 39.7.4 节 中描述。

39.6.5. 捕获错误 #

默认情况下,PL/pgSQL 函数中发生的任何 错误都会中止函数及其外围事务的执行。你可以使用带有 EXCEPTION 子句的 BEGIN 块来捕获错误并从中恢复。其语法是在普通 BEGIN 块语法上的扩展:

[ <<label>> ]
[ DECLARE
    declarations ]
BEGIN
    statements
EXCEPTION
    WHEN condition [ OR condition ... ] THEN
        handler_statements
    [ WHEN condition [ OR condition ... ] THEN
          handler_statements
      ... ]
END;

如果没有发生错误,这种形式的块只是简单地执行所有 statements,并且接着控制转到 END 之后的下一个语句。但是如果在该 statements 内 发生了一个错误,则会放弃对 statements 的进一步处理,然后控制会 转到 EXCEPTION 列表。系统会在列表中寻找匹配 所发生错误的第一个 condition。如果找到一个匹配,则执行 对应的 handler_statements,并且接着 把控制转到 END 之后的下一个语句。如果没有 找到匹配,该错误就会传播出去,就好像根本没有 EXCEPTION 一样:错误可以被一个带有 EXCEPTION 的外围块捕捉,如果没有这样的块 则中止该函数的处理。

condition 的名字可以是 附录 A 中显示的任何名字。一个分类名 匹配其中所有的错误。特殊的条件名 OTHERS 匹配除了 QUERY_CANCELED 之外的所有错误类型(虽然可能 但通常并不明智,还是可以用名字捕获 QUERY_CANCELED)。条件名是大小写无关的。一个 错误条件也可以通过 SQLSTATE 代码指定,例如以下是等价的:

WHEN division_by_zero THEN ...
WHEN SQLSTATE '22012' THEN ...

如果在选中的 handler_statements 内发生了新的错误, 那么它不能被这个 EXCEPTION 子句捕获,而是被传播出去。一个 外层的 EXCEPTION 子句可以捕获它。

当一个错误被 EXCEPTION 子句捕获时, PL/pgSQL 函数的局部变量会保持错误 发生时的值,但是该块中所有对持久数据库状态的改变都会被回滚。 例如,考虑这个片段:

INSERT INTO mytab(firstname, lastname) VALUES('Tom', 'Jones');
BEGIN
    UPDATE mytab SET firstname = 'Joe' WHERE lastname = 'Jones';
    x := x + 1;
    y := x / 0;
EXCEPTION
    WHEN division_by_zero THEN
        RAISE NOTICE 'caught division_by_zero';
        RETURN x;
END;

当控制到达对 y 的赋值时,它会以 division_by_zero 错误失败。该错误会被 EXCEPTION 子句捕获。 RETURN 语句返回的值将是 x 递增后的值,但 UPDATE 命令的效果已被回滚。不过,块之前的 INSERT 命令没有被回滚,因此最终结果是数据 库中包含的是 Tom Jones 而不是 Joe Jones

提示

包含 EXCEPTION 子句的块,其进入和退出的 开销明显高于没有该子句的块。因此,不要在没有必要的情况下 使用 EXCEPTION

在异常处理器中,SQLSTATE 变量包含与所引发异常对应的错误代码(可能错误代码的列表参见 表 A.1)。 SQLERRM 变量包含与该异常关联的错误消息。 这些变量在异常处理器之外是未定义的。

例 39.2. UPDATE/INSERT 与异常

本例使用异常处理来按需执行 UPDATEINSERT

CREATE TABLE db (a INT PRIMARY KEY, b TEXT);

CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
    LOOP
        -- first try to update the key
        UPDATE db SET b = data WHERE a = key;
        IF found THEN
            RETURN;
        END IF;
        -- not there, so try to insert the key
        -- if someone else inserts the same key concurrently,
        -- we could get a unique-key failure
        BEGIN
            INSERT INTO db(a,b) VALUES (key, data);
            RETURN;
        EXCEPTION WHEN unique_violation THEN
            -- do nothing, and loop to try the UPDATE again
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');

提交更正

译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。