40.6. 控制结构

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

40.6.1. 从函数返回

有两个命令可以用来从函数中返回数据:RETURNRETURN NEXT

40.6.1.1. RETURN

RETURN expression;

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

如果函数中返回标量类型,那么表达式结果将被自动转换成函数的返回类型, 就像在赋值中描述的那样。但是要返回一个复合(行)数值, 你必须写一个准确提供需求列集合的表达式,这可能需要显式转换。

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

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

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

例子:

-- 返回一个标量类型函数
RETURN 1 + 2;
RETURN scalar_var;
-- 返回复合类型函数
RETURN composite_type_var;
RETURN (1, 2, 'three'::text);  -- must cast columns to correct types

40.6.1.2. RETURN NEXTRETURN QUERY

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

如果一个PL/pgSQL函数声明为返回SETOF sometype, 那么遵循的过程则略有不同。 在这种情况下,要返回的独立项是在RETURN NEXT或者 RETURN QUERY命令里声明的, 然后最后有一个不带参数的RETURN命令用于告诉这个函数已经完成执行了。 RETURN NEXT可以用于标量和复合数据类型;对于复合类型, 将返回一个完整的结果"table"RETURN QUERY命令将一条查询的结果追加到一个函数的结果集中。 RETURN NEXTRETURN QUERY在单一集合返回 函数中自由混合,在这种情况下,结果将被级联。

RETURN NEXTRETURN QUERY实际上不会从函数中返回, 它们是将零或者多个行追加到函数的结果集中。 然后继续执行PL/pgSQL函数里的下一条语句。 随着后继的RETURN NEXT或者RETURN 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 get_all_foo() RETURNS SETOF foo AS
$BODY$
DECLARE
    r foo%rowtype;
BEGIN
    FOR r IN
        SELECT * FROM foo WHERE fooid > 0
    LOOP
   
-- 可以在这里做一些处理
        RETURN NEXT r; -- return current row of SELECT
    END LOOP;
    RETURN;
END
$BODY$
LANGUAGE plpgsql;

SELECT * FROM get_all_foo();

这是一个使用RETURN QUERY的函数例子:

CREATE FUNCTION get_available_flightid(date) RETURNS SETOF integer AS
$BODY$
BEGIN
    RETURN QUERY SELECT flightid
                   FROM flight
                  WHERE flightdate >= $1
                    AND flightdate < ($1 + 1);
    
-- 由于没有完成执行,我们可以检查行是否返回并且如果没有则抛出异常。
    IF NOT FOUND THEN
        RAISE EXCEPTION 'No flight at %.', $1;
    END IF;

    RETURN;
 END
$BODY$
LANGUAGE plpgsql;
-- 如果没有可用航班,则返回可用航班或者抛出异常。
SELECT * FROM get_available_flightid(CURRENT_DATE);

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

40.6.2. 条件

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

以及两种形式的CASE:

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

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

40.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
    
-- 唯一可能性是号码为空
    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;

然而,这个方法需要为每个IFEND IF, 因此当有很多选择时,这种方法明显比ELSIF繁琐。

40.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子句中的每个表达式。 如果找到匹配,那么相应的statements被执行, 然后控制在END CASE之后传递到下一个语句。 (随后的WHEN表达式不被评估。) 如果没有发现匹配,执行ELSE statements; 但如果ELSE是不存在的,然后引发CASE_NOT_FOUND异常。

例如:

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

40.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异常。

例如:

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子句结果而不是什么都不做的规则。

40.6.3. 简单循环

使用LOOP, EXIT,CONTINUE, WHILEFORFOREACH语句, 可以控制PL/pgSQL函数重复一系列命令。

40.6.3.1. 循环

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

LOOP定义一个无条件的循环,无限循环, 直到由EXIT或者 RETURN语句终止。 可选的label可以由EXITCONTINUE语句使用, 用于在嵌套循环中声明应该应用于哪一层循环。

40.6.3.2. 退出

EXIT [ label ] [ WHEN boolean-expression ];

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

如果声明了WHEN, 循环退出只有在boolean-expression为真的时候才发生, 否则控制会落到EXIT后面的语句上。

EXIT可以用于在所有的循环类型中,它并不仅仅限制于在无条件循环中使用。

在和BEGIN块一起使用的时候,EXIT把控制交给块结束后的下一个语句。 需要注意的是,一个标签必须用于这个目的; 一个没有标记的EXIT永远无法与BEGIN进行匹配。 (这是PostgreSQL 8.4之前版本的一个变化, 这将允许未标记EXIT匹配BEGIN块)。

