Oracle PL/SQL Programming 第5章:Iterative Processing with Loops 读书笔记

总的目录和进度,请参见开始读 Oracle PL/SQL Programming 第6版

本章探讨 PL/SQL 的迭代控制结构(也称为循环),它允许您重复执行相同的代码。

PL/SQL 提供了三种不同类型的循环结构:

  • 简单或无限循环
  • FOR 循环(数字和游标)
  • WHILE 循环

每种类型的循环都是针对特定目的而设计:

属性 描述
循环如何终止 循环重复执行代码。 如何使循环停止执行其主体?
何时进行终止测试 终止测试是在循环的开始还是结束时进行? 后果是什么?
使用此循环的原因 您应该考虑哪些特殊因素来确定此循环是否适合您的情况?

Loop Basics

为什么存在三种不同类型的循环? 为了给您提供灵活性,为了代码简单,更容易理解和维护。

Examples of Different Loops

简单或无限循环:

LOOP
    EXIT WHEN i > 100;
    ...;
    i:=  i + 1;
END LOOP;

FOR循环(数字):

FOR i IN 1 .. 100
LOOP
   ...;
END LOOP;

FOR循环(游标):

FOR i IN (SELECT ...)
LOOP
   ...;
END LOOP;

WHILE循环:

WHILE (i <= 100)
LOOP
   ...;
END LOOP;

Structure of PL/SQL Loops

虽然这三个循环结构之间存在差异,但每个循环都有两个部分:

  • 循环边界
    它由启动循环的保留字、导致循环终止的条件以及结束循环的 END LOOP 语句组成。

  • 循环体
    这是循环边界内的可执行语句序列,在循环的每次迭代中执行。

The Simple Loop

形式为:

LOOP
  执行语句...
END LOOP;

仅当执行语句包含EXIT或EXIT WHEN时,才会退出循环;否则为无限循环。

EXIT;
EXIT WHEN 判断条件;

EXIT WHEN等同于IF-THEN加EXIT。

使用简单循环的场景:

  • 希望循环至少执行1次
  • 无法确定循环需执行多少次

当存在单个条件表达式来确定循环是否应终止时,最好使用 EXIT WHEN。 在有多个退出条件的情况下,或者当您需要根据不同的条件设置从循环中退出时,最好使用 IF 或 CASE 语句,然后加上EXIT 语句 。

Emulating a REPEAT UNTIL Loop

PL/SQL中没有WHILE…UNTIL语句,但有类似的实现:

LOOP
   ...
   EXIT WHEN ...;
END LOOP;

The Intentionally Infinite Loop

无限循环可能存在,但通常都会加sleep语句:

LOOP 
   执行操作;
   DBMS_LOCK.sleep(10);
END LOOP;

如何终止无限循环?在交互式PL/SQL中可以用“Ctrl+C”,在后台运行的程序可以用操作系统的kill命令,注意数据库中的ALTER SYSTEM KILL SESSION命令不一定可以终止无限循环。但是kill命令有可能误杀,例如在共享服务器模式下。

作者推荐的方式为利用PL/SQL中的管道,这类似于Shell编程中的信号:

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;

上面是接收信号的程序,下面是发送信号的程序:

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 循环:

WHILE 布尔变量|表达式
LOOP
   执行语句
END LOOP;

The Numeric FOR Loop

PL/SQL FOR 循环有两种:数字FOR 循环和游标FOR 循环。 数字 FOR 循环是传统且熟悉的“计数”循环。 FOR 循环的迭代次数在循环开始时就已知。

范围方案隐式声明循环索引(如果尚未声明),指定范围的起点和终点,并可选择指定循环索引进行的顺序(从最低到最高或从最高到最低)。

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

一个倒计时程序:

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 数据库语言的强大功能的紧密且有效的集成。 它减少了从游标获取数据所需编写的代码量。 它大大减少了在编程中引入循环错误的机会,而循环是程序中最容易出错的部分之一。

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循环之前:

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 属性。:

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

您可以使用标签为循环命名。 格式为:

<<label_name>>

<> 必须出现在循环体第一个语句之前,而在END LOOP也可以加label_name,但不是必须。

循环标签作用:

  • 代码更容易维护和调试。就好像括号必须成对出现。
  • 可使用标签来限定循环索引变量的名称,这有助于提高可读性。
  • 当您有嵌套循环时,您可以使用标签来提高可读性并增强对循环执行的控制。如EXIT loop_label [WHEN condition];不过这种用户和GOTO一样不建议。

The CONTINUE Statement

CONTINUE 语句退出循环的当前迭代,并立即继续该循环的下一次迭代。 该语句有两种形式,就像 EXIT 一样:无条件 CONTINUE 和条件 CONTINUE WHEN。

下例演示了利用CONTINUE实现步进为3:

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的例子:

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:

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 多奇怪!多怪异!多愚蠢!

你可能感兴趣的:(Oracle,PL/SQL,Oracle数据库开发,oracle,sql,database,plsql,programming)