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

游标不是「一次拿回所有行」,是「服务端先占一块地方放结果集,然后一行一行往回取」。 它解决的是内存问题。 显式游标四步:声明、OPEN、FETCH、CLOSE,一步都不能少。 最省事的写法是 FOR r IN (查询) LOOP——四步全自动。

📝 本章和前面几章的关系

4.5 节的子查询是「查出来的结果给另一条 SQL 用」,游标是「查出来的结果给自己的代码用」。 一个在 SQL 内部流转,一个跨到 PL/SQL 里。 游标是过程、函数里最常见的结构。第 6 章写存储过程时,几乎每个都要开游标遍历一遍。

一、基本原理:为什么需要游标

SELECT ... INTO 只能接一行。 要处理多行,就得有个东西「记住取到哪了」——那就是游标。 游标在服务端占一块私有内存(在 PGA 里,2.4 节讲过),里面放着执行结果和当前指针。 FETCH 一次就往前挪一行,所以它天然是「流式」的。 这叫「逐行处理」,也叫「慢速-by-慢速」。 代价是慢:一行一次网络往返、一行一次上下文切换。 所以能一条 SQL 干完的事,别用游标循环——这是 PL/SQL 性能优化里最常被提的一条。

二、显式游标:四步,一步都不能少

四步:声明、打开、取、关闭。 声明写 CURSOR c IS SELECT ...,打开写 OPEN c,取写 FETCH c INTO ..., 关闭写 CLOSE c。 声明只是「登记一条 SQL」,不执行。 真正执行是在 OPEN 那一刻——这时候结果集才在服务端生成。 理解这一点很重要:OPEN 之后数据就「定格」了,后面别人改了表,游标里的结果不会变。 FETCH 的循环写法有个固定套路: 先 FETCH,再 EXIT WHEN c%NOTFOUND,最后才处理数据。 顺序反过来(先判断后取)会多处理一行空数据。 CLOSE 一定要写。 不关的话,那块 PGA 内存不会释放, 而 Oracle 有 OPEN_CURSORS 上限(默认 300),开满了就报错。 「游标泄漏」在生产上是很典型的一类故障。

三、游标的四个属性

属性 返回什么 什么时候用
%FOUND 布尔:最近一次 FETCH 取到行了 判断「还有没有」
%NOTFOUND 布尔:最近一次 FETCH 没取到 循环出口,最常用
%ROWCOUNT 数字:目前为止取了多少行 「只取前 N 行」
%ISOPEN 布尔:游标是不是开着 关之前先确认一下

%ROWCOUNT 和另外三个不一样:它返回数字,另外三个返回布尔。 所以 EXIT WHEN c%ROWCOUNT > 5 是合法写法,用来「只取前五行」很顺手。 %FOUND 判断的是「最近一次 FETCH」,不是「整个游标」。 所以它在循环里每一轮都会变。 在循环外、或者 OPEN 之前读它,值是未定义的——别这么用。

四、隐式游标:不用声明的那个

每执行一条 DML(insert/update/delete/merge),Oracle 自动开一个隐式游标, 名字固定叫 SQL。 不用 OPEN,不用 CLOSE,直接读属性。 最常用的就是 SQL%ROWCOUNT。 「这条 update 到底改了几行」是排障时第一个要看的东西—— 改 0 行通常意味着条件写错了,而不是数据不存在。 但它的值会被「下一条 DML」覆盖。 中间插了任何一条 DML,SQL%ROWCOUNT 就变成那条的了。 要用就立刻用,别隔着几行再读。 SELECT INTO 也有隐式游标属性。 SQL%ROWCOUNT 是 1 说明查到了,0 说明没查到—— 这比靠异常判断更省事,但它俩不能同时用(异常一抛,后面的语句不执行了)。

五、游标 FOR 循环:最省事的写法

