【问题标题】: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 列)。直接更改列会导致事务变得太大,因此我选择执行以下操作:

  1. 使用新列定义创建一个表。
  2. 复制数据,同时每几千个条目执行一次提交。
  3. 删除所有引用旧表的约束和索引。
  4. 放下旧桌子。
  5. 将新表重命名为旧表。
  6. 在新表上重新创建约束和索引。

最少的可重复样本

以下最小示例说明了上述步骤,但为简洁起见,我省略了任何索引和外键。

首先是应该迁移的表的定义。

-- 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;

如果我从放置注释行的每个步骤中运行它,它就会起作用。但是如果我完全运行它,我会得到上面显示的错误。

【问题讨论】:

    标签: oracle plsql oracle11g


    【解决方案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 错误。
    猜你喜欢
    • 2014-04-25
    • 1970-01-01
    • 2011-08-28
    • 2019-07-07
    • 2012-11-06
    • 1970-01-01
    • 1970-01-01
    • 2011-12-03
    • 1970-01-01
    相关资源
    最近更新 更多