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

简单或无限循环:

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 多奇怪!多怪异!多愚蠢!
相关推荐
贺今宵13 分钟前
安装better-sqlite3报错electron-vite
javascript·sql·sqlite·sqlite3
山峰哥3 小时前
SQL调优核心战法——索引失效场景与Explain深度解析
大数据·汇编·数据库·sql·编辑器·深度优先
程序 代码狂人6 小时前
开窗函数 集合运算 行列转换
sql
AlfredZhao7 小时前
新版MOS(My Oracle Support)主要变化
oracle·mos
lifewange7 小时前
数据库索引里面的游标是什么?
数据库·oracle
老年DBA8 小时前
Ora2Pg 迁移Oracle至 PostgreSQL 之实战指南
数据库·postgresql·oracle
l1t8 小时前
达梦数据库和Oracle兼容性和性能比较
数据库·sql·oracle·达梦
weixin_436525079 小时前
NestJS-TypeORM QueryBuilder 常用 SQL 写法
java·数据库·sql
两拆9 小时前
Redhat7.9安装部署Oracle 19C
数据库·oracle
白露与泡影10 小时前
详细描述一条 SQL 语句在 MySQL 中的执行过程。
数据库·sql·mysql