FOR r IN (SELECT ...) LOOP ... END LOOP——四步全自动。 不用声明游标、不用 OPEN、不用 FETCH、不用 CLOSE,连 %NOTFOUND 都替你判了。 r 是一个记录变量,r.ename、r.sal 直接取。 它的类型自动按查询结果的列来定,不用写 %ROWTYPE。 代价是控制粒度变粗。 你没法在循环中途 CLOSE 再重开,也没法「退出之后回头重取」——循环一结束,游标就自动关了。 要精细控制,还得老老实实用显式游标。 实践中的默认选择就是它。 因为绝大多数「遍历一遍做点什么」的场景,都不需要精细控制。 先写 FOR 循环,真遇到限制再换显式游标——反过来先写显式游标,多出来的四行代码永远没人看。

六、游标变量:能把结果集当参数传

前面那些游标都是「编译时定死 SQL」的。 游标变量(REF CURSOR)不一样:SQL 可以在运行时才定。 典型用法:一个过程打开游标,把游标交给调用方去取。 这就是「返回一个结果集」在 Oracle 里的实现方式——Java 那边拿到的 ResultSet,底层就是它。 分两种:强类型(带 RETURN 子句,规定列结构)和弱类型(SYS_REFCURSOR,不规定)。 强类型编译期能查出列名错,弱类型查不出。 日常用得最多的是弱类型 SYS_REFCURSOR—— 因为「返回什么列」常常要到运行时才知道(比如动态拼的 SQL)。 代价就是把「列名写错」这类错误推到了运行期。

七、动手 Demo:四步走一遍,再看隐式游标的行数

这一段用 Python 的游标模拟 Oracle 的游标:fetchall 相当于 OPEN 时生成结果集, 逐行取相当于 FETCH,计数相当于 %ROWCOUNT。 重点看三处: OPEN 之后 %ROWCOUNT 是 0、update 的 rowcount 是几、 FOR 循环怎么把四步全省掉。

import sqlite3
c = sqlite3.connect(':memory:')
c.execute('create table emp (ename text, sal real, deptno int)')
c.executemany('insert into emp values (?,?,?)', [
    ('SCOTT', 3000, 20), ('FORD', 3000, 20), ('JONES', 2975, 20),
    ('ADAMS', 1100, 20), ('SMITH', 800, 20),
    ('KING', 5000, 10)])

# 显式游标:OPEN -> FETCH -> EXIT WHEN %NOTFOUND -> CLOSE
cur = c.execute('select ename, sal from emp where deptno = 20 order by sal desc')
pos = rowcount = 0
rows = cur.fetchall()
print('%ISOPEN', True, '%ROWCOUNT', rowcount)
while pos < len(rows):
    name, sal = rows[pos]
    pos += 1
    rowcount += 1
    print('FETCH', rowcount, name, sal)

# 隐式游标:SQL%ROWCOUNT 就是「改了几行」
cur2 = c.execute('update emp set sal = sal * 1.05 where deptno = 10')
print('SQL%ROWCOUNT', cur2.rowcount)

# 游标 FOR 循环:不用声明、不用开关
for r in c.execute('select ename, sal from emp where deptno = 10 order by ename'):
    print('r.ename', r[0], 'r.sal', r[1])

实跑输出:

--------------------------------------------------------------------
第二十四课:PL/SQL 游标——结果集上的一个指针
--------------------------------------------------------------------

  **游标不是「一次拿回所有行」,是「先在服务端占一块地方放结果集,然后一行一行往回取」。**
  **它解决的是内存问题:一百万行的查询,不可能一次全塞进变量里。**  **游标让你一次只处理一行。**


  显式游标:四步,一步都不能少
  --------------------------------------------------------------------

  [Oracle 原码 · 本沙箱不执行]
  | DECLARE
  |   CURSOR c_emp IS
  |     SELECT ename, sal FROM emp WHERE deptno = 20 ORDER BY sal DESC;
  |   v_name emp.ename%TYPE;
  |   v_sal  emp.sal%TYPE;
  | BEGIN
  |   OPEN c_emp;
  |   LOOP
  |     FETCH c_emp INTO v_name, v_sal;
  |     EXIT WHEN c_emp%NOTFOUND;
  |     DBMS_OUTPUT.PUT_LINE(v_name || ' ' || v_sal);
  |   END LOOP;
  |   CLOSE c_emp;
  | END;
    声明 → 打开 → 取 → 关闭,四步。

  **这台机器上把它跑一遍(用 Python 模拟那四个动作):**

  **OPEN 之后 `%ISOPEN` = 真,`%ROWCOUNT` = 0。**
  FETCH 第 1 行:SCOTT 3000.0
  FETCH 第 2 行:FORD 3000.0
  FETCH 第 3 行:JONES 2975.0
  FETCH 第 4 行:ADAMS 1100.0
  FETCH 第 5 行:SMITH 800.0
  **取完最后一行之后,`FETCH` 再执行一次,`%NOTFOUND` 才变真。**
  **所以循环里必须「先 FETCH,再判 `%NOTFOUND`」**——
  **反过来会多处理一次空数据。这个顺序错了,末尾会多出一行莫名其妙的记录。**

  四个属性:%FOUND、%NOTFOUND、%ROWCOUNT、%ISOPEN
  --------------------------------------------------------------------

  SQL> select name, means, when_true as "何时为真" from attr
  NAME      MEANS                   何时为真
  -------------------------------------------------------
  %FOUND    最近一次 FETCH 取到行了 正常取到数据
  %NOTFOUND 最近一次 FETCH 没取到   取完了,该退出了
  %ROWCOUNT 到目前为止取了多少行    永远是数字,不是布尔
  %ISOPEN   游标是不是开着          OPEN 之后、CLOSE 之前
  已选择 4 行
  **`%ROWCOUNT` 和另外三个不一样:它返回数字,另外三个返回布尔。**
  **所以 `EXIT WHEN c_emp%ROWCOUNT > 5` 也是合法写法**,  **用来「只取前五行」很顺手。**
  **注意 `%FOUND` 判断的是「最近一次 FETCH」,不是「整个游标」。**  **所以它在循环里每一轮都会变**
  ,别在循环外拿它判断「有没有数据」——**那个位置它的值是未定义的。**

  隐式游标:不用声明的那个
  --------------------------------------------------------------------
  **每执行一条 DML,Oracle 自动开一个隐式游标,名字固定叫 `SQL`。**
  **不用 `OPEN`,也不用 `CLOSE`,直接读属性。**

  **执行 `update emp set sal = sal * 1.05 where deptno = 10` 之后:**
  **`SQL%ROWCOUNT` = 3**(改了几行)
  **`SQL%FOUND` = 真**(改到行了)
  **部门 10 本来有 3 人,改完还是 3 人。**

  **隐式游标最大的用处就是 `SQL%ROWCOUNT`。**
  **「这条 `update` 到底改了几行」是排障时第一个要看的东西**——
  **改 0 行通常意味着条件写错了,而不是数据不存在。**
  **但要注意:隐式游标的属性会被「下一条 DML」覆盖。**
  **中间插了任何一条语句,`SQL%ROWCOUNT` 就变成那条的了。**  **要用就立刻用,别隔着几行再读。**

  游标 FOR 循环:最省事的写法
  --------------------------------------------------------------------

  [Oracle 原码 · 本沙箱不执行]
  | BEGIN
  |   FOR r IN (SELECT ename, sal FROM emp WHERE deptno = 10) LOOP
  |     DBMS_OUTPUT.PUT_LINE(r.ename || ' ' || r.sal);
  |   END LOOP;
  | END;
    不用声明、不用 open/fetch/close,全自动。

  **跑出来三行,`r` 就是「一整行」:**
  r.ename = CLARK    r.sal = 2450.0
  r.ename = KING     r.sal = 5000.0
  r.ename = MILLER   r.sal = 1300.0
  **它替你做掉了四件事:声明游标、`OPEN`、每轮 `FETCH`、循环结束 `CLOSE`。**
  **而且 `%NOTFOUND` 的判断也替你写好了。**
  **代价是「控制粒度变粗」**——你没法在循环中途 `CLOSE` 再重开,
  **也没法「退出之后回头重取」——循环一结束,游标就自动关掉了。**  **要精细控制,还得用显式游标。**

  游标变量:能把结果集当参数传
  --------------------------------------------------------------------
  **前面那些游标都是「编译时定死 SQL」的。**
  **游标变量(`REF CURSOR`)不一样:SQL 可以在运行时才定。**
  **典型用法是一个过程打开游标,把游标交给调用方去取。**
  **这就是「返回一个结果集」在 Oracle 里的实现方式。**
  **分两种:强类型(带 `RETURN` 子句,规定列结构)和弱类型(`SYS_REFCURSOR`,不规定)。**
  **日常用得最多的是弱类型 `SYS_REFCURSOR`**——灵活,代价是编译期查不出列名错。

