第四章 PL/SQL的控制结构

PL/SQL的控制结构

一、PL/SQL控制结构一览

根据“计算机程序基本控制结构” 及结构定理(通常指结构化程序设计理论),这三种结构是结构化程序设计的基石,包括序列结构、选择结构、循环结构,这正是结构化程序设计理论(由 Böhm 和 Jacopini 于 1966 年证明)的核心 。任何计算机程序都可以用下图中的基本控制结构来表示。这三种结构是结构化程序设计的基石,所有程序逻辑都可通过它们的组合实现。

序列结构(Sequence Structure)

  • 描述:“简单的按照顺序执行语句”。

  • 流程图示意
    用“矩形”表示“执行语句”,用“箭头”表示执行顺序,语句按自上而下的线性顺序依次执行,无分支、无重复。
    例:

    1
    
    [语句1] → [语句2] → [语句3] → (结束)
    
  • 逻辑特点:最基础的结构,任何程序的“骨架”都是序列——先做A,再做B,最后做C,顺序不可乱(除非被选择/循环结构改变)。

选择结构(Selection Structure,又称“分支结构”)

  • 描述:“测试条件,根据条件的真假,执行一系列语句”“条件返回布尔值(TRUE/FALSE)”。

  • 流程图示意
    用“菱形”表示“条件判断”,从菱形延伸出两条分支:

    • 若条件为 TRUE(真),执行“分支1”的语句;
    • 若条件为 FALSE(假),执行“分支2”的语句(或直接跳过,即“空分支”);
      两条分支最终会“汇合”,继续执行后续序列。
      例(双分支,如 if-else):
    1
    2
    3
    4
    5
    
            → (TRUE) → [语句A] → 
    (开始) → [条件?] 
            → (FALSE) → [语句B] → 
                        (汇合后继续)
    
  • 逻辑特点:实现“二选一”或“多选一”(如 if-else if-else),是程序“判断逻辑”的核心(比如“如果用户输入密码正确,就登录;否则提示错误”)。

循环结构(Loop Structure,又称“重复结构”)

  • 描述:“在条件满足的情况下反复执行”。

  • 流程图示意
    核心是“判断条件→执行循环体→回到判断条件”的闭环,分两种常见形式(均符合“条件满足则反复执行”的定义):
    当型循环(While 型):先判断条件,若 TRUE,则执行循环体;执行后再次判断,直到条件为 FALSE 时退出(“先判断,后执行”,可能一次都不执行)。
    例:

    1
    2
    3
    
    (开始) → [条件?] → (TRUE) → [循环体语句] → 回到[条件?]
                     (FALSE) → (退出循环)
    

    直到型循环(Do-While 型):先执行一次循环体,再判断条件;若 TRUE,则回到循环体继续执行,直到条件为 FALSE 时退出(“先执行,后判断”,至少执行一次)。
    例:

    1
    2
    3
    
    (开始) → [循环体语句] → [条件?] → (TRUE) → 回到[循环体语句]
                                     (FALSE) → (退出循环)
    
  • 逻辑特点:实现“重复执行某段逻辑”(比如“反复读取用户输入,直到输入符合要求为止”“计算1到100的和,重复加100次”)。

选择结构是用于测试条件的,根据条件的真假,执行一系列语句。一个条件语句可以是任何能够返回布尔值(TRUE或FALSE)的变量或表达式。循环结构能在条件满足的情况下反复执行。序列结构只是简单的按照顺序执行语句。

二、条件控制:IF和CASE语句

有时候,我们需要根据具体的条件来采取不同的对策。IF语句就能让我们按条件来执行语句序列。也就是说,语句序列的执行与否取决于某个给定的条件。有三种IF语句:IF-THEN、IF-THEN-ELSE和IF-THEN-ELSIF。CASE语句是条件判断的精简形式,它能计算条件表达式的值并在多个对应动作中做出选择。

1. IF-THEN语句

IF语句最简单的形式就是把一个条件和一个语句序列用关键字THEN和END IF关联起来:

1
2
3
IF condition THEN
   sequence_of_statements
END IF;

只有在条件值为真的时候语句序列才能被执行。如果条件值为假或是空,IF语句就什么都不做。无论哪种情况,控制权最后还是会被传递到下一个语句,如下例:

1
2
3
4
5
6
IF sales > QUOTA THEN
    compute_bonus(empid);
    UPDATE payroll
        SET pay = pay + bonus
        WHERE empno = emp_id;
END IF;

如果我们把IF语句放到一行,就可以像下面这样编写:

1
2
3
IF x > y THEN
    high := x;
END IF;

