🏺 第5章 PL/SQL 编程
📄 一句话概括

SQL 没有流程控制,这正是 PL/SQL 存在的理由。 选择两个(IF、CASE),循环三个(LOOP、WHILE、FOR)。 三处和别的语言不一样:ELSIF 少一个 E;FOR 的上下界是闭区间;FOR 不能指定步长。

📝 本章和前面几章的关系

5.2 节把变量备好了,这一篇让变量「能按条件动」。 游标、过程、函数里的逻辑判断,全靠这一篇的东西拼。 循环这件事在 PL/SQL 里要格外小心——没有「最大执行次数」保护,写错了就是真死循环, 只能杀会话。

一、选择:IF / ELSIF / ELSE

拼写是 ELSIF,少一个 E。 写成 ELSEIF 会报语法错,而且报错位置常常指在下一行, 很难看出是拼写问题——这是新手最常卡的一处。 IF 后面没有括号,条件直接写在 THEN 前面,THEN 不能省。 END IF 是两截,中间有空格。 这三条和 C 系语言全不一样。 ELSIF 可以有很多个,ELSE 最多一个且在最后。 判断顺序是从上往下,命中一个就跳出整个 IF, 所以条件要「从窄到宽」写——写反了后面的分支永远轮不到。

二、CASE:两种形态

简单 CASE 是「一个值对多个固定分支」: CASE v_job WHEN 'CLERK' THEN ... WHEN 'MANAGER' THEN ... END。 搜索 CASE 是「一堆条件各判各的」: CASE WHEN v_sal < 1000 THEN ... WHEN v_sal < 3000 THEN ... END。 两者只能选一种,不能混。 而且 CASE 是「表达式」,能出现在赋值右边——这一点比 IF 强,IF 是语句,不能当值用。 CASE 里所有分支必须返回同一类型。 一个分支给数字、一个给字符串,编译不过。 这个限制看着烦,其实是好事——它保证了你不会在运行时拿到一个「说不清是什么类型」的值。

三、循环一:LOOP + EXIT WHEN,至少跑一次

LOOP ... END LOOP 本身是死循环,出口只有一个:EXIT 或 EXIT WHEN。 忘了写出口,就是真死循环。 EXIT WHEN 是在「循环体末尾」判断的,所以语义是「先做完这一轮,再决定走不走」。 这意味着循环体至少执行一次。 这个特性和 WHILE 正好相反。 所以「至少要尝试一次」的场景(比如先取一行再看要不要继续), LOOP + EXIT WHEN 才是对的写法。 游标遍历就是标准例子:先 FETCH,再 EXIT WHEN %NOTFOUND。 用 WHILE 写反而别扭,因为「有没有下一行」要先取一次才知道。

四、循环二:WHILE,先判断再进

WHILE 条件 LOOP ... END LOOP——条件为假就一次都不跑。 这一点和 LOOP 是根本差别。 条件在每一轮开始前重新算一次。 所以循环体里必须改那个条件依赖的变量,否则永远为真。 最常见的死循环就是这么来的:条件里用了 i,循环体里忘了 i := i + 1。 PL/SQL 不会提醒你,它就这么一直转下去,直到你杀掉会话。

五、循环三:FOR,闭区间且不能指定步长

FOR i IN 1..5 LOOP 跑 5 次,i 依次是 1 到 5,两头都含。 这和 C 语言的 for (i=0;i<5;i++) 不一样——后者是「小于」,前者是「小于等于」。 REVERSE 反的是方向,不是范围。 FOR i IN REVERSE 1..5 是 5、4、3、2、1,写法还是 1..5, 写成 5..1 会直接报「下界大于上界」。 循环变量是自动声明的——不用在 DECLARE 里写,也不能在循环体里给它赋值。 而且作用域只在循环内,出了 END LOOP 就没了。这三条都省心。 步长不能指定,FOR 只能一次走 1。 要步长 2 只能自己用 WHILE 加。 这一条和 Python 的 range(1, 6, 2) 比就很不方便, 但 PL/SQL 的 FOR 定位是「遍历」,不是「计数循环」。 CONTINUE 从 11g 起才有,写法是 CONTINUE 或 CONTINUE WHEN 条件。 在 10g 及更早版本里没有,只能用 IF ... THEN ... END IF 把剩下的代码包起来。 接手老库时这一条很实用——看到 CONTINUE 就知道这库至少是 11g。