OPEN 之后 %ROWCOUNT 是 0,%ISOPEN 是真。 因为结果集刚生成,还一行都没取。 %ROWCOUNT 数的是「取了多少」,不是「有多少」。 五次 FETCH 打出五行,部门 20 正好五个人。 取完之后再 FETCH 一次才会让 %NOTFOUND 变真—— 所以循环里必须「先取再判」,这一点和 5.3 节的 LOOP + EXIT WHEN 是同一个道理。 update 那条的 rowcount 是 1,因为部门 10 只有 KING 一个人。 这个数就是 Oracle 里的 SQL%ROWCOUNT。 排障时先看它,能立刻区分「条件写错」和「数据不在」。 FOR 循环那三行,代码里一行 open/fetch/close 都没有。 这就是「四步全自动」的意思。 r 直接就是一行,r[0] 是 ename、r[1] 是 sal。 注意这一段我是「先把结果全取出来再逐行走」的,这和真正的游标有区别:真游标是取一行、 处理一行、再取下一行。 数据量大的时候这个区别很要命——一次性 fetchall 会把整个结果集塞进客户端内存。 所以真写代码时,游标循环比 fetchall 更省内存,但更慢。

30 秒实验

  1. 把部门条件从 20 改成 10,看 FETCH 打出几行。 再改成 40——部门 40 没有员工,游标一次都取不到,%NOTFOUND 第一次就是真。
  2. 在循环里加一句「取到第 3 行就 EXIT」,看 %ROWCOUNT 停在几。 这就是「只取前 N 行」的写法,比 FETCH FIRST 更灵活(可以带业务条件)。
  3. 把 update 的 where 改成 deptno = 99,看 rowcount 变成 0。 这就是「改 0 行」的现场——条件没错,是数据不存在。 两种情况的处理方式完全不同,所以这个数必须看。
  4. 试着在 FOR 循环里 break 出来,然后再开一个同样的循环。 你会发现第二个循环还是能正常跑——因为 FOR 循环的游标是每次自动新建的。

常见误区

  1. CLOSE 忘了写。 游标泄漏,OPEN_CURSORS 开满就报错。 生产上这是典型故障,尤其是异常路径上更容易漏。
  2. 循环里先判 %NOTFOUND 再 FETCH。 会多处理一行空数据。 顺序必须是:先取、再判、后处理。
  3. 以为 OPEN 之后能看到别人新提交的数据。 看不到。 OPEN 那一刻结果集就定格了,这是游标「一致性读」的表现。
  4. 拿游标循环去替代一条能干完的 SQL。 慢几十倍不夸张。能一条 SQL 干完的,别写循环。
自测三问(点开对答案) 1. 游标的四个属性里,哪个返回数字? —— **`%ROWCOUNT`**,其他三个(`%FOUND`、`%NOTFOUND`、`%ISOPEN`)都返回布尔。 **所以它能直接拿来比较大小,适合「只取前 N 行」。** 2. 为什么 `FETCH` 要写在 `EXIT WHEN %NOTFOUND` 前面? —— 因为 `%NOTFOUND` 反映的是「最近一次 `FETCH`」的结果。 **先判后取的话,判断依据还是上一轮的状态**,会多处理一行空数据。 3. `SQL%ROWCOUNT` 最常用来干什么? —— 看「这条 DML 到底改了几行」。 **改 0 行通常意味着 `where` 条件写错了**, **这是排障时区分「条件错」和「数据不在」最快的办法。**

下一篇讲异常处理:把「出错」也当成一条正常路径。重点是 NO_DATA_FOUND、自定义 RAISE, 还有 WHEN OTHERS 该怎么写才不埋雷。

相关笔记