为啥这个 PL-SQL 循环只有在与 DDL 语句分开执行时才起作用?
Posted
技术标签:
【中文标题】为啥这个 PL-SQL 循环只有在与 DDL 语句分开执行时才起作用?【英文标题】:Why does this PL-SQL loop only work when executing it separately from DDL statements?为什么这个 PL-SQL 循环只有在与 DDL 语句分开执行时才起作用? 【发布时间】:2019-12-03 13:42:00 【问题描述】:问题
我正在尝试执行一些 DDL 语句来创建一个临时表,然后在 PL/SQL 中的游标上执行循环以将一些数据复制到新创建的临时表中,然后再执行一些 DDL 来进行一些最终调整复制数据后到表中。
但是,我最终得到了错误消息
SP2-0552: Bind variable "NEW" is not declared
在初始 DDL 之后没有任何内容(即循环中或之后的任何内容)都没有被执行。
奇怪的是,如果我一个接一个地执行语句(首先是 DDL,然后是循环,然后是剩余的 DDL)而不是使用 SQL Developer 执行单个批处理,它就像一个魅力。如果其他一切都失败了,这可能是最后的手段,但首选的解决方案是一次完成所有事情,所以我们可以只分发一个更新脚本供人们使用。我想知道问题是什么。
动机
我需要修改包含大量数据的表中的一些列(数百万行,可能有 30 列)。直接更改列会导致事务变得太大,因此我选择执行以下操作:
-
使用新列定义创建一个表。
复制数据,同时每几千个条目执行一次提交。
删除所有引用旧表的约束和索引。
放下旧桌子。
将新表重命名为旧表。
在新表上重新创建约束和索引。
最少的可重复样本
以下最小示例说明了上述步骤,但为简洁起见,我省略了任何索引和外键。
首先是应该迁移的表的定义。
-- This is the old table A.
CREATE TABLE TestA (
ID number not null,
Val varchar2(20 CHAR),
constraint PK_TestA primary key
(
ID
)
);
CREATE SEQUENCE SEQ_TestA
INCREMENT BY 1;
CREATE TRIGGER TRI_TestA
BEFORE INSERT ON TestA
FOR EACH ROW
BEGIN
SELECT SEQ_TestA.NEXTVAL
INTO :NEW.ID FROM DUAL;
EXCEPTION WHEN OTHERS THEN NULL;
END;
.
RUN;
-- Insert some test data.
INSERT INTO TestA (Val) VALUES ('Hello');
INSERT INTO TestA (Val) VALUES ('World');
INSERT INTO TestA (Val) VALUES ('Foo');
INSERT INTO TestA (Val) VALUES ('Bar');
commit;
然后是实际的迁移脚本:
-- Create the new table B with the new column definitions.
CREATE TABLE TestB (
ID number not null,
Val nvarchar2(50),
constraint PK_TestB primary key
(
ID
)
);
-- Copy data from A to B and commit every once in a while.
DECLARE
counter number := 0;
CURSOR migrationrows
IS
SELECT
ID,
Val
FROM TestA;
BEGIN
FOR migrationrow
IN migrationrows
LOOP
INSERT INTO TestB (
ID,
Val
)
VALUES
(
migrationrow.ID,
migrationrow.Val
);
counter := counter + 1;
IF counter > 2 THEN
commit;
counter := 0;
END IF;
END LOOP;
commit;
END;
-- Now get rid of the old A and make B the new A.
DROP TRIGGER TRI_TestA;
DROP TABLE TestA;
ALTER TABLE TestB RENAME TO TestA;
ALTER TABLE TestA RENAME CONSTRAINT PK_TestB TO PK_TestA;
CREATE TRIGGER TRI_TestA
BEFORE INSERT ON TestA
FOR EACH ROW
BEGIN
SELECT SEQ_TestA.NEXTVAL
INTO :NEW.ID FROM DUAL;
EXCEPTION WHEN OTHERS THEN NULL;
END;
.
RUN;
commit;
如果我从放置注释行的每个步骤中运行它,它就会起作用。但是如果我完全运行它,我会得到上面显示的错误。
【问题讨论】:
【参考方案1】:这很可能是因为您缺少 pl/sql 和触发器语句末尾的 /
s。
例如您的脚本应该类似于:
-- This is the old table A.
CREATE TABLE TestA (
ID number not null,
Val varchar2(20 CHAR),
constraint PK_TestA primary key
(
ID
)
);
CREATE SEQUENCE SEQ_TestA
INCREMENT BY 1;
CREATE TRIGGER TRI_TestA
BEFORE INSERT ON TestA
FOR EACH ROW
BEGIN
SELECT SEQ_TestA.NEXTVAL
INTO :NEW.ID FROM DUAL;
EXCEPTION WHEN OTHERS THEN NULL;
END;
/
-- Insert some test data.
INSERT INTO TestA (Val) VALUES ('Hello');
INSERT INTO TestA (Val) VALUES ('World');
INSERT INTO TestA (Val) VALUES ('Foo');
INSERT INTO TestA (Val) VALUES ('Bar');
commit;
和
-- Create the new table B with the new column definitions.
CREATE TABLE TestB (
ID number not null,
Val nvarchar2(50),
constraint PK_TestB primary key
(
ID
)
);
-- Copy data from A to B and commit every once in a while.
DECLARE
counter number := 0;
CURSOR migrationrows
IS
SELECT
ID,
Val
FROM TestA;
BEGIN
FOR migrationrow
IN migrationrows
LOOP
INSERT INTO TestB (
ID,
Val
)
VALUES
(
migrationrow.ID,
migrationrow.Val
);
counter := counter + 1;
IF counter > 2 THEN
commit;
counter := 0;
END IF;
END LOOP;
commit;
END;
/
-- Now get rid of the old A and make B the new A.
DROP TRIGGER TRI_TestA;
DROP TABLE TestA;
ALTER TABLE TestB RENAME TO TestA;
ALTER TABLE TestA RENAME CONSTRAINT PK_TestB TO PK_TestA;
CREATE TRIGGER TRI_TestA
BEFORE INSERT ON TestA
FOR EACH ROW
BEGIN
SELECT SEQ_TestA.NEXTVAL
INTO :NEW.ID FROM DUAL;
EXCEPTION WHEN OTHERS THEN NULL;
END;
/
另外,您不需要在 select 语句中选择下一个序列值,您可以这样做:
:new.id := SEQ_TestA.NEXTVAL;
你为什么要在提交中获取?你可以直接做:
insert into testb (id,
val)
select id,
val
from testa;
您可以在运行插入语句后立即通过将SQL%ROWCOUNT
存储在变量中来存储插入的行数。
【讨论】:
天哪,是的,这就是问题所在。我已经连续两天在这了。 D: 关于“跨提交获取”:我在 Oracle 中相当新手,但在 SQL-Server 中我们基本上解决了相同的问题(事务日志文件大小在 DDL 更改大表期间变得太大数据量)通过进行中间提交。所以思路是将相同的原则应用于 Oracle 并进行中间提交。使用INSERT INTO () SELECT
只会在一笔大交易中完成所有事情,不是吗?这不会导致重做日志大小的问题吗?还是有更好的方法来实现这一点?
重做日志的主要问题是磁盘空间不足。如果您要迁移数据,我希望您的磁盘空间足够大!如果您真的关心重做日志空间,您总是可以在 nologging 模式下创建表,然后执行直接路径插入(不要忘记将表切换回 logging 模式!)。您当前在每一行之后提交的方式是迁移这些数据的最慢方式(除了自己手动键入数据等!)。
单行仅用于示例目的。实际上,它更像是每 1000 或 10000 行。不过我会考虑的,谢谢。
它仍然会比 insert-as-select 慢。而且您可能会遇到ORA-01555: snapshot too old
错误。以上是关于为啥这个 PL-SQL 循环只有在与 DDL 语句分开执行时才起作用?的主要内容,如果未能解决你的问题,请参考以下文章
为啥我不能在动态 SQL 的 DDL/SCL 语句中使用绑定变量?