为啥这个 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 语句分开执行时才起作用?的主要内容,如果未能解决你的问题,请参考以下文章

您可以从立即执行语句中退出 PL-SQL 循环吗?

为啥我不能在动态 SQL 的 DDL/SCL 语句中使用绑定变量?

pl-sql 不能比较两个字符串?为啥[重复]

为啥这个 switch 语句在运行时会结束 while 循环?

sql 循环更新(PL-SQL)

处理PL-SQL异常并继续循环