游标不是「一次拿回所有行」,是「服务端先占一块地方放结果集,然后一行一行往回取」。
它解决的是内存问题。
显式游标四步:声明、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 秒实验
- 把部门条件从 20 改成 10,看
FETCH打出几行。 再改成 40——部门 40 没有员工,游标一次都取不到,%NOTFOUND第一次就是真。 - 在循环里加一句「取到第 3 行就
EXIT」,看%ROWCOUNT停在几。 这就是「只取前 N 行」的写法,比FETCH FIRST更灵活(可以带业务条件)。 - 把
update的where改成deptno = 99,看rowcount变成 0。 这就是「改 0 行」的现场——条件没错,是数据不存在。 两种情况的处理方式完全不同,所以这个数必须看。 - 试着在
FOR循环里break出来,然后再开一个同样的循环。 你会发现第二个循环还是能正常跑——因为FOR循环的游标是每次自动新建的。
常见误区
CLOSE忘了写。 游标泄漏,OPEN_CURSORS开满就报错。 生产上这是典型故障,尤其是异常路径上更容易漏。- 循环里先判
%NOTFOUND再FETCH。 会多处理一行空数据。 顺序必须是:先取、再判、后处理。 - 以为
OPEN之后能看到别人新提交的数据。 看不到。OPEN那一刻结果集就定格了,这是游标「一致性读」的表现。 - 拿游标循环去替代一条能干完的 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 该怎么写才不埋雷。
- ☐ (本篇完,下一篇 5.5 PL/SQL 异常处理)
相关笔记
- 流程控制语句
- 子查询的用法
- Oracle 服务器结构
-
- 🐬 MySQL必知必会/00-索引
- 🏺 Oracle从入门到精通 · 索引