PL/SQL Error: A loop that contains DML statements should be refactored to use BULK COLLECT and FORALL
PL/SQL Error: A loop that contains DML statements should be refactored to use BULK COLLECT and FORALL
我已经在整个 Internet 上搜索了一些示例,但我仍然无法理解为什么我不能在该游标内使用 DML 语句。我有点想念它背后的理论,但我不会否认如何正确编写这个的例子也会让我的生活更轻松。这是我正在处理的查询(注意:我删除了未找到结果时退出,如果游标已经打开则关闭以及类似的事情只是为了关注这里的要点):
DECLARE
// lots of vars
// the cursor below gets all datasources connected to Node XXYZ123
CURSOR DataSourceCheck
IS
SELECT NODENAAM, NAAM, URL, DBNODE1, DBNODE2, DBUSERNAAM, DBNAAM
FROM SCHEMA.TABLENAME
WHERE NODENAAM = 'XXYZ123';
// this cursor will execute row-by-row based on the result set of above cursor
CURSOR CheckIfOnlyDataSource
IS
SELECT NODENAAM, NAAM, URL, DBNODE1, DBNODE2, DBUSERNAAM, DBNAAM
FROM SCHEMA.TABLENAME
WHERE DBUSERNAAM = var_dbusernaam AND (DBNode1 = var_dbnode1 OR DBNode2 = var_dbnode2);
BEGIN
OPEN DataSourceCheck;
LOOP
FETCH DataSourceCheck into var_nodenaam, var_naam, var_URL, var_dbnode1, var_dbnode2, var_dbusernaam, var_dbnaam;
var_rowcount:= 0;
OPEN CheckIfOnlyDataSource;
LOOP
FETCH CheckIfOnlyDataSource into var_nodenaam2, var_naam2, var_URL2, var_dbnode12, var_dbnode22, var_dbusernaam2, var_dbnaam2;
var_rowcount:= var_rowcount + 1;
END LOOP;
// only save result in a temp table when var_rowcount is 1 and not higher.
IF var_rowcount = 1
THEN
INSERT INTO global_temp_table
(t_dbusernaam, t_nodenaam, t_dbnode1, t_dbnode2, t_distinctcount)
VALUES
(var_dbusernaam2, var_nodenaam2, var_dbnode12, var_dbnode22, var_rowcount)
END IF;
CLOSE CheckIfOnlyDataSource;
END LOOP;
END;
失败点是这部分,消息是 DML 应该重新配置为 FORALL 或 BULK INTO 语句:
IF var_rowcount = 1
THEN
INSERT INTO global_temp_table
(t_dbusernaam, t_nodenaam, t_dbnode1, t_dbnode2, t_distinctcount)
VALUES
(var_dbusernaam2, var_nodenaam2, var_dbnode12, var_dbnode22, var_rowcount)
END IF;
我不明白为什么 DML 不能在逐行方法中工作?输出显然存储在变量 var_dbusernaam2
、var_nodenaam2
、var_dbnode12
和 var_dbnode22
中,因此我可以执行 dbms_output.put_line
来显示它们。但是,如果它已经存储到变量中,那么为什么我不能将它简单地存储到一个 table 中(这不是数十亿的批量数据,甚至不是 1000 条记录!)。
没有简单的解决方法吗?我尝试了 BULK COLLECT 和 FORALL,但我需要更多时间来理解它并正确查询 - 游标中的游标肯定不会让它变得更容易。
这不是错误,而是编号规则 4809 的 TOAD 建议。
P.S。如果两个查询中的 table 相同,则可以使用
..., COUNT(*) OVER (PARTITION BY DBNODE1, DBNODE2, DBUSERNAAM) c
在第一个查询中获取每个 DBNODE1、DBNODE2、DBUSERNAAM 的行数,不需要第二个。
除了 Mottor 的回答中的建议之外,Toad 标记您的代码的原因是逐行处理速度慢。 PL/SQL 和 SQL 引擎之间有很多上下文切换。
把它想象成在你家附近建造新墙 - 如果砖块被送到驱动器的底部,你会不会:
- 去砖堆
- 捡起一块砖
- 回到你的墙上
- 把砖块加到墙上
- 返回步骤 1 并重复直到墙完成
(相当于逐行处理)
或者:
- 把你的手推车推到砖堆旁
- 在您的手推车上装上您可以携带的砖块and/or
- 把手推车推回墙边
- 把每块砖都加到墙上
- 返回步骤 1 并重复直到墙完成。
(相当于批量处理)
当然,如果您够精明,您可以首先将砖块送到墙边,从而避免上述场景中所需的所有行走和搬运。 (这相当于基于集合的处理)。
将您的程序转变为基于集合的方法(结合 Mottor 的回答)将使它变得简单:
declare
-- lots of vars
begin
insert into global_temp_table (t_dbusernaam,
t_nodenaam,
t_dbnode1,
t_dbnode2,
t_distinctcount)
select dbusernaam,
nodenaam,
dbnode1,
dbnode2,
cnt
from (select nodenaam,
naam,
url,
dbnode1,
dbnode2,
dbusernaam,
dbnaam,
count(*) over (partition by dbnode1, dbnode2, dbusernaam) cnt
from schema.tablename
where nodenaam = 'XXYZ123')
where cnt = 1;
end;
/
这样做的好处是比您的原始代码更紧凑,更易于阅读、理解和调试。另外,您可以 运行 select 语句在过程之外单独使用 - 更容易看到它在做什么。
它也将比您原来的循环两个游标的方法更快(顺便说一下,这是重新发明嵌套循环连接——数据库在纯 SQL 中进行了优化。 .而且可能不是最快的连接方式,如果你一直坚持保持连接!)。
我也想知道为什么您需要将行插入 GLOBAL_TEMP_TABLE(我怀疑这是 GTT - 全局临时 table - 而不是普通堆 table) - 你能不能在单个 SQL 语句中进行后续处理,使用上面的 select 语句而不是将数据插入 GTT?
我已经在整个 Internet 上搜索了一些示例,但我仍然无法理解为什么我不能在该游标内使用 DML 语句。我有点想念它背后的理论,但我不会否认如何正确编写这个的例子也会让我的生活更轻松。这是我正在处理的查询(注意:我删除了未找到结果时退出,如果游标已经打开则关闭以及类似的事情只是为了关注这里的要点):
DECLARE
// lots of vars
// the cursor below gets all datasources connected to Node XXYZ123
CURSOR DataSourceCheck
IS
SELECT NODENAAM, NAAM, URL, DBNODE1, DBNODE2, DBUSERNAAM, DBNAAM
FROM SCHEMA.TABLENAME
WHERE NODENAAM = 'XXYZ123';
// this cursor will execute row-by-row based on the result set of above cursor
CURSOR CheckIfOnlyDataSource
IS
SELECT NODENAAM, NAAM, URL, DBNODE1, DBNODE2, DBUSERNAAM, DBNAAM
FROM SCHEMA.TABLENAME
WHERE DBUSERNAAM = var_dbusernaam AND (DBNode1 = var_dbnode1 OR DBNode2 = var_dbnode2);
BEGIN
OPEN DataSourceCheck;
LOOP
FETCH DataSourceCheck into var_nodenaam, var_naam, var_URL, var_dbnode1, var_dbnode2, var_dbusernaam, var_dbnaam;
var_rowcount:= 0;
OPEN CheckIfOnlyDataSource;
LOOP
FETCH CheckIfOnlyDataSource into var_nodenaam2, var_naam2, var_URL2, var_dbnode12, var_dbnode22, var_dbusernaam2, var_dbnaam2;
var_rowcount:= var_rowcount + 1;
END LOOP;
// only save result in a temp table when var_rowcount is 1 and not higher.
IF var_rowcount = 1
THEN
INSERT INTO global_temp_table
(t_dbusernaam, t_nodenaam, t_dbnode1, t_dbnode2, t_distinctcount)
VALUES
(var_dbusernaam2, var_nodenaam2, var_dbnode12, var_dbnode22, var_rowcount)
END IF;
CLOSE CheckIfOnlyDataSource;
END LOOP;
END;
失败点是这部分,消息是 DML 应该重新配置为 FORALL 或 BULK INTO 语句:
IF var_rowcount = 1
THEN
INSERT INTO global_temp_table
(t_dbusernaam, t_nodenaam, t_dbnode1, t_dbnode2, t_distinctcount)
VALUES
(var_dbusernaam2, var_nodenaam2, var_dbnode12, var_dbnode22, var_rowcount)
END IF;
我不明白为什么 DML 不能在逐行方法中工作?输出显然存储在变量 var_dbusernaam2
、var_nodenaam2
、var_dbnode12
和 var_dbnode22
中,因此我可以执行 dbms_output.put_line
来显示它们。但是,如果它已经存储到变量中,那么为什么我不能将它简单地存储到一个 table 中(这不是数十亿的批量数据,甚至不是 1000 条记录!)。
没有简单的解决方法吗?我尝试了 BULK COLLECT 和 FORALL,但我需要更多时间来理解它并正确查询 - 游标中的游标肯定不会让它变得更容易。
这不是错误,而是编号规则 4809 的 TOAD 建议。
P.S。如果两个查询中的 table 相同,则可以使用
..., COUNT(*) OVER (PARTITION BY DBNODE1, DBNODE2, DBUSERNAAM) c
在第一个查询中获取每个 DBNODE1、DBNODE2、DBUSERNAAM 的行数,不需要第二个。
除了 Mottor 的回答中的建议之外,Toad 标记您的代码的原因是逐行处理速度慢。 PL/SQL 和 SQL 引擎之间有很多上下文切换。
把它想象成在你家附近建造新墙 - 如果砖块被送到驱动器的底部,你会不会:
- 去砖堆
- 捡起一块砖
- 回到你的墙上
- 把砖块加到墙上
- 返回步骤 1 并重复直到墙完成
(相当于逐行处理)
或者:
- 把你的手推车推到砖堆旁
- 在您的手推车上装上您可以携带的砖块and/or
- 把手推车推回墙边
- 把每块砖都加到墙上
- 返回步骤 1 并重复直到墙完成。
(相当于批量处理)
当然,如果您够精明,您可以首先将砖块送到墙边,从而避免上述场景中所需的所有行走和搬运。 (这相当于基于集合的处理)。
将您的程序转变为基于集合的方法(结合 Mottor 的回答)将使它变得简单:
declare
-- lots of vars
begin
insert into global_temp_table (t_dbusernaam,
t_nodenaam,
t_dbnode1,
t_dbnode2,
t_distinctcount)
select dbusernaam,
nodenaam,
dbnode1,
dbnode2,
cnt
from (select nodenaam,
naam,
url,
dbnode1,
dbnode2,
dbusernaam,
dbnaam,
count(*) over (partition by dbnode1, dbnode2, dbusernaam) cnt
from schema.tablename
where nodenaam = 'XXYZ123')
where cnt = 1;
end;
/
这样做的好处是比您的原始代码更紧凑,更易于阅读、理解和调试。另外,您可以 运行 select 语句在过程之外单独使用 - 更容易看到它在做什么。
它也将比您原来的循环两个游标的方法更快(顺便说一下,这是重新发明嵌套循环连接——数据库在纯 SQL 中进行了优化。 .而且可能不是最快的连接方式,如果你一直坚持保持连接!)。
我也想知道为什么您需要将行插入 GLOBAL_TEMP_TABLE(我怀疑这是 GTT - 全局临时 table - 而不是普通堆 table) - 你能不能在单个 SQL 语句中进行后续处理,使用上面的 select 语句而不是将数据插入 GTT?