例如:

LOOP
    
-- 一些计算
    IF count > 0 THEN
        EXIT;  -- exit loop
    END IF;
END LOOP;

LOOP
    
-- 一些计算
EXIT WHEN count > 0;  -- 和前面的例子相同结果
END LOOP;

<<ablock>>
BEGIN
    
-- 一些计算
    IF stocks > 100000 THEN
    EXIT ablock;  -- 导致从BEGIN块退出
    END IF;
 -- 忽略这儿的计算,当stocks > 100000时
END;

40.6.3.3. CONTINUE

CONTINUE [ label ] [ WHEN boolean-expression ];

如果没有给出label,那么就开始最内层循环的下一次执行。 也就是说,控制传递回给循环控制表达式(如果有),然后重新计算循环体。 如果出现了label,它声明即将继续执行的循环的标签。

如果声明了WHEN,那么循环的下一次执行只有 在boolean-expression为真的情况下才进行。 否则,控制传递给CONTINUE后面的语句。

CONTINUE可以用于所有类型的循环;它并不仅仅限于无条件循环。

例如:

LOOP
    
-- 一些计算
    EXIT WHEN count > 100; 
    CONTINUE WHEN count < 50;
-- 在[50 .. 100]内的计算
END LOOP;

40.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
    -- 这里的一些计算
END LOOP;

WHILE NOT done LOOP
    -- 这里的一些计算
END LOOP;

40.6.3.5. FOR (Integer 变量)

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

这种形式的FOR对一定范围的整数进行迭代的循环。 变量name会自动定义为BY类型并且只在循环里存在 (任何该变量名的现存定义在此循环内都将被忽略)。 给出范围上下界的两个表达式在进入循环的时候计算一次。 BY子句指定迭代步长(缺省为 1), 但如果声明了REVERSE步长将变为相应的负值。

一些整数FOR循环的例子:

FOR i IN 1..10 LOOP
    -- 我将在值1,2,3,4,5,6,7,8,9,10中循环
END LOOP;

FOR i IN REVERSE 10..1 LOOP
    -- 将在值10,9,8,7,6,5,4,3,2,1中循环
END LOOP;

FOR i IN REVERSE 10..1 BY 2 LOOP
    -- 将在值10,8,6,4,2中循环
END LOOP;

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

如果label被附加到FOR循环,那么整数循环变量 可以使用label引用适当名称。

40.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
    RAISE NOTICE 'Refreshing materialized views...';

    FOR mviews IN SELECT * FROM cs_materialized_views ORDER BY sort_key LOOP
        
-- 现在"mviews"里有了一条来自 cs_materialized_views 的记录 

        RAISE NOTICE 'Refreshing materialized view %s ...', 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;

    RAISE NOTICE 'Done refreshing materialized views.';
    RETURN 1;
END;
$$ LANGUAGE plpgsql;

如果循环是用一个EXIT语句终止的, 那么在循环之后你仍然可以访问最后赋值的行。

FOR语句中使用的这种query可以是任何返回行的SQL命令, 通常是SELECT,不过带有RETURNING子句的INSERT, UPDATEDELETE也是可以的, 一些诸如EXPLAIN之类的命令也可以。

PL/pgSQL变量代替查询文本,并且查询计划为了重新使用被缓存,正如 第 40.10.1 节第 40.10.2 节

FOR-IN-EXECUTE语句是遍历所有行的另外一种方法:

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

这个例子类似前面的形式,只不过源查询语句声明为了一个字符串表达式, 这样它在每次进入FOR循环的时候都会重新计算和生成执行计划。 这样就允许程序员在一个预先规划好了的命令所获得的速度和一个动态命令所获得的灵活性 (就像一个简单的EXECUTE语句那样)之间进行选择。 当使用EXECUTE时, 可以通过USING将参数值插入到动态命令中。

对于一个需要将结果迭代的查询, 另外一个声明的方法是将它定义为游标(cursor), 可参阅第 40.7.4 节

40.6.5. 遍历数组

FOREACH循环类似于FOR循环, 但不是遍历SQL查询返回的行,它遍历数组值元素。 (一般而言,FOREACH是遍历复合值表达式组成部分; 循环遍历除数组外的复合值变量将来可以被添加。) FOREACH语句循环数组是:

[ <<label>> ]
FOREACH target [ SLICE number ] IN ARRAY expression LOOP
    statements
END LOOP [ label ];