2. IF-THEN-ELSE语句

第二种形式的IF语句使用关键字ELSE添加了一个额外的处理选项,如下:

1
2
3
4
5
IF condition THEN
  sequence_of_statements1
ELSE
  sequence_of_statements2
END IF;

当条件为假或空时,ELSE子句中的语句序列就会被执行。下例中,第一个UPDATE语句在条件为真的情况下执行,而第二个UPDATE语句在条件为假或为空的情况下才会被执行:

1
2
3
4
5
6
7
8
9
IF trans_type = 'CR' THEN
  UPDATE accounts
     SET balance = balance + credit
   WHERE ...
ELSE
  UPDATE accounts
     SET balance = balance - debit
   WHERE ...
END IF;

THEN和ELSE子句中也可以包含IF语句。就是说IF语句能够被嵌套使用,如下例所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
IF trans_type = 'CR' THEN
  UPDATE accounts
     SET balance = balance + credit
   WHERE ...
ELSE
  IF new_balance >= minimum_balance THEN
    UPDATE accounts
       SET balance = balance - debit
     WHERE ...
  ELSE
    RAISE insufficient_funds;
  END IF;
END IF;

3. IF-THEN-ELSIF语句

有时我们可能需要从几个选项中选择一个,这时我们就需要使用第三种IF语句,添加一个ELSIF关键字提供额外的条件选项,使用方法如下:

1
2
3
4
5
6
7
IF condition1 THEN
  sequence_of_statements1
ELSIF condition2 THEN
  sequence_of_statements2
ELSE
  sequence_of_statements3
END IF;

如果第一个条件为假或空,ELSIF子句就会检测另外一个条件。一个IF语句可以有多个ELSIF子句;最后一个ELSE子句是可选的。条件表达式从上而下的计算。只要有满足的条件,与它关联的语句就会执行,然后控制权转到下一个语句。如果所有的条件都为假或是空,ELSE部分的语句就会执行。看一下下面的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
BEGIN
  ...
  IF sales > 50000 THEN
    bonus := 1500;
  ELSIF sales > 35000 THEN
    bonus := 500;
  ELSE
    bonus := 100;
  END IF;
  
  INSERT INTO payroll
       VALUES (emp_id, bonus, ...);
END;

如果sales的值大于50000的话,第一个和第二个条件就为真。然而,bonus只会被赋予1500的值,因为第二个条件并没有执行到。当第一个条件为真的话,它关联的语句就会执行,然后控制权转到INSERT语句。

4. CASE语句

同IF语句一样,CASE语句也是选出一个语句序列来执行。但是,为了选择出合适的语句序列,CASE会使用一个选择器,而不是多个布尔表达式。想要比较IF和CASE语句的话,请看下面对学校成绩的描述信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
IF grade = 'A' THEN
  DBMS_OUTPUT.put_line('Excellent');
ELSIF grade = 'B' THEN
  DBMS_OUTPUT.put_line('Very Good');
ELSIF grade = 'C' THEN
  DBMS_OUTPUT.put_line('Good');
ELSIF grade = 'D' THEN
  DBMS_OUTPUT.put_line('Fair');
ELSIF grade = 'F' THEN
  DBMS_OUTPUT.put_line('Poor');
ELSE
  DBMS_OUTPUT.put_line('No such grade');
END IF;

请注意这五个布尔表达式,在每一个实例中,我们只对同一变量的值进行检测,看它的分数值是否等于"A"、“B”、“C”、“D”、“E"或"F”。下面我们用CASE语句重新编写上面的程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
CASE grade
  WHEN 'A' THEN
    DBMS_OUTPUT.put_line('Excellent');
  WHEN 'B' THEN
    DBMS_OUTPUT.put_line('Very Good');
  WHEN 'C' THEN
    DBMS_OUTPUT.put_line('Good');
  WHEN 'D' THEN
    DBMS_OUTPUT.put_line('Fair');
  WHEN 'F' THEN
    DBMS_OUTPUT.put_line('Poor');
  ELSE
    DBMS_OUTPUT.put_line('No such grade');
END CASE;

CASE语句的可读性高而且高效,所以,如果可能的话,尽量把IF-THEN-ELEIF都改写成CASE语句。

