pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
控制结构可能是PL/pgSQL中最有用的(以及最重要)的部分了。利用PL/pgSQL的控制结构,你可以以非常灵活而且强大的方法操纵PostgreSQL的数据。
有两个命令让我们能够从函数中返回数据:RETURN和RETURN NEXT。
RETURN
RETURN expression;
带有一个表达式的RETURN用于终止函数并把expression的值返回给调用者。这种形式被用于不返回集合的PL/pgSQL函数。
如果函数返回的是标量类型,任何表达式都可以使用。表达式结果会 按照赋值部分的说明自动转换为函数的返回类型。但要返回一个 复合(行)值,你必须把记录变量或行变量写为 expression。
如果你声明带输出参数的函数,那么就只需要写不带表达式的RETURN。输出参数变量的当前值将被返回。
如果你声明函数返回void,一个RETURN语句可以被用来提前退出函数;但是不要在RETURN后面写一个表达式。
一个函数的返回值不能是未定义。如果控制到达了函数最顶层块的末尾而没有碰到一个RETURN语句,那么会发生一个运行时错误。不过,这个限制不适用于带输出参数的函数以及返回void的函数。在这些情况中,如果顶层的块结束,将自动执行一个RETURN语句。
RETURN NEXT and RETURN QUERYRETURN NEXTexpression; RETURN QUERYquery;
当 PL/pgSQL 函数被声明为返回 SETOF 时,返回过程会略有不同。在这种情况下,要返回的各个项通过一系列 sometypeRETURN NEXT 或 RETURN QUERY 命令指定,最后再用一个不带参数的 RETURN 命令表明函数已经执行完毕。RETURN NEXT 可用于标量和复合数据类型;对于复合结果类型,会返回完整的结果“表”。RETURN QUERY 会把查询执行结果追加到函数的结果集中。在同一个集合返回函数中,RETURN NEXT 和 RETURN QUERY 可以自由混用,此时它们的结果会被串接起来。
RETURN NEXT和RETURN QUERY实际上不会从函数中返回 — 它们简单地向函数的结果集中追加零或多行。然后会继续执行PL/pgSQL函数中的下一条语句。随着后继的RETURN NEXT和RETURN QUERY命令的执行,结果集就建立起来了。最后一个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();
注意,使用RETURN NEXT或RETURN QUERY的函数必须作为表源在FROM子句中调用。
如上所述,目前RETURN NEXT和RETURN QUERY的实现在从函数返回之前会把整个结果集都保存起来。这意味着如果一个PL/pgSQL函数生成一个非常大的结果集,性能可能会很差:数据将被写到磁盘上以避免内存耗尽,但是函数本身在整个结果集都生成之前不会退出。将来的PL/pgSQL版本可能会允许用户定义没有这种限制的集合返回函数。目前,数据开始被写入到磁盘的时机由配置变量work_mem控制。拥有足够内存来存储大型结果集的管理员可以考虑增大这个参数。
IF 语句让你能够根据特定条件执行命令。 PL/pgSQL 有五种形式的 IF:
IF ... THEN
IF ... THEN ... ELSE
IF ... THEN ... ELSE IF
IF ... THEN ... ELSIF ... THEN ... ELSE
IF ... THEN ... ELSEIF ... THEN ... ELSE
IF-THENIFboolean-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;
IF-THEN-ELSEIFboolean-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;
IF-THEN-ELSE IFIF语句可以嵌套,如下例所示:
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也需要一个。这是可行的,但当要检查的备选情况很多时就变得繁琐了。因此有了下一种形式。
IF-THEN-ELSIF-ELSEIFboolean-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;
IF-THEN-ELSEIF-ELSEELSEIF是ELSIF的一个别名。
使用 LOOP、EXIT、 CONTINUE、WHILE 和 FOR 语句,可以让你的 PL/pgSQL 函数重复执行一系列命令。
LOOP[ <<label>> ] LOOPstatementsEND LOOP [label];
LOOP 定义了一个无条件循环,它会无限重复,直到被 EXIT 或 RETURN 语句终止。可选的 label 可以供嵌套循环中的 EXIT 和 CONTINUE 语句使用, 以指定这些语句作用的是哪一个循环。
EXITEXIT [label] [ WHENboolean-expression];
如果没有给出 label,那么最内层的循环会被终止, 然后跟在 END LOOP 后面的语句会被执行。如果给出了 label,那么它必须是当前或者更高 层的嵌套循环或者语句块的标签。然后该命名循环或块就会被终止, 并且控制会转移到该循环/块相应的 END 之后的语句上。
如果指定了 WHEN,则只有在 boolean-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;
CONTINUECONTINUE [label] [ WHENboolean-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;
WHILE[ <<label>> ] WHILEboolean-expressionLOOPstatementsEND 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;
FOR(整型变体) #[ <<label>> ] FORnameIN [ REVERSE ]expression..expression[ BYexpression] LOOPstatementsEND 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 限定的名称来引用。
使用另一种形式的 FOR 循环,可以遍历查询的 结果并相应地处理这些数据。语法是:
[ <<label>> ] FORtargetINqueryLOOPstatementsEND 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 子句的 INSERT、UPDATE 或 DELETE。某些工具命令(如 EXPLAIN)也可以。
PL/pgSQL 变量会被替换到查询文本中, 并且查询计划会被缓存以备可能的重用,详见 第 38.10.1 节 和 第 38.10.2 节。
FOR-IN-EXECUTE 语句是另一种遍历行的方式:
[ <<label>> ] FORtargetIN EXECUTEtext_expressionLOOPstatementsEND LOOP [label];
它与前一种形式类似,只是源查询被指定为一个字符串表达式, 在每次进入 FOR 循环时都会重新求值和重新规划。 这让程序员可以在预先规划的查询的速度与动态查询的灵活性之间 做出选择,就像使用普通的 EXECUTE 语句一样。
默认情况下,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。
在异常处理器中,SQLSTATE 变量包含与所引发异常对应的错误代码(可能错误代码的列表参见 表 A.1)。 SQLERRM 变量包含与该异常关联的错误消息。 这些变量在异常处理器之外是未定义的。
例 38.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 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。