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

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

36.7. 控制结构 #

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

36.7.1. 从函数返回 #

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

36.7.1.1. RETURN

RETURN expression;

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

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

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

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

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

36.7.1.2. RETURN NEXT

RETURN NEXT expression;

当 PL/pgSQL 函数被声明为返回 SETOF sometype 时,返回过程会略有不同。在这种情况下,要返回的各个项通过一系列 RETURN NEXT 命令指定,最后再用一个不带参数的 RETURN 命令表明函数已经执行完毕。RETURN NEXT 可用于标量和复合数据类型;对于复合结果类型,会返回完整的结果“表”。

RETURN NEXT 实际上不会从函数中返回 — 它只是把表达式的值保存起来。然后会继续执行PL/pgSQL函数中的下一条语句。随着后继的RETURN NEXT命令的执行,结果集就建立起来了。最后一个RETURN(应该没有参数)会导致控制退出该函数(或者你可以让控制到达函数的结尾)。

如果函数是用输出参数声明的,只需写不带表达式的 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();

也就是说,该函数必须作为表源在FROM子句中使用。

注意

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

36.7.2. 条件语句 #

IF 语句让你能够根据特定条件执行命令。 PL/pgSQL 有五种形式的 IF:

  • IF ... THEN

  • IF ... THEN ... ELSE

  • IF ... THEN ... ELSE IF

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

  • IF ... THEN ... ELSEIF ... THEN ... ELSE

36.7.2.1. IF-THEN

IF boolean-expression THEN
    statements
END IF;

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

示例:

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

36.7.2.2. IF-THEN-ELSE

IF boolean-expression THEN
    statements
ELSE
    statements
END IF;

IF-THEN-ELSE 语句对 IF-THEN 进行了增加,它让你能够指定一组在条件求值为假时应该被执行的语句。

示例:

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;

36.7.2.3. IF-THEN-ELSE IF

IF语句可以嵌套,如下例所示:

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

使用这种形式时,你实际上是把一个IF语句嵌套在外层IF语句的ELSE部分中。这样,每个嵌套的IF都需要一个END IF语句,外层的IF-ELSE也需要一个。这是可行的,但当要检查的备选情况很多时就变得繁琐了。因此有了下一种形式。

36.7.2.4. IF-THEN-ELSIF-ELSE

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

IF-THEN-ELSIF-ELSE提供了一种在一条语句中检查多种备选情况的更方便的方法。在功能上它等价于嵌套的IF-THEN-ELSE-IF-THEN命令,但只需要一个END IF。

这里有一个示例:

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;

36.7.2.5. IF-THEN-ELSEIF-ELSE

ELSEIF是ELSIF的一个别名。

36.7.3. 简单循环 #

使用 LOOP、EXIT、 CONTINUE、WHILE 和 FOR 语句,可以让你的 PL/pgSQL 函数重复执行一系列命令。

36.7.3.1. LOOP

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

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

36.7.3.2. EXIT

EXIT [ label ] [ WHEN expression ];

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

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

EXIT 可以与所有类型的循环一起使用;它并 不限于在无条件循环中使用。当与 BEGIN 块一起使用时, EXIT 会把控制转交给该块结束后的下一条 语句。

示例:

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;  -- causes exit from the BEGIN block
    END IF;
    -- computations here will be skipped when stocks > 100000
END;

36.7.3.3. CONTINUE

CONTINUE [ label ] [ WHEN expression ];

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

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

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

示例:

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

36.7.3.4. WHILE

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

只要条件表达式的计算结果为真,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;

36.7.3.5. FOR(整型变体)

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

这种形式的 FOR 会创建一个在一个整数范围 上迭代的循环。变量 name 会自动定义 为类型 integer 并且只在循环内存在。给出范围上下界 的两个表达式在进入循环的时候计算一次。迭代步长通常为 1,但在 指定了 REVERSE 时为 -1。

整数 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;

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

36.7.4. 遍历查询结果 #

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

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

记录或行变量会被依次赋予query(它必须是一个SELECT命令)产生的每一行,并且循环体对每一行都执行一次。这里有一个示例:

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-IN-EXECUTE 语句是另一种遍历行的方式:

[ <<label>> ]
FOR record_or_row IN EXECUTE text_expression LOOP
    statements
END LOOP [ label ];

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

注意

PL/pgSQL 解析器目前通过检查 IN 和 LOOP 之间、任何括号之外是否出现 .. 来区分两类 FOR 循环(整数循环和查询结果循环)。如果没有看到 ..,就假定该循环是遍历行的循环。因此,如果把 .. 打错,很可能得到类似“遍历行的循环的循环变量必须是记录或行变量或者标量变量列表”这样的报错,而不是人们预想的简单语法错误。

36.7.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)。条件名是大小写无关的。

如果在选中的 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 变量包含与该异常关联的错误消息。 这些变量在异常处理器之外是未定义的。

例 36.1. UPDATE/INSERT 与异常

本例使用异常处理来按需执行 UPDATE 或 INSERT:

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