删除Oracle中的大量数据

时间:2009-03-13 23:31:02

标签: sql oracle plsql

我不是一个数据库人员,而且我的大部分数据库工作都是使用MySQL,所以如果这个问题中的某些内容非常天真,请原谅我。

我需要从大约有1亿行的Oracle表中删除550万行。我有一个临时表中需要删除的行的ID。如果它只是几千行,我会这样做:

DELETE FROM table_name WHERE id IN (SELECT id FROM temp_table);
COMMIT;

我需要注意和/或做些什么,因为它有550万行?我想做一个循环,就像这样:

DECLARE
  vCT NUMBER(38) := 0;

BEGIN
  FOR t IN (SELECT id FROM temp_table) LOOP
    DELETE FROM table_name WHERE id = t.id;
    vCT := vCT + 1;
    IF MOD(vCT,200000) = 0 THEN
      COMMIT;
    END IF;
  END LOOP;
  COMMIT;
END;

首先 - 这是我认为的做法 - 一次批量提交20万次?假设它是,我仍然不确定生成550万个SQL语句,批量提交200,000,或者有一个SQL语句并一次提交所有更好。

想法?最佳做法?

编辑:我运行了第一个选项,即单个删除语句,在开发过程中只需要2个小时即可完成。基于此,它排队等待在生产中运行。

9 个答案:

答案 0 :(得分:14)

第一种方法更好,因为您可以让查询优化器清楚地了解您要执行的操作,而不是尝试隐藏它。数据库引擎可能采用不同的方法在内部删除5.5m(或表的5.5%)而不是删除200k(或0.2%)。

这里还有一个关于Oracle中大量DELETE的article,你可能想要阅读它。

答案 1 :(得分:8)

最快的方法是使用CREATE TABLE AS SELECT选项创建一个NOLOGGING的新帐户。我的意思是:

ALTER TABLE table_to_delete RENAME TO tmp;
CREATE TABLE table_to_delete NOLOGGING AS SELECT .... ;

当然你必须重新创建没有验证的约束,带有nologging,grants的索引......但是非常快。

如果您在制作中遇到麻烦,可以执行以下操作:

ALTER TABLE table_to_delete RENAME to tmp;
CREATE VIEW table_to_delete AS SELECT * FROM tmp;
-- Until there can be instantly
CREATE TABLE new_table NOLOGGING AS SELECT .... FROM tmp WHERE ...;
<create indexes with nologging>
<create constraints with novalidate>
<create other things...>
-- From here ...
DROP VIEW table_to_delete;
ALTER TABLE new_table RENAME TO table_to_delete;
-- To here, also instantly

你已经照顾好了:

  • 存储过程可能无效,但会在第二次调用时重新编译。你必须测试它。
  • NOLOGGING表示会生成 minimal 重做。如果您具有DBA角色,请运行ALTER SYSTEM CHECKPOINT以确保在实例崩溃时不会丢失数据。
  • 对于NOLOGGING,表空间必须也在NOLOGGING

另一个比创建数百万个插件更好的选择是:

-- Create table with ids
DELETE FROM table_to_delete
 WHERE ID in (SELECT ID FROM table_with_ids WHERE ROWNUM < 100000);
DELETE FROM table_with_ids WHERE ROWNUM < 100000;
COMMIT;
-- Run this 50 times ;-)

PLSQL选择是不可取的,因为可以创建 Snapshot too old 消息,因为您要使用打开的游标(循环的游标)提交(并关闭事务) 。 Oracle允许它,但这不是一个好习惯。

更新:为什么我可以确保最后的PLSQL块能够正常工作?因为我说:

  • 没有其他人因任何原因使用此临时表(dba或收集统计信息的作业,dab任务,如移动,插入记录等)。这是可以确保的,因为它只是一个辅助表格。
  • 然后,在最后一个断言中,查询将以相同的计划完全执行,并将返回具有相同顺序的行。

答案 2 :(得分:7)

Oracle中执行大量删除操作时,请确保您没有用完UNDO SEGMENTS

执行DML时,Oracle首先将所有更改写入REDO日志(旧数据和新数据)。

REDO日志被填满或发生超时时,Oracle执行log synchronization:它将new数据写入数据文件(在您的情况下,标记数据文件块)自由),并将旧数据写入UNDO表空间(以便在您commit更改之前,它对并发事务仍然可见。)

当您提交更改时,yuor事务占用的UNDO段中的空间将被释放。

这意味着,如果您删除5M行数据,则需要在all段中为UNDO行添加空格,以便可以先将数据移到那里(all at once)并仅在提交后删除。

这也意味着在执行表扫描时,并发查询(如果有)需要从REDO日志或UNDO段读取。这不是访问数据的最快方式。

