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 和 RETURN QUERYRETURN NEXTexpression; RETURN QUERYquery; RETURN QUERY EXECUTEcommand-string[ USINGexpression[, ... ] ];
当 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 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 NEXT和RETURN QUERY的实现在从函数返回之前会把整个结果集都保存起来。这意味着如果一个PL/pgSQL函数生成一个非常大的结果集,性能可能会很差:数据将被写到磁盘上以避免内存耗尽,但是函数本身在整个结果集都生成之前不会退出。将来的PL/pgSQL版本可能会允许用户定义没有这种限制的集合返回函数。目前,数据开始被写入到磁盘的时机由配置变量work_mem控制。拥有足够内存来存储大型结果集的管理员可以考虑增大这个参数。
IF 和 CASE 语句让你可以根据条件执行不同的命令。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
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 进行了增加,它让你能够指定一组在 条件不为真时应该被执行的语句(注意这也包括条件求值为 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;
IF-THEN-ELSIFIFboolean-expressionTHENstatements[ ELSIFboolean-expressionTHENstatements[ ELSIFboolean-expressionTHENstatements...]] [ ELSEstatements] 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 笨拙得多。
CASECASEsearch-expressionWHENexpression[,expression[ ... ]] THENstatements[ WHENexpression[,expression[ ... ]] THENstatements... ] [ ELSEstatements] END CASE;
CASE 的简单形式提供基于操作数相等的条件执行。 search-expression 被求值(一次),并依次与 WHEN 子句中的每个 expression 比较。如果找到匹配,则执行相应的 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;
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 表达式不会被求值。)如果没有找到为真的结果, 则执行 ELSE 的 statements;但如果没有 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 子句时会报错,而不是什么也不做。
使用 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 会把控制转交给该块结束后的下一条 语句。需要注意的是,为此必须使用标签;未加标签的 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;
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 变量会被替换到查询文本中, 并且查询计划会被缓存以备可能的重用,详见 第 39.10.1 节 和 第 39.10.2 节。
FOR-IN-EXECUTE 语句是另一种遍历行的方式:
[ <<label>> ] FORtargetIN EXECUTEtext_expression[ USINGexpression[, ... ] ] LOOPstatementsEND LOOP [label];
它与前一种形式类似,只是源查询被指定为一个字符串表达式, 在每次进入 FOR 循环时都会重新求值和重新规划。 这让程序员可以在预先规划的查询的速度与动态查询的灵活性之间 做出选择,就像使用普通的 EXECUTE 语句一样。 与 EXECUTE 一样,可以通过 USING 向动态命令中插入参数值。
指定要遍历其结果的查询的另一种方式,是把它声明为游标。这 在 第 39.7.4 节 中描述。
默认情况下,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)。条件名是大小写无关的。一个 错误条件也可以通过 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 与异常
本例使用异常处理来按需执行 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 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。