35.7. 控制结构 #
控制结构可能是PL/pgSQL中最有用的(以及最重要)的部分了。利用PL/pgSQL的控制结构,你可以以非常灵活而且强大的方法操纵PostgreSQL的数据。
35.7.1. 从函数返回 #
有两个命令让我们能够从函数中返回数据:RETURN和RETURN NEXT。
35.7.1.1. RETURN
RETURN expression; 带有一个表达式的RETURN用于终止函数并把expression的值返回给调用者。这种形式被用于不返回集合的PL/pgSQL函数。
如果函数返回的是标量类型,任何表达式都可以使用。表达式结果会按照赋值部分的说明自动转换为函数的返回类型。但要返回一个复合(行)值,你必须把记录变量或行变量写为
expression。
一个函数的返回值不能是未定义。如果控制到达了函数最顶层块的末尾而没有碰到一个RETURN语句,那么会发生一个运行时错误。
如果你已把函数声明为返回void,那么仍然必须提供RETURN语句;但在这种情况下,RETURN后面的表达式是可选的,如果出现也会被忽略。
35.7.1.2. RETURN NEXT
RETURN NEXT expression; 当 PL/pgSQL 函数被声明为返回 SETOF 时,返回过程会略有不同。在这种情况下,要返回的各个项通过一系列 sometypeRETURN NEXT 命令指定,最后再用一个不带参数的 RETURN 命令表明函数已经执行完毕。RETURN NEXT 可用于标量和复合数据类型;对于后一种情况,会返回完整的结果“表”。
使用RETURN NEXT的函数应当按照下面的方式调用:
SELECT * FROM some_func();
也就是说,该函数必须作为表源在FROM子句中使用。
RETURN NEXT 实际上不会从函数中返回;它只是把表达式的值保存起来。然后会继续执行PL/pgSQL函数中的下一条语句。随着后继的RETURN NEXT命令的执行,结果集就建立起来了。最后一个RETURN(应该没有参数)会导致控制退出该函数。
注意
如上所述,目前 PL/pgSQL 的 RETURN NEXT 实现在从函数返回之前会把整个结果集都保存起来。这意味着如果一个PL/pgSQL函数生成一个非常大的结果集,性能可能会很差:数据将被写到磁盘上以避免内存耗尽,但是函数本身在整个结果集都生成之前不会退出。将来的PL/pgSQL版本可能会允许用户定义没有这种限制的集合返回函数。目前,数据开始被写入到磁盘的时机由配置变量work_mem控制。拥有足够内存来存储大型结果集的管理员可以考虑增大这个参数。
35.7.2. 条件语句 #
IF 语句让你能够根据特定条件执行命令。
PL/pgSQL 有五种形式的 IF:
IF ... THENIF ... THEN ... ELSEIF ... THEN ... ELSE IFIF ... THEN ... ELSIF ... THEN ... ELSEIF ... THEN ... ELSEIF ... THEN ... ELSE
35.7.2.1. IF-THEN
IFboolean-expressionTHENstatementsEND 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;
35.7.2.2. IF-THEN-ELSE
IFboolean-expressionTHENstatementsELSEstatementsEND 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;
35.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也需要一个。这是可行的,但当要检查的备选情况很多时就变得繁琐了。因此有了下一种形式。
35.7.2.4. IF-THEN-ELSIF-ELSE
IFboolean-expressionTHENstatements[ ELSIFboolean-expressionTHENstatements[ ELSIFboolean-expressionTHENstatements...]] [ ELSEstatements] 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;
35.7.2.5. IF-THEN-ELSEIF-ELSE
ELSEIF是ELSIF的一个别名。
35.7.3. 简单循环 #
使用 LOOP、EXIT、WHILE
和 FOR 语句,可以让你的
PL/pgSQL 函数重复执行一系列命令。
35.7.3.1. LOOP
[<<label>>] LOOPstatementsEND LOOP;
LOOP 定义了一个无条件循环,它会无限重复,直到被
EXIT 或 RETURN 语句终止。可选的标签可以被嵌套循环中的 EXIT 语句使用,以指定应终止哪一层嵌套。
35.7.3.2. EXIT
EXIT [label] [ WHENexpression];
如果没有给出
label,那么最内层的循环会被终止,然后跟在 END
LOOP 后面的语句会被执行。如果给出了
label,那么它必须是当前或者更高层的嵌套循环或者语句块的标签。然后该命名循环或块就会被终止,并且控制会转移到该循环/块相应的
END 之后的语句上。
如果出现了 WHEN,则只有当指定条件为真时才会退出循环,否则控制传递到 EXIT 之后的语句。
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;
BEGIN
-- some computations
IF stocks > 100000 THEN
EXIT; -- causes exit from the BEGIN block
END IF;
END;
35.7.3.3. WHILE
[<<label>>] WHILEexpressionLOOPstatementsEND LOOP;
只要条件表达式的计算结果为真,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;
35.7.3.4. FOR(整型变体)
[<<label>>] FORnameIN [ REVERSE ]expression..expressionLOOPstatementsEND LOOP;
这种形式的 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
情况下是小于),循环体根本不会被执行。而且不会抛出任何错误。
35.7.4. 遍历查询结果 #
使用另一种形式的 FOR 循环,可以遍历查询的结果并相应地处理这些数据。语法是:
[<<label>>] FORrecord_or_rowINqueryLOOPstatementsEND LOOP;
记录或行变量会被依次赋予 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>>] FORrecord_or_rowIN EXECUTEtext_expressionLOOPstatementsEND LOOP;
它与前一种形式类似,只是源 SELECT 语句被指定为一个字符串表达式,在每次进入 FOR 循环时都会重新求值和重新规划。这让程序员可以在预先规划的查询的速度与动态查询的灵活性之间做出选择,就像使用普通的 EXECUTE 语句一样。
注意
PL/pgSQL 解析器目前通过检查 IN 和 LOOP 之间、任何括号之外是否出现 .. 来区分两类 FOR 循环(整数循环和查询结果循环)。如果没有看到 ..,就假定该循环是遍历行的循环。因此,如果把 .. 打错,很可能得到类似“遍历行的循环的循环变量必须是记录或行变量”这样的报错,而不是人们预想的简单语法错误。
35.7.5. 捕获错误 #
默认情况下,PL/pgSQL 函数中发生的任何错误都会中止函数及其外围事务的执行。你可以使用带有
EXCEPTION 子句的
BEGIN 块来捕获错误并从中恢复。其语法是在普通
BEGIN 块语法上的扩展:
[ <<label>> ] [ DECLAREdeclarations] BEGINstatementsEXCEPTION WHENcondition[ ORcondition... ] THENhandler_statements[ WHENcondition[ ORcondition... ] THENhandler_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。
例 35.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');