总的目录和进度,请参见开始读 Oracle PL/SQL Programming 第6版
本章探讨 PL/SQL 的迭代控制结构(也称为循环),它允许您重复执行相同的代码。
PL/SQL 提供了三种不同类型的循环结构:
- 简单或无限循环
- FOR 循环(数字和游标)
- WHILE 循环
每种类型的循环都是针对特定目的而设计:
属性 | 描述 |
---|---|
循环如何终止 | 循环重复执行代码。 如何使循环停止执行其主体? |
何时进行终止测试 | 终止测试是在循环的开始还是结束时进行? 后果是什么? |
使用此循环的原因 | 您应该考虑哪些特殊因素来确定此循环是否适合您的情况? |
Loop Basics
为什么存在三种不同类型的循环? 为了给您提供灵活性,为了代码简单,更容易理解和维护。
Examples of Different Loops
简单或无限循环:
sql
LOOP
EXIT WHEN i > 100;
...;
i:= i + 1;
END LOOP;
FOR循环(数字):
sql
FOR i IN 1 .. 100
LOOP
...;
END LOOP;
FOR循环(游标):
sql
FOR i IN (SELECT ...)
LOOP
...;
END LOOP;
WHILE循环:
sql
WHILE (i <= 100)
LOOP
...;
END LOOP;
Structure of PL/SQL Loops
虽然这三个循环结构之间存在差异,但每个循环都有两个部分:
-
循环边界
它由启动循环的保留字、导致循环终止的条件以及结束循环的 END LOOP 语句组成。
-
循环体
这是循环边界内的可执行语句序列,在循环的每次迭代中执行。
The Simple Loop
形式为:
sql
LOOP
执行语句...
END LOOP;
仅当执行语句包含EXIT或EXIT WHEN时,才会退出循环;否则为无限循环。
sql
EXIT;
EXIT WHEN 判断条件;
EXIT WHEN等同于IF-THEN加EXIT。
使用简单循环的场景:
- 希望循环至少执行1次
- 无法确定循环需执行多少次
当存在单个条件表达式来确定循环是否应终止时,最好使用 EXIT WHEN。 在有多个退出条件的情况下,或者当您需要根据不同的条件设置从循环中退出时,最好使用 IF 或 CASE 语句,然后加上EXIT 语句 。
Emulating a REPEAT UNTIL Loop
PL/SQL中没有WHILE...UNTIL语句,但有类似的实现:
sql
LOOP
...
EXIT WHEN ...;
END LOOP;
The Intentionally Infinite Loop
无限循环可能存在,但通常都会加sleep语句:
sql
LOOP
执行操作;
DBMS_LOCK.sleep(10);
END LOOP;
如何终止无限循环?在交互式PL/SQL中可以用"Ctrl+C",在后台运行的程序可以用操作系统的kill命令,注意数据库中的ALTER SYSTEM KILL SESSION
命令不一定可以终止无限循环。但是kill命令有可能误杀,例如在共享服务器模式下。
作者推荐的方式为利用PL/SQL中的管道,这类似于Shell编程中的信号:
sql
DECLARE
pipename CONSTANT VARCHAR2(12) := 'signaler';
result INTEGER;
pipebuf VARCHAR2(64);
BEGIN
/* create private pipe with a known name */
result := DBMS_PIPE.create_pipe(pipename);
LOOP
DBMS_OUTPUT.PUT_LINE('I''m doing works ...'); -- 请替换为实际执行的操作
DBMS_LOCK.sleep(5);
/* see if there is a message on the pipe */
IF DBMS_PIPE.receive_message(pipename, 0) = 0
THEN
/* interpret the message and act accordingly */
DBMS_PIPE.unpack_message(pipebuf);
IF pipebuf = 'stop'
THEN
DBMS_OUTPUT.PUT_LINE('Exiting ...');
EXIT;
END IF;
END IF;
END LOOP;
END;
上面是接收信号的程序,下面是发送信号的程序:
sql
DECLARE
pipename VARCHAR2 (12) := 'signaler';
result INTEGER := DBMS_PIPE.create_pipe (pipename);
BEGIN
DBMS_PIPE.pack_message ('stop');
result := DBMS_PIPE.send_message (pipename);
END;
测试在SQL Plus中通过,但SQL Developer没有通过,不知为何。
以下为成功时的输出:
I'm doing works ...
I'm doing works ...
I'm doing works ...
I'm doing works ...
I'm doing works ...
Exiting ...
PL/SQL procedure successfully completed.
此示例使用私有管道,因此发送和接收程序需为同一用户。 另请注意,私有管道的数据库命名空间在当前用户运行的所有会话中是全局的。
The WHILE Loop
如事先不知道循环执行的次数(可以一次都不执行),则可使用 WHILE 循环:
sql
WHILE 布尔变量|表达式
LOOP
执行语句
END LOOP;
The Numeric FOR Loop
PL/SQL FOR 循环有两种:数字FOR 循环和游标FOR 循环。 数字 FOR 循环是传统且熟悉的"计数"循环。 FOR 循环的迭代次数在循环开始时就已知。
范围方案隐式声明循环索引(如果尚未声明),指定范围的起点和终点,并可选择指定循环索引进行的顺序(从最低到最高或从最高到最低)。
sql
FOR loop index IN [REVERSE] lowest number .. highest number
LOOP
executable statement(s)
END LOOP;
Rules for Numeric FOR Loops
- 不要声明循环索引。 PL/SQL 自动且隐式地将其声明为数据类型为 INTEGER 的局部变量。 该索引的范围是循环本身; 您不能在循环外引用循环索引。
- 范围方案中使用的表达式(最低和最高边界)在循环开始时评估一次。 在循环执行期间不会重新评估范围。所以,后面改了也不会生效。
- 切勿在循环内更改循环索引或范围边界的值。
Examples of Numeric FOR Loops
一个倒计时程序:
sql
BEGIN
FOR i IN REVERSE 1 .. 10
LOOP
DBMS_OUTPUT.PUT_LINE(i);
END LOOP;
END;
还有范围可以由常数定义,也可以由变量或表达式定义。
Handling Nontrivial Increments
和C语言不一样,PL/SQL中的循环索引的步增/步减永远是1。所以,如果增减量为非1,则需要另加IF判断,如MOD函数。
The Cursor FOR Loop
游标 FOR 循环是与直接合并在循环边界内的显式游标或 SELECT 语句关联(并实际由其定义)的循环。 仅当需要从游标中获取并处理每条记录时(游标经常出现这种情况),才使用游标 FOR 循环。
游标 FOR 循环充分利用了过程结构与 SQL 数据库语言的强大功能的紧密且有效的集成。 它减少了从游标获取数据所需编写的代码量。 它大大减少了在编程中引入循环错误的机会,而循环是程序中最容易出错的部分之一。
sql
FOR record IN { cursor_name | (explicit SELECT statement) }
LOOP
executable statement(s)
END LOOP;
其中 record 是由 PL/SQL 使用 %ROWTYPE 属性针对cursor_name 指定的游标隐式声明的记录。
不要显式声明与循环索引记录同名的记录。 它不是必需的(PL/SQL 隐式声明它在循环中使用)并且可能导致逻辑错误。 有关在循环执行之外或之后访问有关游标 FOR 循环记录的信息的提示,请看本文后面部分:Obtaining Information About FOR Loop Execution。
Example of Cursor FOR Loops
没有用游标FOR循环之前:
sql
SET SERVEROUTPUT ON
DECLARE
CURSOR hr_cur IS
SELECT first_name, last_name
FROM employees WHERE department_id = 100;
hr_rec hr_cur%ROWTYPE;
BEGIN
OPEN hr_cur;
LOOP
FETCH hr_cur INTO hr_rec;
EXIT WHEN hr_cur%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(hr_rec.first_name || ' ' || hr_rec.last_name);
END LOOP;
CLOSE hr_cur;
END;
用游标FOR循环后,简洁很多! 无需记录的声明。 OPEN、FETCH 和 CLOSE 语句已消失。 不再需要检查 %NOTFOUND 属性。:
sql
SET SERVEROUTPUT ON
DECLARE
CURSOR hr_cur IS
SELECT first_name, last_name
FROM employees WHERE department_id = 100;
BEGIN
FOR hr_rec IN hr_cur
LOOP
DBMS_OUTPUT.PUT_LINE(hr_rec.first_name || ' ' || hr_rec.last_name);
END LOOP;
END;
与所有其他游标一样,您可以在游标 FOR 循环中将参数传递给游标?。 如果光标选择列表中的任何列是表达式,请记住您必须在选择列表中为该表达式指定别名。 在循环内,访问游标记录中特定值的唯一方法是使用点符号(record_name.column_name,如 ocupancy_rec.room_number),因此您需要一个与表达式关联的列名。
Loop Labels
您可以使用标签为循环命名。 格式为:
sql
<<label_name>>
<<label_name>> 必须出现在循环体第一个语句之前,而在END LOOP也可以加label_name,但不是必须。
循环标签作用:
- 代码更容易维护和调试。就好像括号必须成对出现。
- 可使用标签来限定循环索引变量的名称,这有助于提高可读性。
- 当您有嵌套循环时,您可以使用标签来提高可读性并增强对循环执行的控制。如
EXIT loop_label [WHEN condition];
不过这种用户和GOTO一样不建议。
The CONTINUE Statement
CONTINUE 语句退出循环的当前迭代,并立即继续该循环的下一次迭代。 该语句有两种形式,就像 EXIT 一样:无条件 CONTINUE 和条件 CONTINUE WHEN。
下例演示了利用CONTINUE实现步进为3:
sql
BEGIN
FOR l_index IN 1 .. 10
LOOP
CONTINUE WHEN MOD (l_index - 1, 3) != 0;
DBMS_OUTPUT.PUT_LINE ('Loop index = ' || TO_CHAR (l_index));
END LOOP;
END;
/
输出为:
Loop index = 1
Loop index = 4
Loop index = 7
Loop index = 10
您还可以使用 CONTINUE 终止内部循环并立即继续进行外部循环体的下一次迭代。 为此,您需要使用标签为外部循环命名。
IS CONTINUE AS BAD AS GOTO?
continue 语句不能滥用,但用对地方则很有价值,因为它使代码更短,使代码更易于阅读,并减少了对布尔变量的需求。
作者举了使用和不使用continue的2个例子作为对比。我看懂了,并认可。
使用continue的例子:
sql
LOOP
EXIT WHEN exit_condition_met;
CONTINUE WHEN condition1;
CONTINUE WHEN condition2;
setup_steps_here;
IF condition4 THEN
action4_executed;
CONTINUE;
END IF;
IF condition5 THEN
action5_executed;
CONTINUE; -- Not strictly required.
END IF;
END LOOP;
如果不使用continue:
sql
LOOP
EXIT WHEN exit_condition_met;
IF condition1
THEN
NULL;
ELSIF condition2
THEN
NULL;
ELSE
setup_steps_here;
IF condition4 THEN
action4_executed;
ELSIF condition5 THEN
action5_executed;
END IF;
END IF;
END LOOP;
Tips for Iterative Processing
循环是非常强大且有用的结构,但您应该谨慎使用它们。 程序中的性能问题通常可以追溯到循环,并且循环中的任何问题都会因其重复执行而被放大。 确定何时停止循环的逻辑可能非常复杂。 本节提供了一些关于如何编写干净、易于理解且易于维护的循环的技巧。
Use Understandable Names for Loop Indexes
使用有意义的循环索引变量名称,而非简单的i,j,k。
The Proper Way to Say Goodbye
结构化编程的一个重要且基本的原则是"一进一出"; 也就是说,程序应该有一个入口点和一个出口点。一个入口是必然的,这里讲的是如何避免多个出口。
您应该遵循以下循环终止准则:
- 不要在 FOR 和 WHILE 循环中使用 EXIT 或 EXIT WHEN 语句。
- 不要在循环中使用 RETURN 或 GOTO 语句,这同样会导致循环过早、非结构化终止。
如果需要根据游标 FOR 循环获取的信息终止循环(例如当取得值的合计大于某值时退出),则应使用 WHILE 循环或简单循环代替。 那么代码的结构就会更清楚地表达你的意图。
Obtaining Information About FOR Loop Execution
FOR 循环是方便且简洁的结构,对于游标 FOR 循环尤其如此。 然而,有一个权衡:数据库自动为您完成大量工作,但您在循环终止后对有关循环最终结果的信息的访问受到限制。
简单来说,游标 FOR 循环的END LOOP语句后,游标就被关闭了,也就是说,此时无法获取游标的信息。因此,你需要再循环内部(游标关闭前)暂存游标的信息,如行数(cursor%ROWCOUNT),后续关闭后就可以继续访问。
SQL Statement as Loop
实际上,您可以将像 SELECT 这样的 SQL 语句视为循环。 毕竟,这样的语句指定了对一组数据采取的操作; 然后,SQL 引擎"循环"数据集并应用操作。
例如一个数据归档的例子,从源表中逐行读取,然后插入归档表后删除。这既可以用PL/SQL实现,也可以用2条SQL实现(INSERT INTO ... DELETE,DELETE )
SQL实现编写的代码更少,而且运行效率更高,因为减少了上下文切换的次数(在 PL/SQL 和 SQL 执行引擎之间来回移动)。 只执行一次插入和一次删除。
但SQL的灵活性差一点,因为SQL是事务型的,要么全成功,要么全失败;SQL也不能做特殊处理,如记录归档失败的记录。因此,PL/SQL 提供更大的灵活性。
总之,PL/SQL 提供一次访问和处理单行并采取操作(或许还有基于该特定记录内容的复杂过程逻辑)的能力。 另一方面,使用原生SQL代码更少,运行效率更高。必要时,可以混合使用 PL/SQL 和 SQL。
单词
- go figure 多奇怪!多怪异!多愚蠢!