CASE语句以关键字CASE开头,然后跟上一个选择器,也就是上例中的变量grade。选择器表达式可能是很复杂的。例如,它有可能是一个函数调用。但在通常情况下,它只是一个独立的变量。选择器表达式只被计算一次。它的值可以是除BLOB、BFILE、对象类型、PL/SQL记录、索引表、变长数组或嵌套表之外的任何有效的PL/SQL数据类型。选择器后面跟着一个或多个WHEN子句,它们是按顺序检测的。选择器的值决定了哪个子句被执行。如果选择器的值等于WHEN子句的表达式值, WHEN子句中的语句序列就会被执行。例如在上面例子中,如果grade等于"C",程序就会输出"Good"。当WHEN子句中的语句序列被执行完毕,控制权会转到下一个语句,而不会再执行后续的WHEN子句。

ELSE子句的工作原理与IF中的类似。上例子中,如果grade的值不与任何一个WHEN子句匹配,ELSE部分就会被执行,“No such grade"就会被输出。ELSE子语是可选的。但是,如果我们省略了ELSE子句,PL/SQL就会为我们添加隐式的ELSE子句:

1
ELSE RAISE CASE_NOT_FOUND;

如果CASE语句选择了隐式的ELSE子句,PL/SQL就会抛出预定义异常CASE_NOT_FOUND。所以,即使我们省略了ELSE子句,ELSE也会有一个默认的动作。

关键字END CASE是CASE语句结束的标志。这两个关键字必须用空格分开。形式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[<<label_name>>]
CASE selector
  WHEN expression1 THEN
    sequence_of_statements1;
  WHEN expression2 THEN
    sequence_of_statements2;
  ...
  WHEN expressionn THEN
    sequence_of_statementsn;
  [ELSE sequence_of_statementsN+1;]
END CASE [label_name];

同 PL/SQL块一样,CASE语句也是可以加标签的。标签是一个未声明的标识符,必须出现在CASE语句的开头,用双尖括号夹起来。标签的名称也可以出现在CASE语句的结尾处,但不是必须的。

CASE语句中的异常会按正常的方法处理,就是说正常的执行语句停止,控制权转到PL/SQL块或子程序的异常控制部分。

搜寻式CASE语句

PL/SQL还提供下面搜寻形式的CASE语句:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[<<label_name>>]
CASE
  WHEN search_condition1 THEN
    sequence_of_statements1;
  WHEN search_condition2 THEN
    sequence_of_statements2;
  ...
  WHEN search_conditionn THEN
    sequence_of_statementsn;
  [ELSE sequence_of_statementsN+1;]
END CASE [label_name];

搜寻式CASE语句没有选择器。并且,它的WHEN子句只能包含结果为布尔类型的表达式,产生其它类型结果的表达式是不允许的。示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
CASE
  WHEN grade = 'A' THEN
    DBMS_OUTPUT.put_line('Excellent');
  WHEN grade = 'B' THEN
    DBMS_OUTPUT.put_line('Very Good');
  WHEN grade = 'C' THEN
    DBMS_OUTPUT.put_line('Good');
  WHEN grade = 'D' THEN
    DBMS_OUTPUT.put_line('Fair');
  WHEN grade = 'F' THEN
    DBMS_OUTPUT.put_line('Poor');
  ELSE
    DBMS_OUTPUT.put_line('No such grade');
END CASE;

搜寻条件是按顺序计算的。每个搜寻条件的布尔值决定了哪个WHEN子句被执行。一旦WHEN子句被执行,控制权就会被交给下一个语句,后续的搜寻条件就不会被考虑。如果没有找到搜寻条件为TRUE的子句,ELSE子句就会执行。ELSE虽然是可选的,但是,如果省略了ELSE,PL/SQL就会添加隐式的ELSE子句:

1
ELSE RAISE CASE_NOT_FOUND;

如果执行过程中有异常发生,我们可以在块或子程序的异常控制部分捕获到。

5. PL/SQL条件控制语句使用准则

我们不应该像下面这样使用笨拙的IF语句:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
IF new_balance < minimum_balance THEN
    overdrawn := TRUE;
ELSE
    overdrawn := FALSE;
END IF;

...

IF overdrawn = TRUE THEN
    RAISE insufficient_funds;
END IF;

上面的代码忽视了两个地方。首先,布尔表达式的值可以直接赋给布尔变量。所以我们可以把第一个IF语句简化成下面的语句形式:

1
overdrawn := new_balance < minimum_balance;

第二,布尔变量本身就是TRUE或FALSE。所以,在IF的条件表达式中直接使用变量本身即可:

1
IF overdrawn THEN ...

尽可能地使用ELSIF子句代替嵌套IF语句。这样我们的代码就更易读易理解。比较下面两个IF语句:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
IF condition1 THEN
    statement1;
ELSE
    IF condition2 THEN
        statement2;
    ELSE
        IF condition3 THEN
            statement3;
        END IF;
    END IF;
END IF;
1
2
3
4
5
6
7
IF condition1 THEN
    statement1;