六、动手 Demo:两个选择、三个循环,逐个跑出次数

这一段用 Python 复刻 PL/SQL 的语义,重点看「跑了几次、结果是多少」。 次数比结果更能说明问题——它直接反映「先判断还是先执行」。 FOR 的闭区间和 REVERSE 的方向,也在这一段里验证。

# IF / ELSIF / ELSE 与 CASE 在「按值分档」上等价
def grade_if(v):
    if v < 1000:
        return '低'
    elif v < 3000:
        return '中'
    else:
        return '高'

def grade_case(v):
    return '低' if v < 1000 else ('中' if v < 3000 else '高')

for v in [800, 1600, 2975, 5000]:
    print(v, grade_if(v), grade_case(v))

# 循环一:LOOP + EXIT WHEN —— 先做再判,至少跑一次
i, acc, n = 1, 0, 0
while True:
    acc += i
    n += 1
    if i >= 5:
        break
    i += 1
print('LOOP', n, '次', acc)

# 循环二:WHILE —— 先判再做,可能一次都不跑
i, acc, n = 10, 0, 0
while i <= 5:
    acc += i
    n += 1
    i += 1
print('WHILE', n, '次', acc)

# 循环三:FOR 1..5 是闭区间,REVERSE 反方向不反范围
print('FOR', sum(range(1, 6)))
print('REVERSE', list(range(5, 0, -1)))

实跑输出:

--------------------------------------------------------------------
第二十三课:流程控制——两个选择,三个循环
--------------------------------------------------------------------

  **SQL 没有流程控制,这正是 PL/SQL 存在的理由。**
  **这一节的东西别的语言都有,但 PL/SQL 有几个细节和它们不一样。**
  **记住三件事:`ELSIF` 不是 `ELSEIF`;跳出循环靠 `EXIT`/`EXIT WHEN`;`FOR` 的上下界是「闭区间」。**


  选择:IF / ELSIF / ELSE
  --------------------------------------------------------------------
  **注意拼写:PL/SQL 是 `ELSIF`,少一个 `E`。**
  **写成 `ELSEIF` 会报语法错,而且报错位置常常指在下一行,很难看出是拼写问题。**
  **`IF` 后面没有括号,条件直接写在 `THEN` 前面;`THEN` 不能省。**
  **`END IF` 是两截,中间有空格。**

  按工资分档,跑出来是这样:

  SQL> select sal as 工资, if_grade as IF写的档, case_grade as CASE写的档 from grade
  工资 IF写的档 CASE写的档
  ------------------------
  800  低       低
  1600 中       中
  2975 中       中
  5000 高       高
  已选择 4 行

  **两列结果完全一样**——`IF/ELSIF` 和 `CASE` 在同一种「按值分档」的场景下等价。
  **区别在可读性:`CASE` 更适合「一个值对多个固定分支」,`IF` 更适合「区间判断」。**

  循环一:LOOP + EXIT WHEN
  --------------------------------------------------------------------
  **`LOOP … END LOOP` 是死循环,必须靠 `EXIT WHEN` 跳出来。**  **忘了写出口,就是真死循环**——
  PL/SQL 里没有「最大执行次数」保护。

  **累加 1 到 5:循环体跑了 5 次,结果 15。**
  **走的是 1 → 2 → 3 → 4 → 5。**
  **`EXIT WHEN` 是在「循环体末尾」判断的,所以条件是「先做完这一轮再决定走不走」。**
  **这一点和 `WHILE` 相反——`WHILE` 是「先判断再进」。**

  循环二:WHILE——先判断再进
  --------------------------------------------------------------------
  **同样的累加,`WHILE i <= 5 LOOP` 结果也是 15。**
  **但两者有个关键差别:条件一开始就不成立时,`WHILE` 一次都不跑,`LOOP` 至少跑一次。**
  **`WHILE i <= 5` 而 `i` 一开始就是 10:跑 0 次,结果 0。**
  **换成 `LOOP … EXIT WHEN i > 5`:跑 1 次,结果 10。**

  循环三:FOR——闭区间,且不用自己加
  --------------------------------------------------------------------
  **`FOR i IN 1..5 LOOP` 会跑 5 次,`i` 依次是 1、2、3、4、5。**  **两头都含**——
  这是 PL/SQL 的 `FOR`,和 C 语言的 `for (i=0;i<5;i++)` 不一样。
  **累加结果 15。**
  **`FOR i IN REVERSE 1..5` 是 5、4、3、2、1,还是 5 次**——**`REVERSE` 反的是方向,不是范围**,
  写法还是 `1..5`,不是 `5..1`。
  实际顺序:5 → 4 → 3 → 2 → 1

  **`FOR` 的循环变量是「自动声明的」**——不用在 `DECLARE` 里写,
  **也不能在循环体里给它赋值。**  **而且它的作用域只在循环内**,出了 `END LOOP` 就没了。
  **步长不能指定。** PL/SQL 的 `FOR` 只能一次走 1,  **要步长 2 只能自己用 `WHILE` 加。**

  **`CONTINUE` 从 11g 起才有,写法是 `CONTINUE` 或 `CONTINUE WHEN 条件`。**
  **在 10g 及更早的版本里,只能用 `IF … THEN … END IF` 把剩下的代码包起来。**
  **接手老库的时候这一条很实用。**