没有SLICE,或者如果声明SLICE 0,则 循环遍历通过评估expression产生的数组的单个元素。 target变量分配每个 序列中的元素值,并为每个元素执行循环体。 这里是遍历整数数组元素的一个例子:

CREATE FUNCTION sum(int[]) RETURNS int8 AS $$
DECLARE
  s int8 := 0;
  x int;
BEGIN
  FOREACH x IN ARRAY $1
  LOOP
    s := s + x;
  END LOOP;
  RETURN s;
END;
$$ LANGUAGE plpgsql;

元素以存储顺序进行访问,不论数组维数的数量。尽管target 通常只是一个单一的变量,当循环复合值的数组(记录)时,它可以是一个变量列表, 在这种情况下,每个数组元素,从连续的复合值列中分配变量。

以正数SLICE值,FOREACH遍历数组的元素部分,而不是单一元素。 SLICE的值必须是不大于数组维数的整数常数。 target变量必须是一个数组, 并且它接收数组值的连续片段,而每个片段 是通过SLICE指定的维数。这里是遍历一维切片的一个例子:

CREATE FUNCTION scan_rows(int[]) RETURNS void AS $$
DECLARE
  x int[];
BEGIN
  FOREACH x SLICE 1 IN ARRAY $1
  LOOP
    RAISE NOTICE 'row = %', x;
  END LOOP;
END;
$$ LANGUAGE plpgsql;

SELECT scan_rows(ARRAY[[1,2,3],[4,5,6],[7,8,9],[10,11,12]]);

NOTICE:  row = {1,2,3}
NOTICE:  row = {4,5,6}
NOTICE:  row = {7,8,9}
NOTICE:  row = {10,11,12}

40.6.6. 捕获错误

缺省时,一个在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

例 40-2. 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
    
-- 第一次尝试更新key
        UPDATE db SET b = data WHERE a = key;
        IF found THEN
            RETURN;
        END IF;
-- 不存在,所以尝试插入key,如果其他人同时插入相同的key,我们可能得到唯一key失败。
        BEGIN
            INSERT INTO db(a,b) VALUES (key, data);
            RETURN;
        EXCEPTION WHEN unique_violation THEN
    -- 什么也不做,并且循环尝试再次更新。
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

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

这个代码假设通过INSERT不是说表上触发器函数中的INSERT 产生unique_violation错误,如果表上有超过一个以上的唯一索引,它可能行为不端, 因为将重试操作不论哪个索引产生错误。 可以通过特性讨论下一步检查捕获的错误是预期的来获取更高的安全性。

40.6.6.1. 获得有关错误的信息

异常处理程序经常需要确定发生的具体错误。有两种方法来获得 当前PL/pgSQL异常: 特殊变量和GET STACKED DIAGNOSTICS命令 的有关信息。

在一个异常处理程序中,特殊变量 SQLSTATE包含相当于发生异常的错误代码 (参考表 A-1获得可能错误代码列)。 特殊变量SQLERRM包含与异常有关的错误消息。 这些变量是在异常处理外未被定义的。

在一个异常处理程序中,也可以检索关于使用GET STACKED DIAGNOSTICS命令的当前异常信息,形成了:

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

每个item是识别被分配到指定变量的状态值(应该是接收它的正确数据类型)的一个关键字。 目前可用的状态显示在表 40-1中。

表 40-1. 错误诊断值

名字类型描述
RETURNED_SQLSTATEtext异常的SQLSTATE错误代码
COLUMN_NAMEtext与异常相关的列名
CONSTRAINT_NAMEtext与异常相关的约束名
PG_DATATYPE_NAMEtext与异常相关的数据类型名
MESSAGE_TEXTtext异常的主要消息文本
TABLE_NAMEtext与异常相关的表名
SCHEMA_NAMEtext与异常相关的模式名
PG_EXCEPTION_DETAILtext异常的详细消息文本,如果有的话
PG_EXCEPTION_HINTtext异常的提示消息文本,如果有的话
PG_EXCEPTION_CONTEXTtext描述调用堆栈的文本线程

如果异常没有设置项值,则返回空字符串。

例子:

DECLARE
  text_var1 text;
  text_var2 text;
  text_var3 text;
BEGIN
  
  -- 一些处理可能引起异常
  ...
EXCEPTION WHEN OTHERS THEN
  GET STACKED DIAGNOSTICS text_var1 = MESSAGE_TEXT,
                          text_var2 = PG_EXCEPTION_DETAIL,
                          text_var3 = PG_EXCEPTION_HINT;
END;