ELSIF condition2 THEN
    statement2;
ELSIF condition3 THEN
    statement3;
END IF;

这两个语句在逻辑上是等价的,但第一个语句看起来有些混乱,而第二个就较为明显。如果把单独一个表达式与多个值进行比较的话,我们可以使用CASE语句来代替多个ELSIF子句。

三、循环控制:LOOP和EXIT语句

LOOP语句能让我们反复执行一个语句序列。有三种形式的LOOP语句:LOOP,WHILE-LOOP和FOR-LOOP。

1. LOOP语句

LOOP语句最简单的形式就是把语句序列放到关键字LOOP和END LOOP之间,语法如下:

1
2
3
LOOP
  sequence_of_statements;
END LOOP;

在每一个循环中,语句序列都会被顺序执行,然后再返回循环顶部从头执行。如果不想继续执行,可以使用EXIT语句退出循环。我们可以把一个或多个EXIT语句放到循环里,但不能放到循环外面。有两种形式的EXIT语句:EXIT和EXIT-WHEN。

EXIT语句的格式如下:

EXIT语句会强迫循环无条件终止。当遇到EXIT语句时,循环会立即终止,并把控制权交给下面的语句。示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-- 示例1: 在LOOP循环中使用EXIT语句(合法用法)
LOOP
    -- 循环内的业务逻辑代码
    ...
    
    -- 检查信用评级条件
    IF credit_rating < 3 THEN
        -- 当信用评级小于3时执行的逻辑
        ...
        
        EXIT;  -- 退出循环(在循环中使用是合法的)
    END IF;
END LOOP;
-- EXIT后,程序控制将在这里继续执行


-- 示例2: 在PL/SQL块中直接使用EXIT语句(非法用法)
BEGIN
    -- 主程序块内的业务逻辑
    ...
    
    -- 检查信用评级条件
    IF credit_rating < 3 THEN
        -- 当信用评级小于3时执行的逻辑
        ...
        
        EXIT;  -- 不允许使用!EXIT只能在循环中使用
    END IF;
END;
/

记住,EXIT语句必须放在循环内。如果想在PL/SQL块正常到达程序结尾之前而终止执行,可以使用RETURN语句。

EXIT-WHEN语句的格式如下:

EXIT-WHEN语句可以根据给定的条件跳出循环。当遇到EXIT语句时,WHEN子句中的表达式值就会被计算。如果条件满足,循环就会被终止,控制权转到循环语句之后的语句。示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
-- 使用EXIT WHEN在FETCH后判断游标是否还有数据
LOOP
    FETCH c1
      INTO ...;  -- 将游标数据提取到变量中
      
    EXIT WHEN c1%NOTFOUND;  -- 当游标没有更多数据时退出循环
    
    -- 循环体内的其他处理逻辑
    ...
END LOOP;

CLOSE c1;  -- 关闭游标

在条件满足之前,循环是不会结束的。所以,循环里的语句必须要改变循环条件的值。上例中,如果FETCH语句返回了一行值,WNEN子句中的条件就为假;如果不能返回结果,WNEN子句中的条件就为真,循环就会结束,控制权转入CLOSE语句。

EXIT-WHEN语句可以替代简单的IF语句,例如,比较下面两段代码:

1
2
3
4
-- 方式1:使用IF语句判断条件并退出循环
IF count > 100 THEN
    EXIT;  -- 当count大于100时退出循环
END IF;

这两个语句在逻辑上是等价的,但EXIT-WHEN语句更容易阅读和理解。

循环标签
跟PL/SQL块一样,循环也是可以添加标签。标签必须出现在LOOP语句的开端,语法如下:

1
2
3
4
<<label_name>>
LOOP
  sequence_of_statements
END LOOP;

而在LOOP语句结束部分出现的标签名称是可选的,语法如下:

1
2
3
4
<<my_loop>>
LOOP
  ...
END LOOP my_loop;

在LOOP结束部分使用标签名称能够改善可读性。无论使用哪种EXIT语句形式,都可以结束一个封闭的LOOP块,而不仅仅局限于当前的LOOP块。只要在我们想结束的封闭LOOP块上添加一个标签,然后像下面这样在EXIT语句中使用这个标签就可以了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<<outer>>  -- 外部循环标签
LOOP
    -- 外部循环的业务逻辑
    ...
    
    -- 内部循环
    LOOP
        -- 内部循环的业务逻辑
        ...
        
        -- 当满足条件时,退出外部循环(同时退出两个循环)
        EXIT outer WHEN ...;
    END LOOP;  -- 内部循环结束
    
    -- 外部循环的其他逻辑
    ...
