1. 从一次“多打印一行”的诡异现象说起如果你刚开始写 Oracle 的显式游标大概率会撞上这个场景明明表里只有 3 条数据dbms_output却打印了 4 行最后一行还是空的或者游标查出来一条记录都没有结果照样打印出一行莫名其妙的内容。我第一次遇到时也懵了很久反复检查 SQL 没问题select count(*)也确认是 3可循环就是多跑了一轮。这个问题的核心其实就藏在exit when cursor%notfound和fetch ... into ...这两行代码的先后顺序里。Oracle 游标的%notfound不是“实时探测表里还有没有数据”而是根据最近一次 fetch 的结果来打标记。你把它放在 fetch 前面它读到的就是上一轮的旧标记自然判断失误。这篇就围绕 Oracle 显式游标里 exit 与 fetch 的顺序问题把“为什么多打印一次”“为什么 fetch 取值为空也照样输出”这两个坑讲透。我会给出可以直接复制运行的游标循环骨架、dbms_output验证步骤以及本地复现和修正的完整过程。适合正在学 PL/SQL 游标、被%notfound绕晕的同学跟着敲一遍就能彻底搞明白。2. 先搞懂 %notfound 到底在判断什么要理解顺序问题得先接受一个反直觉的事实cursor%notfound是一个状态标记不是一次查询动作。它反映的是“上一次 fetch 有没有取到行”而不是“现在还有没有行可取”。2.1 游标的四个属性各管什么Oracle 显式游标有四个常用属性理解它们的分工很关键属性含义何时被更新%found上次 fetch 是否取到行每次 fetch 后%notfound上次 fetch 是否没取到行每次 fetch 后%rowcount到目前为止 fetch 到的行数每次 fetch 后累加%isopen游标是否处于打开状态open/close 时重点看前两个它们只在 fetch 执行后才刷新。你 open 完游标、还没 fetch 的时候%notfound的初始值是false也就是“还没判定为没找到”。这个初始值正是“空游标也打印一行”的元凶。2.2 一个类比取快递的号码牌把 fetch 想成去快递柜取件%notfound想成柜门上的指示灯。你还没去取没 fetch灯默认是“有件”状态%notfound false。只有当你真的去取了一次、发现柜子是空的灯才会翻成“无件”%notfound true。所以如果你在“去取”之前就问“灯是不是无件”它当然回答“不是”于是你就不会退出循环继续往下走——这就是多跑一轮的根源。3. 错误写法复现exit 在 fetch 之前先看网上流传很广、也是初学者最容易写出的版本。注意exit when和fetch的顺序declare cursor c_emp is select ename, empno from emp; v_row emp%rowtype; begin open c_emp; loop exit when c_emp%notfound; -- 先判断 fetch c_emp into v_row; -- 后取值 dbms_output.put_line(v_row.ename || --- || v_row.empno); end loop; close c_emp; end; /假设emp表里有 3 行。我们逐轮推演一下第一轮open 后%notfound初始为 falseexit 不触发fetch 取到第 1 行%notfound仍为 false打印第 1 行。第二轮exit 读到 false不退出fetch 取到第 2 行打印第 2 行。第三轮exit 读到 false不退出fetch 取到第 3 行打印第 3 行。第四轮exit 读到 false因为上一轮 fetch 成功标记还是 false不退出fetch 这次取不到行%notfound翻成 true但v_row 不会被清空它还是第 3 行的值打印第 3 行的内容——于是你看到重复的一行。第五轮exit 这次读到 true终于退出。结果就是3 行数据打印了 4 次最后那次是重复的旧值。如果表是空的第一轮 exit 读到初始 false 不退出fetch 取空、%notfound变 true但 v_row 是未初始化的空值照样打印一行空白——这就是“空游标也打印”的原因。4. 正确骨架先 fetch 再 exit修正思路只有一句话让 exit 永远读最新鲜的 fetch 结果。把 fetch 放到 exit 前面declare cursor c_emp is select ename, empno from emp; v_row emp%rowtype; begin open c_emp; loop fetch c_emp into v_row; -- 先取值 exit when c_emp%notfound; -- 再判断 dbms_output.put_line(v_row.ename || --- || v_row.empno); end loop; close c_emp; end; /再推演一遍3 行数据第一轮fetch 取到第 1 行%notfound falseexit 不触发打印第 1 行。第二轮fetch 取到第 2 行打印第 2 行。第三轮fetch 取到第 3 行打印第 3 行。第四轮fetch 取不到%notfound trueexit 立即触发在打印之前就退出所以不会打印脏数据。空表的情况第一轮 fetch 取空%notfound trueexit 直接退出一行都不打印。完美。4.1 为什么这个顺序能同时解决两个问题关键点在于exit when紧跟在 fetch 之后它读到的永远是刚刚那次 fetch 的判定结果。取到行就继续取不到就立刻退出绝不会走到打印语句。而错误写法里exit 读的是上一轮的旧标记永远慢一拍于是多跑一轮、多打印一次。另外要记住fetch 取不到行时不会清空 into 的变量。这也是为什么错误写法会打印出“上一次的旧值”而不是空——变量里还留着上一轮的数据。这个机制本身没错错的是你在取空之后还去用它。5. 用 dbms_output 本地验证与排障光看理论不够建议你在本地实际跑一遍亲眼看到差异。下面给出可复制的验证步骤。5.1 准备一张测试表create table t_cursor_demo ( id number, name varchar2(20) ); insert into t_cursor_demo values (1, A); insert into t_cursor_demo values (2, B); insert into t_cursor_demo values (3, C); commit;5.2 打开输出并分别运行两种写法在 SQL*Plus 或 SQL Developer 里先执行set serveroutput on;然后分别运行第 3 节的错误写法和第 4 节的正确写法对比输出行数。你会清楚看到错误版本打印 4 行最后一行是 C 的重复正确版本打印 3 行。5.3 用 %rowcount 辅助确认想更直观地看循环跑了几轮可以在循环里加一行计数dbms_output.put_line(第 || c_emp%rowcount || 次 fetch当前行 || v_row.name);注意%rowcount只在 fetch 成功后累加取空那一次不会加。如果发现打印次数比%rowcount大说明你在取空之后还执行了打印顺序就是错的。5.4 常见报错与排查对照现象原因修正多打印一行重复数据exit 在 fetch 前慢一拍把 fetch 提到 exit 前空表也打印一行空白%notfound初始为 false同上先 fetch 再 exit打印出上一轮的旧值fetch 取空不清空变量取空后立即 exit别用变量ORA-01001: 无效的游标忘了 open 或已 close检查 open/close 配对循环不退出exit 条件写错或没写确认exit when c_emp%notfound6. 把游标写对之后顺手把工具链也理顺游标顺序这种坑本质是“细节决定成败”。写 PL/SQL 时类似的边界问题还有很多比如批量 fetch 的limit、for update的锁范围、异常处理里游标没关导致泄漏等。想少踩坑除了多写多验证把开发环境里的模型调用和编码辅助工具理顺也能省不少事。如果你平时会用大模型帮忙解释报错、生成游标模板或审查 PL/SQL 逻辑可以到 TaoToken 模型对话 里直接贴代码问它对 Oracle 游标这类语法细节的追问响应挺稳。需要长期在 IDE 里做编码辅助、接 Agent 工作流的可以看下 Coding Plan把模型能力接进日常开发链路。接入前先在 API Keys 里生成密钥具体参数和调用方式参考 接入文档照着配就行。回到游标本身记住那条铁律fetch 在前exit 在后打印永远放在 exit 之后。把这三行的顺序刻进肌肉记忆多打印一次和空值输出的问题就再也不会找上你了。