PostgreSQL 9.4.4 中文手册 | |||
---|---|---|---|
上一页 | 上一级 | 章 40. PL/pgSQL - SQL过程语言 | 下一页 |
控制结构可能是PL/pgSQL中最有用的(以及最重要)的部分了。 利用PL/pgSQL的控制结构, 你可以以非常灵活而且强大的方法操纵PostgreSQL的数据。
有两个命令可以用来从函数中返回数据:RETURN和 RETURN NEXT。
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
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 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 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 NEXT和RETURN QUERY 实现在从函数返回之前把整个结果集都保存起来, 就像上面描述的那样。这意味着如果一个PL/pgSQL函数生成一个非常大的结果集, 性能可能会很差:数据将被写到磁盘上以避免内存耗尽, 但是函数在完成整个结果集的生成之前不会退出。 将来的PL/pgSQL版本可能会允许用户定义没有这样限制的返回集合的函数。 目前,数据开始向磁盘里写的时刻是由配置变量work_mem控制的。 拥有足够内存的管理员如果想在内存里存储更大的结果集, 则可以考虑把这个参数增大一些。
IF和CASE语句让你可以根据某种条件执行命令。 PL/pgSQL有三种形式的IF:
IF ... THEN
IF ... THEN ... ELSE
IF ... THEN ... ELSIF ... THEN ... ELSE
以及两种形式的CASE:
CASE ... WHEN ... THEN ... ELSE ... END CASE
CASE WHEN ... THEN ... ELSE ... END CASE
IF boolean-expression THEN statements END 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 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;
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;
然而,这个方法需要为每个IF写END IF, 因此当有很多选择时,这种方法明显比ELSIF繁琐。
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;
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子句结果而不是什么都不做的规则。
使用LOOP, EXIT,CONTINUE, WHILE, FOR和FOREACH语句, 可以控制PL/pgSQL函数重复一系列命令。
[ <<label>> ] LOOP statements END LOOP [ label ];
LOOP定义一个无条件的循环,无限循环, 直到由EXIT或者 RETURN语句终止。 可选的label可以由EXIT 和CONTINUE语句使用, 用于在嵌套循环中声明应该应用于哪一层循环。
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;
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;
[ <<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;
[ <<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引用适当名称。
使用不同类型的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, UPDATE 或DELETE也是可以的, 一些诸如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 节。
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}
缺省时,一个在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错误,如果表上有超过一个以上的唯一索引,它可能行为不端, 因为将重试操作不论哪个索引产生错误。 可以通过特性讨论下一步检查捕获的错误是预期的来获取更高的安全性。
异常处理程序经常需要确定发生的具体错误。有两种方法来获得 当前PL/pgSQL异常: 特殊变量和GET STACKED DIAGNOSTICS命令 的有关信息。
在一个异常处理程序中,特殊变量 SQLSTATE包含相当于发生异常的错误代码 (参考表 A-1获得可能错误代码列)。 特殊变量SQLERRM包含与异常有关的错误消息。 这些变量是在异常处理外未被定义的。
在一个异常处理程序中,也可以检索关于使用GET STACKED DIAGNOSTICS命令的当前异常信息,形成了:
GET STACKED DIAGNOSTICS variable { = | := } item [ , ... ];
每个item是识别被分配到指定变量的状态值(应该是接收它的正确数据类型)的一个关键字。 目前可用的状态显示在表 40-1中。
表 40-1. 错误诊断值
名字 | 类型 | 描述 |
---|---|---|
RETURNED_SQLSTATE | text | 异常的SQLSTATE错误代码 |
COLUMN_NAME | text | 与异常相关的列名 |
CONSTRAINT_NAME | text | 与异常相关的约束名 |
PG_DATATYPE_NAME | text | 与异常相关的数据类型名 |
MESSAGE_TEXT | text | 异常的主要消息文本 |
TABLE_NAME | text | 与异常相关的表名 |
SCHEMA_NAME | 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;
GET [ CURRENT ] DIAGNOSTICS 命令检索关于当前执行状态的信息(GET STACKED DIAGNOSTICS 命令讨论关于自前一个错误起的执行状态的报告信息)。 这个命令有以下形式:
GET [ CURRENT ] DIAGNOSTICS variable { = | := } item [ , ... ];
目前只支持一个信息项。状态项PG_CONTEXT将返回一个文本字符串, 文本行描述调用栈。第一行指的是当前的函数和正在执行的 GET DIAGNOSTICS命令。 第二行和以后的行比调用函数更深一步的调用堆栈。例如:
CREATE OR REPLACE FUNCTION outer_func() RETURNS integer AS $$ BEGIN RETURN inner_func(); END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION inner_func() RETURNS integer AS $$ DECLARE stack text; BEGIN GET DIAGNOSTICS stack = PG_CONTEXT; RAISE NOTICE E'--- Call Stack ---\n%', stack; RETURN 1; END; $$ LANGUAGE plpgsql; SELECT outer_func(); NOTICE: --- Call Stack --- PL/pgSQL function inner_func() line 5 at GET DIAGNOSTICS PL/pgSQL function outer_func() line 3 at RETURN CONTEXT: PL/pgSQL function outer_func() line 3 at RETURN outer_func ------------ 1 (1 row)