END LOOP outer;  -- 外部循环结束

2. WHILE-LOOP语句

WHILE-LOOP语句用关键字LOOP和END LOOP把语句序列封闭起来并与一个布尔条件表达式相关联:

1
2
3
WHILE condition LOOP
  sequence_of_statements
END LOOP;

每次循环之前,程序都是计算布尔表达式的值。如果条件为真,语句序列就会被执行,然后重新返回循环顶部计算布尔表达式的值;如果布尔表达式的值为假或空,控制权就会被交给循环之后的语句。下面看一个例子:

1
2
3
4
5
6
7
WHILE total <= 25000 LOOP
  ...
  SELECT sal
    INTO salary
    FROM emp
   WHERE x = x;   ...  total    := total + salary;
END LOOP;

循环的次数是与条件相关的,而且在循环结束之前是未知的。由于条件是在循环顶部测试的,所以语句序列有可能一次都没有执行。在上面的例子中,如果total的初始值比25000大,那么条件值就是假,循环就会被跳过。

有些语言有LOOP UNTIL或是REPEAT UNTIL这样的结构,在底部测试条件表达式的值。这样,语句序列就会至少执行一次。PL/SQL没有这样的结构,但我们可以变通地使用下面的方法来实现这样的功能:

1
2
3
4
5
LOOP
  sequence_of_statements;
  
  EXIT WHEN condition;  -- 在循环底部测试条件
END LOOP;

要保证WHILE循环至少执行一次,在条件表达式中使用初始化过的布尔变量,如下例所示:

1
2
3
4
5
6
done := FALSE;

WHILE NOT done LOOP
    sequence_of_statements;
    done := boolean_expression;
END LOOP;

在循环内的语句必须为布尔变量赋上一个新值。否则循环就会无限地执行下去。如下例的两个LOOP语句在逻辑上是等价的

1
2
3
4
5
6
7
WHILE TRUE LOOP
    ...
END LOOP;

LOOP
    ...
END LOOP;

3. FOR-LOOP语句

FOR语句会在指定的整数范围内进行循环操作。循环的内容被关键字FOR和LOOP封闭起来。两个"点”(..)作为范围操作符来使用。语法如下:

1
2
3
FOR counter IN [REVERSE] lower_bound .. higher_bound LOOP
    sequence_of_statements;  -- 循环体内的语句序列
END LOOP;

当首次进入FOR循环时,循环的范围就会被确定下来,并且不会重新计算。如下例所示,语句序列会执行三次,每执行一次,循环因子就会增加1。

1
2
3
4
FOR i IN 1 .. 3 LOOP
    -- 循环变量i将依次被赋值为1, 2, 3
    sequence_of_statements;  -- 这段代码将执行3次
END LOOP;

下例演示了如果下界值等于上界值,循环中的语句序列只执行一次:

1
2
3
4
FOR i IN 3 .. 3 LOOP
    -- 循环变量i被赋值为3(上下界相同)
    sequence_of_statements;  -- 这段代码将执行1次
END LOOP;

默认情况下,循环总是从下界到上界。不过也可以使用REVERSE关键字,让循环从上界往下界执行。但是要记住,范围的书写格式仍旧是递增顺序的。

1
2
3
4
FOR i IN REVERSE 1 .. 3 LOOP
    -- 循环变量i将依次被赋值为3, 2, 1(反向循环)
    sequence_of_statements;  -- 这段代码将执行3次
END LOOP;

FOR循环里,循环计数器只能当作常量来引用且不能为它赋值,如下例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
FOR ctr IN 1 .. 10 LOOP
    IF NOT finished THEN
        -- 在循环计数器合法使用范围内:
        INSERT INTO ... 
             VALUES (ctr);  -- 合法:使用计数器作为值插入
        
        factor := ctr * 2;  -- 合法:使用计数器计算其他变量
    ELSE
        ctr := 10;  -- 不允许:不能在循环中直接修改计数器变量
    END IF;
END LOOP;

3.1 迭代法

循环范围的边界可以是文字、变量或表达式,但它们都必须是数字。否则PL/SQL会抛出预定义异常VALUE_ERROR。如下例,下界不一定非得是1。但循环计数器只能是每次增加1。

1
2
3
j IN -5 .. 5                     -- 范围从-5到5的循环计数器
k IN REVERSE first .. last       -- 从first到last的反向循环计数器
step IN 0 .. TRUNC(high/low) * 2 -- 范围由表达式计算的循环计数器

