1. 从 9i 升到 11g 后 SQL 突然报错:先搞清楚版本差异在哪
如果你手上有一套跑了七八年的 Oracle 9i 老系统,最近要迁到 11g,大概率会遇到这种场面:同一段 SQL,9i 里跑得好好的,换到 11g 直接抛错,或者结果集悄悄变了。这不是你写错了,而是 11g 在语法校验、GROUP BY 语义、游标行为和全表扫描策略上都做了收紧。这篇就围绕oracle sql group by 全表扫描 ORA-01002这几个关键词,把升级迁移里最容易踩的几类写法拆开讲清楚,每个都给你可复制的对照示例和排查步骤。
先说清楚这篇适合谁:正在做 9i 到 11g(或更高版本)数据库迁移的 DBA、后端开发,以及需要排查历史 SQL 兼容性问题的同学。核心要解决的问题有三个——哪些 SQL 写法在 11g 会直接报错、ORA-01002 到底怎么复现和定位、以及怎么用执行计划和版本参数逐条验证。我试过在测试库上把这几类语句一条条跑一遍,下面按「问题现象 → 复现步骤 → 排查方法」的顺序展开,你可以直接照着在测试环境验证。
需要提前说明的是,11g 的很多「报错」其实是把 9i 时代依赖未定义行为的写法给禁掉了。9i 允许你写一些语义模糊的 SQL,数据库帮你「猜」一个结果;11g 则要求语义明确,猜不出来就报错。理解这一点,后面的排查思路就顺了。
2. 迁移前的前置准备:环境、参数与工具链
在动手改 SQL 之前,先把验证环境搭好,否则你改一条测一条,效率极低。这一节讲清楚需要准备什么,以及怎么用 TaoToken 这类工具辅助你快速验证模型对 SQL 语义的理解(比如让它帮你解释某段 SQL 在 11g 下的行为差异)。
2.1 数据库侧的准备
你需要一个 11g 的测试实例,版本建议 11.2.0.3 及以上,因为很多兼容性行为在这个版本已经稳定。关键参数先确认几个:
| 参数名 | 9i 常见值 | 11g 建议关注点 |
|---|---|---|
optimizer_mode | RULE / CHOOSE | 11g 默认 ALL_ROWS,CHOOSE 已废弃 |
compatible | 9.2.0 | 迁移时先设成 11.2.0 观察行为 |
cursor_sharing | EXACT | 保持 EXACT,避免绑定变量窥探干扰 |
db_cache_size | 较小 | 影响全表扫描是否走 direct path read |
你可以用下面这条语句确认当前实例的兼容性设置:
show parameter compatible; show parameter optimizer_mode; select * from v$version;如果compatible还停留在 9.2.0,很多 11g 的新行为不会触发,排查时会误判。建议在测试库上先把它调到 11.2.0,再复现问题。
2.2 用 TaoToken 辅助理解 SQL 语义差异
迁移过程中经常遇到「这段 SQL 到底在 9i 里是什么意思」的问题。这时候可以用 TaoToken 的模型对话能力,把 SQL 贴进去让它解释执行语义,尤其是 GROUP BY 和游标相关的模糊写法。接入方式很简单,Base URL 用https://taotoken.net/api,Key 在控制台生成。
如果你要长期做迁移排查,建议直接开 Coding Plan,把常用的 SQL 对照脚本和排查清单沉淀下来。模型对话入口在 https://taotoken.net/api ,控制台和 API Keys 在 https://taotoken.net/console 和 https://taotoken.net/api-keys 。文档在 https://taotoken.net/doc 。
2.3 准备对照测试表
后面几类问题都需要复现,先把测试表建好:
create table tmp(id number, flag number); insert into tmp values(1,1); commit; create table t1(c varchar2(2)); insert into t1 values('a'); commit; create table exam_decision_main(seq number, clientno number); insert into exam_decision_main values(1, 100); commit;这几张表后面会反复用到,建一次就行。
3. 三类典型 SQL 写法对照:GROUP BY、INSERT 自引用、游标回滚
这一节是核心,把 9i 能跑、11g 报错的三类写法逐条对照。每条都给你 9i 的原始写法和 11g 的修正写法,以及可复制的验证脚本。
3.1 GROUP BY 单独使用:9i 自动排序,11g 不保证顺序
9i 里GROUP BY会隐式排序,所以很多人写 SQL 时省略了ORDER BY,依赖 GROUP BY 的排序结果。11g 不再保证这个顺序,结果集顺序可能变化,如果业务代码依赖顺序就会出问题。
更严重的是「仅单行 GROUP BY 时查询结果可包含其他列名」这种写法。看这个例子:
-- 9i 可以执行,11g 报 ORA-00979 select a, flag from (select '1' a, flag from tmp) group by flag;在 9i 里,因为flag只有一行,数据库「猜」出a的值是'1',所以能返回。11g 严格执行 GROUP BY 规则:SELECT 列表里非聚合列必须出现在 GROUP BY 中,否则报ORA-00979: not a GROUP BY expression。
修正写法有两种,看你的业务需求:
-- 方案一:把 a 也放进 GROUP BY select a, flag from (select '1' a, flag from tmp) group by a, flag; -- 方案二:用聚合函数包住 a select max(a) a, flag from (select '1' a, flag from tmp) group by flag;排查方法:在 9i 库里搜所有「GROUP BY 但 SELECT 列表有非聚合列」的 SQL。可以用这个查询从v$sql里捞:
select sql_text from v$sql where upper(sql_text) like '%GROUP BY%' and upper(sql_text) not like '%ORDER BY%';捞出来逐条人工确认,重点看 SELECT 列表和 GROUP BY 列表是否一致。
3.2 INSERT 时查询表本身:9i 容忍,11g 报错
第二类写法是在 INSERT 的 VALUES 子句里查询同一张表:
-- 9i 不报错,11g 报 ORA-00904 或语义错误 insert into exam_decision_main a (seq) values ( (select decode(max(seq), null, 1, max(seq) + 1) seq from exam_decision_main where clientno = a.clientno) );9i 对这种自引用 INSERT 比较宽容,11g 则要求语义明确,a.clientno这种别名引用在 VALUES 子句里解析会出问题。
修正写法:把子查询改成先算好再插入,或者用 MERGE:
-- 方案一:先查后插 declare v_seq number; v_clientno number := 100; begin select decode(max(seq), null, 1, max(seq) + 1) into v_seq from exam_decision_main where clientno = v_clientno; insert into exam_decision_main(seq, clientno) values(v_seq, v_clientno); commit; end; / -- 方案二:用 MERGE merge into exam_decision_main a using (select 100 clientno from dual) b on (a.clientno = b.clientno) when not matched then insert (seq, clientno) values (1, b.clientno) when matched then update set a.seq = a.seq + 1;排查方法:搜所有 INSERT 语句里带子查询且子查询引用了目标表的 SQL。
3.3 游标前有未提交 DML + 循环内 ROLLBACK:ORA-01002 复现
这是最典型的一类,也是 ORA-01002 最常见的触发场景。看复现脚本:
set serveroutput on declare cursor cur is select c from t1; v varchar2(2); begin insert into t1 values('1'); open cur; loop fetch cur into v; exit when cur%notfound; rollback; end loop; close cur; end; /在 9i(9.2.0.8)上这段不报错,在 10g(10.2.0.4)和 11g(11.2.0.3.2)上会报ORA-01002: fetch out of sequence。原因是游标打开后,循环里执行了 ROLLBACK,导致游标依赖的一致性读快照失效,再次 FETCH 就报错。这属于 Bug 13256185,官方在 10.2 之后的行为变更。
注意一个细节:用FOR ... LOOP不报错,用显式OPEN/FETCH/CLOSE才报错。因为 FOR LOOP 内部对游标状态做了额外处理。
修正写法:把 ROLLBACK 移出游标循环,或者用 FOR LOOP:
-- 方案一:ROLLBACK 移到循环外 declare cursor cur is select c from t1; v varchar2(2); begin insert into t1 values('1'); open cur; loop fetch cur into v; exit when cur%notfound; end loop; close cur; rollback; end; / -- 方案二:改用 FOR LOOP begin insert into t1 values('1'); for r in (select c from t1) loop null; end loop; rollback; end; /排查方法:搜所有 PL/SQL 里同时出现OPEN、FETCH、ROLLBACK的存储过程。可以用这个查询:
select name, type from dba_source where upper(text) like '%ROLLBACK%' and name in ( select name from dba_source where upper(text) like '%FETCH%' );4. 验证请求与成功结果:执行计划 + 版本参数逐条确认
改完 SQL 不能只看「不报错了」,还要确认结果集和执行计划符合预期。这一节给你一套验证流程。
4.1 用执行计划确认全表扫描行为
9i 和 11g 的全表扫描策略不同:9i 全表扫描的数据会缓存在 DB CACHE 中,11g 则通过 DIRECT PATH READ 进入 PGA,不缓存。这意味着同一个表如果被多次全表扫描,11g 的效率可能低于 9i。
用EXPLAIN PLAN看执行计划:
explain plan for select * from exam_decision_main where clientno = 100; select * from table(dbms_xplan.display);重点看TABLE ACCESS FULL这一行,以及Note部分有没有dynamic sampling之类的提示。如果发现某张表被频繁全表扫描,考虑加索引:
create index idx_edm_clientno on exam_decision_main(clientno);加完再跑一次执行计划,确认变成INDEX RANGE SCAN。
4.2 用版本参数确认兼容性行为
前面提到的compatible参数,直接决定了很多行为是否触发。验证方法:
select name, value from v$parameter where name = 'compatible';如果值是 9.2.0,很多 11g 的新校验不会生效,你测出来的「不报错」是假象。测试库上建议设成 11.2.0:
alter system set compatible = '11.2.0' scope = spfile; -- 需要重启实例4.3 用 TaoToken 模型对话验证 SQL 语义
改完的 SQL 如果不确定语义对不对,可以贴到 TaoToken 模型对话里,让它解释执行逻辑,尤其是 MERGE 和游标改写这类容易出错的场景。入口在 https://taotoken.net/api ,选模型对话即可。
5. 本篇常见报错排查:ORA-01002、ORA-00979、401 与 local proxy failed
这一节把迁移过程中最常遇到的报错集中列出来,对照真实错误信息给排查方向。
5.1 ORA-01002: fetch out of sequence
这是本篇的核心报错。触发条件:游标打开后,在 FETCH 之间执行了 COMMIT 或 ROLLBACK。排查步骤:
第一步,确认报错的存储过程里有没有OPEN ... FETCH ... ROLLBACK/COMMIT的组合。第二步,看是不是用了显式游标而不是 FOR LOOP。第三步,确认数据库版本,9.2.0.8 不报,10.2.0.4 及以上报。
修正就是前面 3.3 节给的两种方案。如果业务逻辑必须在中途 ROLLBACK,考虑把游标数据先批量取到集合变量里,再处理。
5.2 ORA-00979: not a GROUP BY expression
触发条件:SELECT 列表里有非聚合列没出现在 GROUP BY 中。排查方法:把报错的 SQL 拿出来,逐列对照 GROUP BY 列表。修正用 3.1 节的两种方案。
5.3 401 与 local proxy failed
如果你在用工具链(比如 Cline、Codex 这类)辅助排查,可能会遇到 401 或 local proxy failed。401 通常是 API Key 没配或过期,去 https://taotoken.net/api-keys 重新生成。local proxy failed 一般是本地代理配置问题,检查 Base URL 是否写成了https://taotoken.net/api,注意不要多加路径。
5.4 reading choices 报错
这个报错通常出现在调用模型接口时返回结构解析失败。检查请求体里的 model 参数是否拼写正确,以及返回的 JSON 结构是否符合预期。如果用的是 Coding Plan,确认套餐还在有效期内。
5.5 OAuth 相关报错
如果你在接入 Claude Code 或类似工具时遇到 OAuth 报错,检查回调地址和 token 是否匹配。Claude Code 的接入文档在 https://taotoken.net/doc ,里面有完整的配置步骤。
6. 迁移排查清单与长期编码方案
把前面的内容收拢成一份可执行的检查清单,你迁移时按这个顺序走一遍,基本能覆盖大部分兼容性问题。
第一步,确认测试库compatible参数已设为 11.2.0,optimizer_mode为 ALL_ROWS。第二步,从v$sql和dba_source里捞出所有 GROUP BY、INSERT 自引用、游标含 ROLLBACK 的 SQL。第三步,逐条在 11g 测试库执行,记录报错。第四步,按本篇给的修正方案改写,改写后用执行计划确认性能。第五步,回归测试业务逻辑,重点验证结果集顺序和数值。
如果你要长期做这类迁移和 SQL 审查工作,建议开 TaoToken 的 Coding Plan,把排查脚本、对照示例、修正模板都沉淀成可复用的资产。Coding Plan 入口在 https://taotoken.net/api ,控制台在 https://taotoken.net/console 。模型对话适合临时验证单条 SQL,Coding Plan 适合把整套迁移流程固化下来。
最后提醒一个容易忽略的点:11g 对ORDER BY在 EXISTS 子查询里的处理也和 9i 不同。9i 和 10g 里,EXISTS 子查询里的 ORDER BY 是无效的,11g 支持但会忽略。如果你有类似写法,迁移时顺手清理掉,避免误导后续维护的人。