一个棘手的删除查询的 Oracle 查询优化
Posted
技术标签:
【中文标题】一个棘手的删除查询的 Oracle 查询优化【英文标题】:Oracle Query Optimisation of a tricky delete query 【发布时间】:2016-01-19 04:26:26 【问题描述】:我们为 Oracle DB 中的一个表(已经有 20 亿行)构建了一个删除查询。此查询作为 PL/SQL Proc 的一部分执行。以下是我们当前仍在测试中的查询。
DELETE from TABLE1
where ROWID IN (SELECT rid from (SELECT ROWID rid, ROW_NUMBER() over (PARTITION BY C1_Varchar2,C2_Varchar2 ORDER BY C3_Date desc) as Rank
from TABLE1 where C3_Date < ADD_MONTHS(SYSDATE, -20))
where Rank <> 1);
这个查询是删除所有从当月开始超过20个月的记录(来自TABLE1),除了由C1和C2列的唯一组合形成的最新记录。此查询将删除大约 12% 的记录。
当我们运行查询时,我们得到以下错误。
ORA-00604: 递归 SQL 级别 2 发生错误 ORA-04031: 无法分配 32 字节的共享内存 ("shared pool","select i.obj#,i.ts#,i.file#,...","SQLA","tmp")
请注意,该表是基于 C3_Date 列进行分区的。但是按照上述逻辑,我们仍然会在分区中保留很少的记录,因此不能选择删除整个分区。
谁能建议如何处理此删除以使其更高效和稳定?
计划如下:
Plan hash value: 2112788339
---------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | Pstart| Pstop |
---------------------------------------------------------------------------------------------------------------------------
| 0 | DELETE STATEMENT | | 1 | 59 | | 9080K (2)| 30:16:07 | | |
| 1 | DELETE | TABLE1 | | | | | | | |
| 2 | NESTED LOOPS | | 1 | 59 | | 9080K (2)| 30:16:07 | | |
| 3 | VIEW | VW_NSO_1 | 496M| 5684M| | 6785K (1)| 22:37:12 | | |
| 4 | SORT UNIQUE | | 1 | 11G| | | | | |
|* 5 | VIEW | | 496M| 11G| | 6785K (1)| 22:37:12 | | |
| 6 | WINDOW SORT | | 496M| 20G| 26G| 6785K (1)| 22:37:12 | | |
|* 7 | INDEX SKIP SCAN | XPKTABLE1 | 496M| 20G| | 1206K (1)| 04:01:18 | | |
| 8 | TABLE ACCESS BY USER ROWID| TABLE1 | 1 | 47 | | 1 (0)| 00:00:01 | ROWID | ROWID |
---------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
5 - filter("RANK"<>1)
7 - access("C3_Date"<ADD_MONTHS(SYSDATE@!,-15))
filter("C3_Date"<ADD_MONTHS(SYSDATE@!,-15))
【问题讨论】:
你能发布这个查询的解释计划吗?只需执行EXPLAIN PLAN FOR DELETE from TABLE1 where .... .....
,然后运行SELECT * FROM Table( DBMS_XPLAN.Display )
,然后复制结果(作为文本),并将其粘贴到问题中。
4031 是一个异常错误,可能与查询优化无关。 4031 意味着严重的内存问题。这些问题可能是由其他进程引起的,而失败的查询并不是问题的真正原因。如果它只是一个测试数据库,而你认为这是侥幸,我就重新启动它。
【参考方案1】:
此查询生成大量临时段 (26G),如 SQL 计划所示,并且查询估计运行时间超过 30 小时。因此,您会在运行时收到 ORA-04031。
这些是我的好心建议。
我建议您不要在大段的子查询中使用分析函数,因为它们会阻止优化器取消子查询并因此生成运行时视图。
第二个建议是附加分区消除谓词并使用文字或绑定变量显式提供日期值。在执行查询之前在 PL/SQL 块中预先计算它。不要对条件使用非确定性函数 SYSDATE。此外,最好保留下限日期的合理条件(这是设计问题)。
第三个是让Oracle自己找到必须通过使用另一个条件而不是显式提供rowid来删除的行的rowid。在我们的例子中,它可以减少逻辑 I/O 的数量(使用 autotrace 来验证)。
最后查询可能是这样的(我没有验证它只是想表达这个想法):
delete from TABLE1 t1_1
where C3_Date < :upper_date_bound
and C3_Date >= :lower_date_threshold
and (C1_Varchar2, C2_Varchar2, C3_Date) not in
(select C1_Varchar2, C2_Varchar2, max(C3_Date)
from table1 t1_2
where C3_Date < :upper_date_bound
and C3_Date >= :lower_date_bound
group by C1_Varchar2, C2_Varchar2)
由于要删除的行数少于表的一半,您可以考虑使用另一个子查询而不是“NOT IN”的“IN”或“EXISTS”子句。例如,在 C3_Date 列上创建本地索引,进行统计收集并在主查询中尝试这部分
...
exists (select null from table1 t1_2
where t1_2.C1_Varchar2 = t1_1.C1_Varchar2
and t1_2.C2_Varchar2 = t1_1.C2_Varchar2
and t1_2.C3_Date = t1_1.C3_Date
/* don't forget about partition selectivity hint */
and t1_2.C3_Date < :upper_date_bound
and t1_2.C3_Date >= :lower_date_bound
group by t1_2.C1_Varchar2, t1_2.C2_Varchar2
having t1_1.C3_Date < max(t1_2.C3_Date))
--
问候
【讨论】:
我看到查询总是用完临时表空间,但我从未见过其中一个会生成 ORA-4031。我仍然不相信这个问题甚至与性能有关。【参考方案2】:考虑以块的形式插入(1 天的数据)作为一种高效且稳定的替代方案。
即使表 1 中只有 12% 的月份数据也是巨大的(约 2 亿)!
当前状态下的查询也会导致 ORA-01555(读一致性)错误。它也将处理 UNDO。
我没有测试过这个示例代码,但它会让您了解如何手动创建块(每天 1 个)。
这个想法是在 20 个月之前获得最大和最小 C3_Date。从最小日期导航到最大日期。
尝试在“C3_Date”上添加一个合适的列作为前导列,以防止“INDEX SKIP SCAN”。这也可能有点帮助。祝你好运!
var i_days number ;
SELECT (max(C3_Date) - min(C3_Date)) into :i_days from TABLE1 where C3_Date < ADD_MONTHS(SYSDATE, -20);
-- CHANGE i_mig_start_date IN PROD, if required
var i_mig_start_date varchar2(30);
exec :i_mig_start_date := ADD_MONTHS(SYSDATE, -20));
--exec :i_mig_start_date := '03-OCT-2016 16:00:00';
declare
i number;
v_sql VARCHAR2(4000);
l_cmd_str VARCHAR2(4000);
l_from_datetime date;
l_to_datetime date;
begin
-- Process chunk (1 day)
FOR i IN 1..:i_days LOOP
l_from_datetime := to_date(:i_mig_start_date,'DD-MON-YYYY HH24:MI:SS')-(i);
l_to_datetime := to_date(:i_mig_start_date,'DD-MON-YYYY HH24:MI:SS')-(i-1);
l_cmd_str := 'DELETE from /*+ PARALLEL(TABLE1 4)*/ TABLE1
where ROWID IN (SELECT rid from (SELECT ROWID rid, ROW_NUMBER() over (PARTITION BY C1_Varchar2,C2_Varchar2 ORDER BY C3_Date desc) as Rank
from TABLE1 where C3_Date > to_date(:l_from_datetime) and C3_Date <= to_date(:l_to_datetime))
where Rank <> 1
)';
DBMS_OUTPUT.PUT_LINE('Processing cycle i #' || i || ' From: ' || l_from_datetime || ' To: ' || l_to_datetime ) ;
DBMS_OUTPUT.PUT_LINE(l_cmd_str) ;
execute immediate l_cmd_str using l_from_datetime, l_to_datetime;
commit;
END LOOP;
end;
/
【讨论】:
我很高兴能就“为什么”投反对票获得建设性反馈...谢谢!!以上是关于一个棘手的删除查询的 Oracle 查询优化的主要内容,如果未能解决你的问题,请参考以下文章