在PL/SQL内部,它会把边界值赋给一个临时的PLS_INTEGER变量,并在需要的时候把值转换成最接近的整数。PLS_INTEGER的范围是 -231到231 之间。所以,如果边界值超过这个范围,我们就会得到一个数字溢出错误:

1
2
3
4
5
6
7
8
DECLARE
  hi   NUMBER := 2** 32;  -- 2的32次方数值过大
BEGIN
  -- 循环范围(1 .. hi)会导致"numeric overflow"(数值溢出)错误
  FOR j IN 1 .. hi LOOP
    ...
  END LOOP;
END;

有些语言提供了STEP子句,它能让我们指定循环增量。PL/SQL没有这样的结构,但我们可以在FOR循环内扩大循环计数器的倍数来实现这样的功能。在下面的例子中,我们今天的日期赋给索引表的第一个、第五个和第十五个元素。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
DECLARE
  -- 定义一个索引表类型,元素类型为DATE,索引类型为BINARY_INTEGER
  TYPE datelist IS TABLE OF DATE
    INDEX BY BINARY_INTEGER;
  
  dates  datelist;  -- 声明一个datelist类型的变量dates
  k      CONSTANT INTEGER := 5;  -- 定义常量k,作为递增步长
BEGIN
  -- 循环3次,为dates表的指定索引位置赋值
  FOR j IN 1 .. 3 LOOP
    dates(j * k) := SYSDATE;  -- 索引为循环变量j与步长k的乘积
  END LOOP;
  
  ...  -- 其他处理逻辑
END;

3.2 动态范围

PL/SQL允许我们在运行时决定循环的范围,如下例所示:

1
2
3
4
5
6
7
SELECT COUNT(empno)
  INTO emp_count
  FROM emp;

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

emp_count在运行时是未知的;SELECT语句在运行时才返回结果值。如果循环范围中的下界值超过上界值会怎样呢?如下例所示,循环内的语句序列就不会被执行,控制权直接交给下一个语句:

1
2
3
4
5
6
-- limit becomes 1 明确循环变量范围的背景(LIMIT 值为 1)
FOR i IN 2 .. LIMIT LOOP
    sequence_of_statements;  -- executes zero times (循环不执行)
END LOOP;

-- control passes here (程序流程执行到此处)

3.3 作用域规则

循环计数器只在循环内部定义,我们不能在循环外引用它。循环退出后,循环计数器就会失效,如下例:

1
2
3
4
FOR ctr IN 1 .. 10 LOOP
  ...
END LOOP;
sum := ctr - 1;   -- not allowed

我们不需要显式声明循环计数器,因为它会被隐式地声明为INTEGER类型本地变量。下面的例子中本地声明覆盖了全局声明:

1
2
3
4
5
6
7
8
9
DECLARE
  ctr INTEGER;
BEGIN
  ...
  FOR ctr IN 1 .. 25 LOOP
    ...
    IF ctr > 10 THEN ...   -- refers to loop counter,作用域仅限循环体内,不会使用外部
  END LOOP;
END;

如果想在上例中引用全局变量,我们就得借助标签和点标志,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<<main>>
DECLARE
  ctr   INTEGER;
  ...
BEGIN
  ...
  FOR ctr IN 1 .. 25 LOOP
    ...
    IF main.ctr > 10 THEN   -- refers to global variable
      ...
    END IF;
  END LOOP;
END main;

同样的作用域规则也适用于嵌套FOR循环。如下面的例子,两个循环计数器的名字相同,所以,用从内层循环引用外层循环的循环计数器,就必须使用标签和点标志:

1
2
3
4
5
6
7
<<outer>>
FOR step IN 1 .. 25 LOOP
  FOR step IN 1 .. 10 LOOP
    ...
    IF outer.step > 15 THEN ...
  END LOOP;
END LOOP outer;

3.4 使用EXIT语句

EXIT语句可以立即结束一个FOR循环。例如,下面的循环语句正常情况应该执行十次,但是,如果FETCH语句取得数据失败,循环就会立即终止,无论它执行过多少次:

1
2
3
4
5
FOR j IN 1 .. 10 LOOP
  FETCH c1 INTO emp_rec;
    EXIT WHEN c1%NOTFOUND;
  ...
END LOOP;

在使用EXIT时,我们可以结束任何封闭循环,而不仅仅是当前循环。只要在我们想结束的封闭循环上加上标签,然后在EXIT语句中引用它,就能结束做过标记的FOR循环了,如下例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<<outer>>
FOR i IN 1 .. 5 LOOP
  ...
  FOR j IN 1 .. 10 LOOP
    FETCH c1 INTO emp_rec;
      EXIT outer WHEN c1%NOTFOUND;   -- exit both FOR loops
    ...
  END LOOP;