这也意味着如果优化器将为您的删除查询选择HASH JOIN(它很可能会这样做),并且临时表将不适合HASH_AREA_SIZE(最可能是(如果是这种情况),那么查询将需要对大表进行several次扫描,并且表格的某些部分已经移至REDOUNDO

鉴于上述所有内容,您可能最好删除200,000块中的数据,并在两者之间提交更改。

因此,您将首先摆脱上述问题,然后优化您的HASH_JOIN,因为您将获得相同数量的读取,但读取本身将更有效。

但是,在您的情况下,我会尝试强制优化器使用NESTED LOOPS,因为我预计在您的情况下会更快。

为此,请确保您的临时表在ID上有一个主键,并按以下步骤重写您的查询:

DELETE  
FROM   (
       SELECT  /*+ USE_NL(tt, tn) */
               tn.id
       FROM    temp_table tt, table_name tn
       WHERE   tn.id = tt.id
       )

您需要在temp_table上设置主键才能使此查询生效。

将其与以下内容进行比较:

DELETE  
FROM   (
       SELECT  /*+ USE_HASH(tn tt) */
               tn.id
       FROM    temp_table tt, table_name tn
       WHERE   tn.id = tt.id
       )

,看看什么更快,并坚持这一点。

答案 3 :(得分:6)

最好像第一个例子那样一次性完成所有事情。但我肯定会先用你的DBA来讨论它,因为他们可能想要收回你在清除后不再使用的块。此外,可能存在通常从用户角度看不到的日程安排问题。

答案 4 :(得分:4)

如果您的原始SQL需要很长时间,则某些并发SQL可能会运行缓慢,因为他们必须使用UNDO来重建数据版本,而不会进行未提交的更改。

妥协可能类似于

FOR i in 1..100 LOOP
  DELETE FROM table_name WHERE id IN (SELECT id FROM temp_table) AND ROWNUM < 100000;
  EXIT WHEN SQL%ROWCOUNT = 0;
  COMMIT;
END LOOP;

您可以根据需要调整ROWNUM。较小的ROWNUM意味着更频繁的提交,并且(可能)在需要应用撤销方面减少对其他会话的影响。但是,根据执行计划,可能会有其他影响,整体上可能需要更多时间。 从技术上讲,循环的“FOR”部分是不必要的,因为EXIT将结束循环。但是我对无限循环感到偏执,因为如果他们遇到困难就会很痛苦。

答案 5 :(得分:4)

我建议将其作为单个删除运行。

您要删除的子表是否有任何子表?如果是这样,请确保索引这些表中的外键。否则,您可能会对删除的每一行执行子表的完整扫描,这可能会使事情变得非常缓慢。

您可能需要一些方法来检查删除运行时的进度。见How to check oracle database for long running queries?

正如其他人所说,如果你想测试水,你可以把:rownum&lt;查询结束时10000。

答案 6 :(得分:0)

我在Oracle 7中做过类似的事情,我不得不从数千个表中删除数百万行。对于所有圆形性能,尤其是大型删除(百万行加上一个表),这个脚本运行良好。

您必须稍微修改它(即:检查用户/密码,并获得正确的回滚段)。此外,您确实需要与DBA讨论此问题,并首先在TEST环境中运行它。说完这一切之后,这很容易。函数 delete_sql()在您指定的表中查找一批rowid,然后逐批删除它们。例如;

exec delete_sql('MSF710', 'select rowid from msf710 s where  (s.equip_no, s.eq_tran_date, s.comp_data, s.rec_710_type, s.seq_710_no) not in  (select c.equip_no, c.eq_tran_date, c.comp_data, c.rec_710_type, c.seq_710_no  from  msf710_sched_comm c)', 500);

以上示例基于sql语句从表MSF170一次删除500条记录。

如果您需要从多个表中删除数据,只需在文件delete-tables.sql中包含其他exec delete_sql(...)

哦,记得把你的回滚段重新上线,它不在脚本中。

spool delete-tables.log;
connect system/SYSTEM_PASSWORD
alter rollback segment r01 offline;
alter rollback segment r02 offline;
alter rollback segment r03 offline;
alter rollback segment r04 offline;

connect mims_3015/USER_PASSWORD

CREATE OR REPLACE PROCEDURE delete_sql (myTable in VARCHAR2, mySql in VARCHAR2, commit_size in number) is
  i           INTEGER;
  sel_id      INTEGER;
  del_id      INTEGER;
  exec_sel    INTEGER;
  exec_del    INTEGER;
  del_rowid   ROWID;

  start_date  DATE;
  end_date    DATE;
  s_date      VARCHAR2(1000);
  e_date      VARCHAR2(1000);
  tt          FLOAT;
  lrc         integer;


BEGIN
  --dbms_output.put_line('SQL is ' || mySql);
  i := 0;
  start_date:= SYSDATE;
  s_date:=TO_CHAR(start_date,'DD/MM/YY HH24:MI:SS');


  --dbms_output.put_line('Deleting ' || myTable);
  sel_id := DBMS_SQL.OPEN_CURSOR;
  DBMS_SQL.PARSE(sel_id,mySql,dbms_sql.v7);
  DBMS_SQL.DEFINE_COLUMN_ROWID(sel_id,1,del_rowid);
  exec_sel := DBMS_SQL.EXECUTE(sel_id);
  del_id := DBMS_SQL.OPEN_CURSOR;
  DBMS_SQL.PARSE(del_id,'delete from ' || myTable || ' where rowid = :del_rowid',dbms_sql.v7);
 LOOP
   IF DBMS_SQL.FETCH_ROWS(sel_id) >0 THEN
      DBMS_SQL.COLUMN_VALUE(sel_id,1,del_rowid);
      lrc := dbms_sql.last_row_count;
      DBMS_SQL.BIND_VARIABLE(del_id,'del_rowid',del_rowid);
      exec_del := DBMS_SQL.EXECUTE(del_id);

      -- you need to get the last_row_count earlier as it changes.
      if mod(lrc,commit_size) = 0 then
        i := i + 1;
        --dbms_output.put_line(myTable || ' Commiting Delete no ' || i || ', Rowcount : ' || lrc);
        COMMIT;
      end if;
   ELSE 
       exit;
   END IF;
 END LOOP;
  i := i + 1;
  --dbms_output.put_line(myTable || ' Final Commiting Delete no ' || i || ', Rowcount : ' || dbms_sql.last_row_count);
  COMMIT;
  DBMS_SQL.CLOSE_CURSOR(sel_id);
  DBMS_SQL.CLOSE_CURSOR(del_id);

  end_date := SYSDATE;
  e_date := TO_CHAR(end_date,'DD/MM/YY HH24:MI:SS');
  tt:= trunc((end_date - start_date) * 24 * 60 * 60,2);
  dbms_output.put_line('Deleted ' || myTable || ' Time taken is ' || tt || 's from ' || s_date || ' to ' || e_date || ' in ' || i || ' deletes and Rows = ' || dbms_sql.last_row_count);

END;
/

CREATE OR REPLACE PROCEDURE delete_test (myTable in VARCHAR2, mySql in VARCHAR2, commit_size in number) is
  i integer;
  start_date DATE;
  end_date DATE;
  s_date VARCHAR2(1000);
  e_date VARCHAR2(1000);
  tt FLOAT;
BEGIN
  start_date:= SYSDATE;
  s_date:=TO_CHAR(start_date,'DD/MM/YY HH24:MI:SS');
  i := 0;
  i := i + 1;
  dbms_output.put_line(i || ' SQL is ' || mySql);
  end_date := SYSDATE;
  e_date := TO_CHAR(end_date,'DD/MM/YY HH24:MI:SS');
  tt:= round((end_date - start_date) * 24 * 60 * 60,2);
  dbms_output.put_line(i || ' Time taken is ' || tt || 's from ' || s_date || ' to ' || e_date);
END;
/

show errors procedure delete_sql
show errors procedure delete_test

SET SERVEROUTPUT ON FORMAT WRAP SIZE 200000; 

exec delete_sql('MSF710', 'select rowid from msf710 s where  (s.equip_no, s.eq_tran_date, s.comp_data, s.rec_710_type, s.seq_710_no) not in  (select c.equip_no, c.eq_tran_date, c.comp_data, c.rec_710_type, c.seq_710_no  from  msf710_sched_comm c)', 500);






spool off;

哦,还有最后一个提示。它会很慢,根据表格可能需要一些停机时间。测试,计时和调整是你最好的朋友。

答案 7 :(得分:0)

这里的所有答案都很棒,只需添加一件事:如果您要删除表格中所有的记录,并且确定就赢了“需要回滚,然后你想使用 truncate table 命令。

(在你的情况下,你只想删除一个子集,但对于任何潜伏着类似问题的人,我想我会加上这个)

答案 8 :(得分:-1)

对我来说最简单的方法是: -

DECLARE
L_exit_flag VARCHAR2(2):='N';
L_row_count NUMBER:= 0;

BEGIN
   :exit_code        :=0;
   LOOP
      DELETE table_name
       WHERE condition(s) AND ROWNUM <= 200000;
       L_row_count := L_row_count + SQL%ROWCOUNT;
       IF SQL%ROWCOUNT = 0 THEN
          COMMIT;
          :exit_code :=0;
          L_exit_flag := 'Y';
       END IF;
      COMMIT;
      IF L_exit_flag = 'Y'
      THEN
         DBMS_OUTPUT.PUT_LINE ('Finally Number of Records Deleted : '||L_row_count);
         EXIT;
      END IF;
   END LOOP;
   --DBMS_OUTPUT.PUT_LINE ('Finally Number of Records Deleted : '||L_row_count);
EXCEPTION
   WHEN OTHERS THEN
      ROLLBACK;
      DBMS_OUTPUT.PUT_LINE ('Error Code: '||SQLCODE);
      DBMS_OUTPUT.PUT_LINE ('Error Message: '||SUBSTR (SQLERRM, 1, 240));
      :exit_code := 255;
END;