相关文章推荐
逃课的紫菜汤  ·  POI ...·  1 年前    · 
冷冷的上铺  ·  SAP ...·  1 年前    · 

SQL> INSERT INTO T VALUES (1);

已创建 1 行。

SQL> INSERT INTO T1 SELECT ROWNUM FROM TAB WHERE ROWNUM < 4;

已创建 3 行。

SQL> COMMIT;

提交完成。

SQL> SELECT * FROM T;

ID
----------
1

SQL> SELECT * FROM T1;

ID
----------
1
2
3

下面只需要执行下面的 PL/SQL 语句,就可以重现问题:

SQL> BEGIN
2   UPDATE T SET ID = ID;
3   FOR I IN (SELECT ID FROM T1) LOOP
4    IF I.ID = 2 THEN
5     ROLLBACK;
6    END IF;
7   END LOOP;
8  END;
9  /
BEGIN
*
1 行出现错误 :
ORA-01002:
读取违反顺序

ORA-06512:
line 3

看来是由于 ROLLBACK 语句影响了 FOR 循环中 CURSOR 的状态。如果去掉 ROLLBACK 语句或者去掉 FOR 语句前面的 UPDATE 语句,都是不会报错的。

SQL> BEGIN
2   FOR I IN (SELECT ID FROM T1) LOOP
3    IF I.ID = 2 THEN
4     ROLLBACK;
5    END IF;
6   END LOOP;
7  END;
8  /

PL/SQL 过程已成功完成。

SQL> BEGIN
2   UPDATE T SET ID = ID;
3   FOR I IN (SELECT ID FROM T1) LOOP
4    IF I.ID = 2 THEN
5     NULL;
6    END IF;
7   END LOOP;
8  END;
9  /

PL/SQL 过程已成功完成。

SQL> ROLLBACK;

回退已完成。

如果在 UPDATE 语句后面添加 COMMIT ,也是不会报错的:

SQL> BEGIN
2   UPDATE T SET ID = ID;
3   COMMIT;
4   FOR I IN (SELECT ID FROM T1) LOOP
5    IF I.ID = 2 THEN
6     ROLLBACK;
7    END IF;
8   END LOOP;
9  END;
10  /

PL/SQL 过程已成功完成。

基本上可以确认问题是由于 ROLLBACK 需要回滚 CURSOR 之前的 DML ,导致 Oracle 改变了 CURSOR 本身的状态。

SQL> SELECT * FROM V$VERSION;

BANNER
----------------------------------------------------------------
Oracle9i Enterprise Edition Release 9.2.0.4.0 -
Production
PL/SQL Release 9.2.0.4.0 - Production
CORE    9.2.0.3.0       Production
TNS for 32-bit Windows: Version 9.2.0.4.0 - Production
NLSRTL Version 9.2.0.4.0 - Production

这个问题发生在 9204 10g FOR 循环解决了这个问题:

SQL> CONN YANGTK/YANGTK@YTK102
已连接。
SQL> SELECT * FROM V$VERSION;

BANNER
----------------------------------------------------------------
Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 - Prod
PL/SQL Release 10.2.0.1.0 - Production
CORE    10.2.0.1.0      Production
TNS for 32-bit Windows: Version 10.2.0.1.0 - Production
NLSRTL Version 10.2.0.1.0 - Production

SQL> CREATE TABLE T (ID NUMBER);

表已创建。

SQL> CREATE TABLE T1 (ID NUMBER);

表已创建。

SQL> INSERT INTO T VALUES (1);

已创建 1 行。

SQL> INSERT INTO T1 SELECT ROWNUM FROM TAB WHERE ROWNUM < 4;

已创建 3 行。

SQL> COMMIT;

提交完成。

SQL> SELECT * FROM T;

ID
----------
1

SQL> SELECT * FROM T1;

ID
----------
1
2
3

SQL> BEGIN
2   UPDATE T SET ID = ID;
3   FOR I IN (SELECT ID FROM T1) LOOP
4    IF I.ID = 2 THEN
5     ROLLBACK;
6    END IF;
7   END LOOP;
8  END;
9  /

PL/SQL 过程已成功完成。

不过,只需要将 FOR 循环游标改为用户声明并 FETCH 的游标,文件就会重新:

SQL> DECLARE
2   CURSOR C_CURSOR IS SELECT ID FROM T1;
3   I C_CURSOR%ROWTYPE;
4  BEGIN
5   UPDATE T SET ID = ID;
6   OPEN C_CURSOR;
7   FETCH C_CURSOR INTO I;
8   LOOP
9    EXIT WHEN C_CURSOR%NOTFOUND;
10    IF I.ID = 2 THEN
11     ROLLBACK;
12    END IF;
13    FETCH C_CURSOR INTO I;
14   END LOOP;
15   CLOSE C_CURSOR;
16  END;
17  /
DECLARE
*
1 行出现错误 :
ORA-01002:
提取违反顺序

ORA-06512:
line 13

10G 中虽然修正了这个 bug ,但是修改的并不彻底。在 11g 中,这个问题和 10g 中一样。在 Metalink 上也没有看到 Oracle 对这个 bug 有相关的描述。

这个 bug 也很容易避免,除了上面的几种写法外,推荐一种更合理的做法:

SQL> BEGIN
2   UPDATE T SET ID = ID;
3   FOR I IN (SELECT ID FROM T1) LOOP
4    IF I.ID = 2 THEN
5     RAISE_APPLICATION_ERROR(-20000, 'USER_ERR');
6    END IF;
7   END LOOP;
8  EXCEPTION
9   WHEN OTHERS THEN
10    ROLLBACK;
11  END;
12  /

PL/SQL 过程已成功完成。

这才是一种合理的异常处理方法,而例子中采用的在循环中回滚的方式本身就是不推荐的。

广播电视节目制作经营许可证(京) 字第1234号 中国互联网协会会员