END LOOP outer;
-- control passes here

四、顺序控制:GOTO和NULL语句

与IF和LOOP语句不通,GOTO和NULL语句对PL/SQL编程来说不是必须的。PL/SQL结构中很少用到GOTO语句。有时,它就是用于简化逻辑的。NULL用于改善可读性使条件语句看起来更加清晰。

滥用GOTO能使结构混乱,不易理解和维护(有时被称为意大利面条式代码 - spaghetti code)。所以,GOTO语句的使用一定要有节制。例如,要从一个深嵌套中跳到异常控制块,要用异常抛出而不是使用GOTO语句。

1. GOTO语句

GOTO语句可以无条件跳到一个标签处。标签名称在它所处的作用范围内必须是唯一的。执行的时候,GOTO语句会把控制权交给一个做了标记的语句或块。GOTO语句可以向上或向下跳转,示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
BEGIN
    -- 一些业务逻辑代码
    ...
    
    GOTO insert_row;  -- 跳转到标签insert_row处执行
    
    -- GOTO之后的代码不会被执行
    ...
    
    <<insert_row>>  -- 标签定义
    INSERT INTO emp  -- 跳转到emp表插入数据
    VALUES (...);    -- 插入的值列表
END;
/

再看一个向上跳转的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
BEGIN
    -- 外部块的业务逻辑代码
    ...
    
    <<update_row>>  -- 标签定义(指向内部块)
    BEGIN
        -- 内部块:执行更新操作
        UPDATE emp 
           SET ...;  -- 更新emp表的字段
        ...
    END;  -- 内部块结束
    
    -- 其他业务逻辑
    ...
    
    GOTO update_row;  -- 跳转到update_row标签处(内部块)
    
    -- GOTO之后的代码不会被执行
    ...
END;  -- 外部块结束
/

下例中的标签end_loop是不允许的,因为它并没有到达一个处理语句:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
DECLARE
    done  BOOLEAN;  -- 声明布尔类型变量用于控制循环
BEGIN
    -- 初始化或其他前置逻辑
    ...
    
    FOR i IN 1 .. 50 LOOP
        IF done THEN
            GOTO end_loop;  -- 当done为TRUE时跳转到标签
        END IF;
        
        -- 循环体内的其他处理逻辑
        ...
        
        <<end_loop>>  -- 不允许的标签位置!
    END LOOP;  -- 循环结束(非可执行语句,不能作为GOTO目标)
    
    -- 循环外的后续逻辑
    ...
END;
/

改进方法很简单,只需添加一个NULL语句即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
FOR i IN 1 .. 50 LOOP
    IF done THEN
        GOTO end_loop;  -- 当条件满足时跳转到标签处
    END IF;
    
    -- 循环体内的其他处理逻辑
    ...
    
    <<end_loop>>  -- 合法的标签位置(位于可执行语句前)
    NULL;  -- 空操作语句(可执行语句,作为GOTO的目标)
END LOOP;

GOTO语句还能从当前块跳入一个封闭的块中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
DECLARE
    my_ename  CHAR(10);  -- 声明变量存储员工姓名
BEGIN
    <<get_name>>  -- 标签:指向外层块中的SELECT语句
    SELECT ename
      INTO my_ename
      FROM emp
     WHERE ...;  -- 查询条件
    
    -- 内部匿名块
    BEGIN
        -- 内部块的业务逻辑
        ...
        
        GOTO get_name;  -- 跳转到外层块中的get_name标签
    END;  -- 内部块结束
END;  -- 外层块结束
/

约束

有些情况下,GOTO语句是不能使用的。特别是想用GOTO语句跳到IF、CASE、LOOP或子块。例如,下面的GOTO语句是不允许的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
BEGIN
    -- 一些业务逻辑代码
    ...
    
    GOTO update_row;  -- 错误:不能跳转到IF语句块内部
    
    -- GOTO之后的代码不会被执行
    ...
    
    IF valid THEN
        -- 满足条件时的处理逻辑
        ...
        
        <<update_row>>  -- 位于IF语句块内部的标签
        UPDATE emp 
           SET ...;  -- 更新emp表的操作
    END IF;  -- IF语句结束
END;
/

如下例所示,GOTO不能从一个IF的一个分支跳到另一个分支。同样,也不能从CASE的一个WHEN子句跳到另一个WHEN子句。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
BEGIN
    -- 一些业务逻辑代码
    ...
    
    IF valid THEN
        -- 满足valid条件时的处理逻辑
        ...
        
        GOTO update_row;  -- 错误:不能跳转到ELSE子句内部
    ELSE
        -- 不满足valid条件时的处理逻辑
        ...
        
        <<update_row>>  -- 位于ELSE子句内部的标签
        UPDATE emp 
           SET ...;  -- 更新emp表的操作
    END IF;  -- IF语句结束
