BULK COLLECT 的奇怪行为
Posted
技术标签:
【中文标题】BULK COLLECT 的奇怪行为【英文标题】:Strange behaviour of BULK COLLECT 【发布时间】:2017-12-06 16:19:24 【问题描述】:我对以下 PL/SQL BULK-COLLECT 进行了修补,它对于大型表 (>50.000.000) 的更新工作速度惊人。唯一的问题是,它不执行每个表剩余
DECLARE
-- source table cursor (only columns to be updated)
CURSOR base_table_cur IS
select a.rowid, TARGET_COLUMN from TARGET_TABLE a
where TARGET_COLUMN is null;
TYPE base_type IS
TABLE OF base_table_cur%rowtype INDEX BY PLS_INTEGER;
base_tab base_type;
-- new data
CURSOR new_data_cur IS
select a.rowid,
coalesce(b.SOURCE_COLUMN, 'FILL_VALUE'||a.JOIN_COLUMN) TARGET_COLUMN from TARGET_TABLE a
left outer join SOURCE_TABLE b
on a.JOIN_COLUMN=b.JOIN_COLUMN
where a.TARGET_COLUMN is null;
TYPE new_data_type IS TABLE OF new_data_cur%rowtype INDEX BY PLS_INTEGER;
new_data_tab new_data_type;
TYPE row_id_type IS TABLE OF ROWID INDEX BY PLS_INTEGER;
row_id_tab row_id_type;
TYPE rt_update_cols IS RECORD (
TARGET_COLUMN TARGET_TABLE.TARGET_COLUMN%TYPE
);
TYPE update_cols_type IS
TABLE OF rt_update_cols INDEX BY PLS_INTEGER;
update_cols_tab update_cols_type;
dml_errors EXCEPTION;
PRAGMA exception_init ( dml_errors,-24381 );
BEGIN
OPEN base_table_cur;
OPEN new_data_cur;
LOOP
FETCH base_table_cur BULK COLLECT INTO base_tab LIMIT 5000;
IF base_table_cur%notfound THEN
DBMS_OUTPUT.PUT_LINE('Nothing to update. Exiting.');
EXIT;
END IF;
FETCH new_data_cur BULK COLLECT INTO new_data_tab LIMIT 5000;
FOR i IN base_tab.first..base_tab.last LOOP
row_id_tab(i) := new_data_tab(i).rowid;
update_cols_tab(i).TARGET_COLUMN := new_data_tab(i).TARGET_COLUMN;
END LOOP;
FORALL i IN base_tab.first..base_tab.last SAVE EXCEPTIONS
UPDATE (SELECT TARGET_COLUMN FROM TARGET_TABLE)
SET row = update_cols_tab(i)
WHERE ROWID = row_id_tab(i);
COMMIT;
EXIT WHEN base_tab.count < 5000; -- changing to 1 didn't help!
END LOOP;
COMMIT;
CLOSE base_table_cur;
CLOSE new_data_cur;
EXCEPTION
WHEN dml_errors THEN
FOR i IN 1..SQL%bulk_exceptions.count LOOP
dbms_output.put_line('Some error occured');
END LOOP;
END;
我的错误在哪里?不过对我来说它看起来是正确的。
【问题讨论】:
与您的问题无关,但在循环内提交通常是个坏主意;如果出现问题,重新启动会变得更加困难。 据我了解,这正是性能的来源。 没有。 Commit at the end of the transaction。以及来自汤姆的更多关于为什么frequent commits are bad。使用批量收集"minimizes the performance overhead of the communication between PL/SQL and SQL"。 【参考方案1】:问题出在这一行:
IF base_table_cur%notfound THEN
当找到的记录数小于LIMIT
值时,光标遇到%NOTFOUND
。因此,如果最后一次提取不完全是 5000,则不会处理这些记录。
对于第一次使用 BULK COLLECT ... LIMIT 的人来说,这是一个常见的问题。解决办法是将退出条件改为
EXIT when base_tab.count() = 0;
“我需要确保base_table_cur不为空,如果是则退出。如果为空,我会收到错误”
new_data_cur
光标包括在base_table_cur
光标中选择的表。所以我认为你不需要这两个循环。您需要一个简单的测试来查看第一个光标是否返回某些内容,然后循环第二个光标。
我对你的逻辑并不完全清楚,所以我尽可能少地改变以展示我认为你需要的那种结构。但是,UPDATE 语句看起来有点奇怪,因此您可能仍然会遇到问题。
OPEN base_table_cur;
FETCH base_table_cur BULK COLLECT INTO base_tab LIMIT 1;
if base_table_tab.count = 0 then
DBMS_OUTPUT.PUT_LINE('Nothing to update. Exiting.');
else
OPEN new_data_cur;
LOOP
FETCH new_data_cur BULK COLLECT INTO new_data_tab LIMIT 5000;
exit when new_data_tab.count() = 0;
FOR i IN base_tab.first..base_tab.last LOOP
row_id_tab(i) := new_data_tab(i).rowid;
update_cols_tab(i).TARGET_COLUMN := new_data_tab(i).TARGET_COLUMN;
END LOOP;
FORALL i IN base_tab.first..base_tab.last SAVE EXCEPTIONS
UPDATE (SELECT TARGET_COLUMN FROM TARGET_TABLE)
SET row = update_cols_tab(i)
WHERE ROWID = row_id_tab(i);
END LOOP;
CLOSE new_data_cur;
end if;
COMMIT;
CLOSE base_table_cur;
【讨论】:
我需要确保 base_table_cur 不为空,如果是则退出。如果它是空的,我会得到一个错误。 也许值得明确指出,您的退出条件替换了notfound
检查,并且紧接在fetch
之后;只是因为循环末尾已经有一个exit when
,现在是多余的,所以看起来好像是需要更改现有条件?
@APC:注意到您将提交移出循环。我知道在循环内提交不是一个好习惯,但是当必须更新多达 500.000.000 行的表时,我有某种感觉,正是这种 Devide & Conquor 方法使更新速度更快。我知道在这种情况下,应该首选 INSERT 的任何变化,但有时客户端会出现奇怪的情况,这会阻碍你这样做。我的意思是,在最坏的情况下,一次提交 500.000.000 次与 5000 次提交(也许应该上升到 75.000 次)。在我的平庸看来,后者有明显的优势。以上是关于BULK COLLECT 的奇怪行为的主要内容,如果未能解决你的问题,请参考以下文章
oracle plsql中同时记录类型、Collection和Bulk collect