我不是一个数据库人员,而且我的大部分数据库工作都是使用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个小时即可完成。基于此,它排队等待在生产中运行。
答案 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块能够正常工作?因为我说:
答案 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
次扫描,并且表格的某些部分已经移至REDO
或UNDO
。
鉴于上述所有内容,您可能最好删除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;