END;
/

下例中演示了GOTO语句不能从封闭的块跳入它的子块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
BEGIN
    -- 主块业务逻辑
    ...
    
    IF status = 'OBSOLETE' THEN
        GOTO delete_part;  -- 错误:不能跳转到子块内部
    END IF;
    
    -- 其他处理逻辑
    ...
    
    -- 子块开始
    BEGIN
        -- 子块内部逻辑
        ...
        
        <<delete_part>>  -- 位于子块内部的标签
        DELETE FROM parts 
         WHERE ...;  -- 删除parts表中的记录
    END;  -- 子块结束
END;  -- 主块结束
/

同样,GOTO也不能跳出子程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
DECLARE
    -- 声明部分:变量、类型等定义
    ...
    
    -- 声明内部过程
    PROCEDURE compute_bonus(emp_id NUMBER) IS
    BEGIN
        -- 过程内部的业务逻辑
        ...
        
        GOTO update_row;  -- 错误:不能从子程序跳转到外部块
    END compute_bonus;  -- 过程结束

BEGIN
    -- 主块的业务逻辑
    ...
    
    <<update_row>>  -- 位于主块中的标签
    UPDATE emp 
       SET ...;  -- 更新emp表的操作
END;  -- 主块结束
/

最后,GOTO不能从异常控制部分跳入当前块。例如,下面的GOTO语句就是不允许的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
DECLARE
    -- 声明部分:其他变量定义
    ...
    
    pe_ratio  REAL;  -- 声明市盈率变量
BEGIN
    -- 主逻辑:计算市盈率
    SELECT price / NVL(earnings, 0)
      INTO pe_ratio
      FROM ...;  -- 数据源表
    
    <<insert_row>>  -- 插入操作标签
    INSERT INTO stats  -- 插入到统计信息表
         VALUES (pe_ratio, ...);  -- 插入市盈率及其他数据

EXCEPTION
    WHEN ZERO_DIVIDE THEN  -- 处理除零异常
        pe_ratio := 0;  -- 除零异常时将市盈率设为0
        
        GOTO insert_row;  -- 错误:不能从异常处理块跳转到当前块的执行部分
END;
/

但是,GOTO语句可以从一个异常控制程序中跳转到一个封闭块。

2. NULL语句

NULL语句本身什么都不做,只是简单的把控制权交给下一个语句而已。在控制结构中,NULL只是告诉阅读者一个可能会被考虑到的情况,而实际的没有任何动作。下面的例子中演示了忽视未命名异常的操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
EXCEPTION
    WHEN ZERO_DIVIDE THEN
        -- 处理除零异常:回滚事务
        ROLLBACK;
        
    WHEN VALUE_ERROR THEN
        -- 处理值错误异常:记录错误信息并提交
        INSERT INTO errors 
             VALUES (...);  -- 插入错误详情
        COMMIT;
        
    WHEN OTHERS THEN
        -- 处理其他所有未捕获的异常:不执行任何操作
        NULL;

在IF语句中或其他一些需要至少有一个可执行语句的地方,NULL语句就会被使用来满足这种语法。下例中,NULL语句强调了只有顶级(top-rated)雇员才能得到红利:

1
2
3
4
5
6
7
IF rating > 90 THEN
    -- 当评分大于90时,计算奖金
    compute_bonus(emp_id);
ELSE
    -- 当评分小于等于90时,不执行任何操作
    NULL;
END IF;

在程序设计时,NULL语句很容易创建stub程序。stub 程序是 “先占坑、后填肉” 的临时子程序,目的是让整个系统的框架先搭建起来,后续再补全具体逻辑(中文常叫 “桩程序”)。因为子程序的可执行部分至少需要一句可执行语句,NULL语句就会派上用场,充当占位语句,如下例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
-- 定义一个“扣款存储过程”,有两个参数(账户ID、扣款金额)
PROCEDURE debit_account(
    acct_id  INTEGER,  -- 要扣款的账户ID
    amount   REAL      -- 要扣的金额
) IS
BEGIN
    -- 关键:用NULL;占位,满足“必须有可执行语句”的语法要求
    -- 现在这个存储过程什么都不做,只是个“空架子”
    NULL;
END debit_account;

小结

在数据库时代,特别是Oracle数据库时代,PL/SQL已经成为了数据库应用程序开发的标准语言。