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 v_user_id <> 0 THEN
UPDATE users SET email = v_email WHERE user_id = v_user_id;
END IF;
IF-THEN-ELSE语句对IF-THEN进行了增加,它让你能够指定一组在条件不为真时应该被执行的语句(注意这也包括条件为 NULL 的情况)。
另一个可以完成相同任务的方法是嵌套 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 异常。
下面是一个例子:
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和FOREACH语句,可以让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 块。)
例子:
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
-- 一些计算
IF count > 0 THEN
EXIT; -- 退出循环
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;
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];
The target is a record variable, row variable, or comma-separated list of scalar variables. The target is successively assigned each row resulting from the query and the loop body is executed for each row. Here is an example:
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
-- Now "mviews" has one record from 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;
If the loop is terminated by an EXIT statement, the last assigned row value is still accessible after the loop.
这种形式的 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 节 中描述。
FOREACH循环与FOR循环很相似,但它不是迭代一个 SQL 查询返回的行,而是迭代一个数组值的元素。(一般而言,FOREACH旨在循环遍历组合值表达式的组成部分;将来可能会增加遍历数组之外的其他组合的变体。)用于遍历数组的FOREACH语句是:
[ <<label>> ] FOREACHtarget[ SLICEnumber] IN ARRAYexpressionLOOPstatementsEND 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;
The elements are visited in storage order, regardless of the number of array dimensions. Although the target is usually just a single variable, it can be a list of variables when looping through an array of composite values (records). In that case, for each array element, the variables are assigned from successive columns of the composite value.
当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}
默认情况下,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。
例 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');
这段代码假定 unique_violation 错误是由 INSERT 引起的,而不是由表上的某个触发器函数中的 INSERT 之类引起。如果表上有多个唯一索引,它也可能 表现不当,因为无论错误由哪个索引引起,它都会重试该操作。使用下一节 讨论的特性来检查被捕获的错误是否就是预期的错误,可以获得更多的安全性。
异常处理器经常需要识别所发生的具体错误。有两种方法可以获取PL/pgSQL中当前异常的信息:特殊变量和GET STACKED DIAGNOSTICS命令。
在一个异常处理器内,特殊变量SQLSTATE包含了对应于被抛出异常的错误代码(可能的错误代码列表见表 A.1)。特殊变量SQLERRM包含与该异常相关的错误消息。这些变量在异常处理器外是未定义的。
在一个异常处理器内,我们也可以用GET STACKED DIAGNOSTICS命令检索有关当前异常的信息,该命令的形式为:
GET STACKED DIAGNOSTICSvariable=item[ , ... ];
每个item是一个关键词,它标识一个被赋予给指定变量(应该具有接收该值的正确数据类型)的状态值。表 39.1中显示了当前可用的状态项。
表 39.1. 错误诊断项
| 名称 | 类型 | 描述 |
|---|---|---|
RETURNED_SQLSTATE |
text | 该异常的 SQLSTATE 错误代码 |
MESSAGE_TEXT |
text | 该异常的主要消息的文本 |
PG_EXCEPTION_DETAIL |
text | 该异常的详细消息文本(如果有) |
PG_EXCEPTION_HINT |
text | 该异常的提示消息文本(如果有) |
PG_EXCEPTION_CONTEXT |
text | 描述产生异常时调用栈的文本行 |
如果异常没有为一个项设置值,将返回一个空字符串。
这里是一个示例:
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;
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。