分档那四行,IF 和 CASE 的结果一模一样。 说明在「按值分档」这类场景下,两者完全可以互换。 选哪个只看可读性:区间判断用 IF,固定分支用 CASE。 注意 2975 落在「中」这一档。 因为判断顺序是 < 1000 → < 3000,2975 不满足第一个,满足第二个,就停了。 如果把顺序写成 < 3000 在前,那 800 也会被判成「中」。 LOOP + EXIT WHEN 跑 5 次,累加到 15。 关键是它「先做再判」——所以即使条件一开始就成立,它也至少跑一次。 WHILE 那一段我把 i 设成 10,条件是 i <= 5,结果跑 0 次。 这正好证明了 WHILE 是「先判断再进」。 同样的初始值换成 LOOP,就会跑 1 次——这就是两者最本质的区别,也是「该用哪个」的唯一判据。 FOR 累加 1 到 5 得 15,和前面两个一样。 说明三种循环在「次数确定」的场景下等价,选哪个只看哪种写着顺。 REVERSE 打出 5、4、3、2、1,还是 5 个元素。 这说明 REVERSE 只是反向遍历,范围还是 1..5。 写成 REVERSE 5..1 在 PL/SQL 里直接报错,因为下界大于上界。

30 秒实验

  1. 把 grade_if 的分支顺序掉过来(先判 < 3000),看 800 那一行变成什么档。 这就是「条件要从窄到宽写」的反面教材。
  2. 把 WHILE 那一段的 i 改成 1,看跑几次。 再改成 5,看跑几次——边界值是「闭区间」还是「开区间」,一测就知道。
  3. 用 FOR 写一个「只累加偶数」的循环: FOR i IN 1..10 LOOP IF MOD(i,2)=0 THEN ... END IF; END LOOP;。 注意 MOD 是函数不是运算符,要写括号。
  4. 把 LOOP 那一段的 break 删掉,看会发生什么。 在 PL/SQL 里这就是死循环,只能靠 ALTER SYSTEM KILL SESSION 收拾。

常见误区

  1. 写 ELSEIF。 PL/SQL 是 ELSIF,少一个 E。报错位置还常指在下一行,很容易找错地方。
  2. 以为 FOR i IN 1..5 跑 4 次。 是 5 次,两头都含。 这是 PL/SQL 和 C 系语言最容易搞混的一处。
  3. WHILE 循环体里忘了改条件变量。 永远为真,真死循环。PL/SQL 不会替你中断。
  4. REVERSE 后面写 5..1。 报错。REVERSE 反的是方向,范围照样写 1..5。
自测三问(点开对答案) 1. `LOOP + EXIT WHEN` 和 `WHILE` 最本质的差别是什么? —— **`LOOP` 先执行再判断,至少跑一次;`WHILE` 先判断再执行,可能一次都不跑。** **「至少要尝试一次」的场景必须用 `LOOP`。** 2. `FOR i IN REVERSE 1..5` 的遍历顺序是什么? —— **5、4、3、2、1。** `REVERSE` 只反方向,**范围还是 `1..5`**。 **写成 `5..1` 会报「下界大于上界」。** 3. 为什么说 `CASE` 比 `IF` 灵活一点? —— 因为 **`CASE` 是表达式,能出现在赋值右边**;`IF` 是语句,不能当值用。 **`v_grade := CASE WHEN ... END;` 是合法的,`:= IF ...` 不合法。**

下一篇讲游标:结果集上的一个指针。声明、打开、取、关闭四步,一步都不能少。

相关笔记