Oracle 查询性能:VIEW PUSHED PREDICATE 导致内部查询多次执行
Oracle Query performance : VIEW PUSHED PREDICATE causing multiple executions on inner query
这与我们在 table 和视图(包含 70 多万条记录)上的查询有关的性能问题有关。
在对不同环境中的执行计划进行广泛分析后,我可以将其指向其中一个连接的 VIEW PUSHED PREDICATE 分支。
The number of executions(starts column on execution plan) is equal to
the number rows returned on the driving/outer table - may be it is
evaluating the view for every match on the outer result set.
由于这里涉及的 table 有数百万条记录,
%CPU 和整体执行时间变得非常糟糕。如果我添加一个不推送谓词的提示(no_push_pred),情况就不是这样了;处决只有 1 次。
Is this something expected with VIEW PUSHED PREDICATE or am I missing
any concept around this?
Oracle 数据库版本:12c 企业版 12.1.0.2.0
我尝试使用简单的查询来模拟问题(或行为)- 请参阅下面的详细信息。
注意:这里添加了 no_merge 提示以确保优化器在连接期间不会合并视图,因此该计划与我的实际查询计划相同。
查询:
SELECT
v.STATUS_CODE,
a1.STATUS_DESC
FROM STATUS_DETAIL a1,
(select /*+ no_merge push_pred */
a2.STATUS_CODE
from STATUS a2
where a2.STATUS_CODE < 50) v
where a1.STATUS_CODE = v.STATUS_CODE;
执行计划(使用TABLE(DBMS_XPLAN.display_cursor)提取):
I am referring to Line no 3 & 4 from plan - Starts column has the
value 70 ( equal to value of A-Rows column on Line no 2 - driving
table access)
-----------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | | 213 (100)| | 22 |00:00:00.01 | 350 |
| 1 | NESTED LOOPS | | 1 | 13 | 533 | 213 (0)| 00:00:01 | 22 |00:00:00.01 | 350 |
| 2 | TABLE ACCESS FULL | STATUS_DETAIL | 1 | 70 | 1960 | 3 (0)| 00:00:01 | 70 |00:00:00.01 | 7 |
| 3 | VIEW PUSHED PREDICATE | | 70 | 1 | 13 | 3 (0)| 00:00:01 | 22 |00:00:00.01 | 343 |
|* 4 | FILTER | | 70 | | | | | 22 |00:00:00.01 | 343 |
|* 5 | TABLE ACCESS FULL | STATUS | 49 | 1 | 4 | 3 (0)| 00:00:01 | 22 |00:00:00.01 | 343 |
-----------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - filter("A1"."STATUS_CODE"<50)
5 - filter(("A2"."STATUS_CODE"="A1"."STATUS_CODE" AND "A2"."STATUS_CODE"<50))
你是对的,VIEW PUSHED PREDICATE
操作意味着"The view thus becomes correlated and must be evaluated for each row of the outer query block"。
这是一种特殊的谓词推送,join predicate pushdown 转换。该转换背后的想法是视图可能需要更频繁地执行,但是将连接谓词添加到视图可以使其 运行 快得多,因为该视图中的 table 现在可以使用索引访问。
连接谓词下推本身没有任何问题。类似于笛卡尔积,多次执行一个视图不一定是坏的。有时,大量快速的事物比少量缓慢的事物更好。
那么为什么 Oracle 在这里做出了错误的选择?没有更多的数据很难说。甲骨文正在做一些决定,使用大致如下的等式:
large number * small amount of time
<
small number * large amount of time
再详细一点:
rows returned by outer query * time for index-accessed view
<
1 (for a hash join) * read smaller table, create hash function, then read the other table and probe it for matches, potentially writing and reading to temporary tablespace
与大多数查询调优一样,检查基数。
也许 Oracle 明显低估了 "large number" 并认为外部 table 返回的行比实际情况要小得多。发生这种情况的原因有很多,比如糟糕的统计数据、使用优化器无法估计的大量令人困惑的函数、使用 Oracle 无法理解的关系的相关列(除非你创建多列直方图)等。
也许 Oracle 大大低估了 "small amount of time"。它可能认为视图的索引访问路径比实际要快得多。这可能是由于上述原因之一,也可能是因为有人弄乱了一些关键参数。不幸的是,人们认为这太普遍了,"indexes are fast, I should tell Oracle to use them more often by changing parameter defaults"。 运行 此查询并确保值为默认值 0 和 100。
select * from v$parameter where name in ('optimizer_index_cost_adj', 'optimizer_index_caching');
在实践中,优化器问题几乎总是由 Oracle 低估某些东西而不是高估造成的。所以我会关注等式的左边。 Oracle 总是试图丢弃尽可能多的行,并且总是寻找一种方法将基数降低到 1。如果真的有一条路径可以只从 table 中获取一行,那可以节省大量工作。但是,如果只有一种方法 看起来 像是通往一行的快速路径,但实际上并非如此,它可能会搞乱整个计划。
如果这是一个包含大量细节的巨大查询,放弃尝试查找根本原因并简单地使用提示或额外的 rownum
伪列来强制 Oracle 停止转换并不是没有道理的东西。
Oracle 提供了大量数据结构和算法来访问数据。这为优化器提供了很多方法来找到更快的 运行 查询方法。但这也给了它更多犯错的机会。没有风险就没有回报,但没有必要在每个查询上都赌一把。如果您有一个外部查询和一个内部查询,它们可以单独工作,但不能很好地协同工作,请务必将它们分开,不要让 Oracle 尝试以奇怪的方式组合它们。
下面是一个快速示例,使用的 tables 与您查询中的相似。它使用 VIEW PUSHED PREDICATE
操作错误地显示 Oracle。
首先,创建一些小的 tables,插入数据,并收集统计信息。到目前为止一切看起来都很好。
drop table status_detail;
drop table status;
create table status_detail(status_code number, status_desc varchar2(100));
insert into status_detail select level, level from dual connect by level <= 10;
create table status(status_code number);
create index status_idx on status(status_code);
insert into status select level from dual connect by level <= 100000;
begin
dbms_stats.gather_table_stats(user, 'status_detail');
dbms_stats.gather_table_stats(user, 'status');
end;
/
错误来了。假设有人在 STATUS_DETAIL table 中加载了 100,000 行,但忘记重新收集统计信息。 Oracle 认为外部 table 只有 10 行但实际上有 100,000。
insert into status_detail select 1, level from dual connect by level <= 100000;
commit;
alter system flush shared_pool;
运行 STATUS_DETAIL 和使用 STATUS 的内联视图之间的查询。为了防止视图合并,我在查询中添加了一个 join
和一个 distinct
以使其难以集成 A1 和 V.
explain plan for
select count(*)
from status_detail a1,
(
select distinct a2.status_code
from status a2
join status a3
on a2.status_code=a3.status_code
) v
where a1.status_code = v.status_code;
下面是错误的执行计划。 Oracle 认为 STATUS_DETAIL 只有 returns 10 行,因为优化器统计数据不佳。那个 STATUS table 很大,将它连接到自身会很昂贵。 Oracle 可以使用连接谓词下推,而不是连接大型 table。通过将 STATUS_CODE 谓词传递到视图中,现在它可以对大型 STATUS table 使用简单的 INDEX RANGE SCAN 操作。 10 个小索引范围扫描听起来比散列连接两个大 tables 更快。
select * from table(dbms_xplan.display);
Plan hash value: 3172146404
------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 5 | 23 (0)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | 5 | | |
| 2 | NESTED LOOPS SEMI | | 10 | 50 | 23 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL | STATUS_DETAIL | 10 | 30 | 3 (0)| 00:00:01 |
| 4 | VIEW PUSHED PREDICATE | | 1 | 2 | 2 (0)| 00:00:01 |
| 5 | NESTED LOOPS SEMI | | 1 | 10 | 2 (0)| 00:00:01 |
|* 6 | INDEX RANGE SCAN | STATUS_IDX | 1 | 5 | 1 (0)| 00:00:01 |
|* 7 | INDEX RANGE SCAN | STATUS_IDX | 1 | 5 | 1 (0)| 00:00:01 |
------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
6 - access("A2"."STATUS_CODE"="A1"."STATUS_CODE")
7 - access("A3"."STATUS_CODE"="A1"."STATUS_CODE")
filter("A2"."STATUS_CODE"="A3"."STATUS_CODE")
如果我们收集统计数据并告诉 Oracle STATUS table 的 真实 大小,情况就会大不相同。 100,000 次索引扫描是访问 table 中每一行的缓慢方法。相反,新计划散列将 STATUS table 连接在一起,然后散列将结果与 STATUS_DETAIL 连接起来。 运行 我电脑上的时间从 0.5 秒减少到 0.1 秒。
begin
dbms_stats.gather_table_stats(user, 'status_detail');
dbms_stats.gather_table_stats(user, 'status');
end;
/
explain plan for
select count(*)
from status_detail a1,
(
select distinct a2.status_code
from status a2
join status a3
on a2.status_code=a3.status_code
) v
where a1.status_code = v.status_code;
select * from table(dbms_xplan.display);
Plan hash value: 3579559806
---------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | | 556 (2)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | | | | |
| 2 | VIEW | VM_NWVW_1 | 10 | | | 556 (2)| 00:00:01 |
| 3 | HASH UNIQUE | | 10 | 190 | | 556 (2)| 00:00:01 |
|* 4 | HASH JOIN | | 100K| 1855K| 2056K| 552 (2)| 00:00:01 |
| 5 | TABLE ACCESS FULL | STATUS_DETAIL | 100K| 878K| | 69 (2)| 00:00:01 |
|* 6 | HASH JOIN SEMI | | 100K| 976K| 1664K| 277 (2)| 00:00:01 |
| 7 | INDEX FAST FULL SCAN| STATUS_IDX | 100K| 488K| | 57 (2)| 00:00:01 |
| 8 | INDEX FAST FULL SCAN| STATUS_IDX | 100K| 488K| | 57 (2)| 00:00:01 |
---------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("A1"."STATUS_CODE"="A2"."STATUS_CODE")
6 - access("A2"."STATUS_CODE"="A3"."STATUS_CODE")
这与我们在 table 和视图(包含 70 多万条记录)上的查询有关的性能问题有关。
在对不同环境中的执行计划进行广泛分析后,我可以将其指向其中一个连接的 VIEW PUSHED PREDICATE 分支。
The number of executions(starts column on execution plan) is equal to the number rows returned on the driving/outer table - may be it is evaluating the view for every match on the outer result set.
由于这里涉及的 table 有数百万条记录, %CPU 和整体执行时间变得非常糟糕。如果我添加一个不推送谓词的提示(no_push_pred),情况就不是这样了;处决只有 1 次。
Is this something expected with VIEW PUSHED PREDICATE or am I missing any concept around this?
Oracle 数据库版本:12c 企业版 12.1.0.2.0
我尝试使用简单的查询来模拟问题(或行为)- 请参阅下面的详细信息。
注意:这里添加了 no_merge 提示以确保优化器在连接期间不会合并视图,因此该计划与我的实际查询计划相同。
查询:
SELECT
v.STATUS_CODE,
a1.STATUS_DESC
FROM STATUS_DETAIL a1,
(select /*+ no_merge push_pred */
a2.STATUS_CODE
from STATUS a2
where a2.STATUS_CODE < 50) v
where a1.STATUS_CODE = v.STATUS_CODE;
执行计划(使用TABLE(DBMS_XPLAN.display_cursor)提取):
I am referring to Line no 3 & 4 from plan - Starts column has the value 70 ( equal to value of A-Rows column on Line no 2 - driving table access)
-----------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | | 213 (100)| | 22 |00:00:00.01 | 350 |
| 1 | NESTED LOOPS | | 1 | 13 | 533 | 213 (0)| 00:00:01 | 22 |00:00:00.01 | 350 |
| 2 | TABLE ACCESS FULL | STATUS_DETAIL | 1 | 70 | 1960 | 3 (0)| 00:00:01 | 70 |00:00:00.01 | 7 |
| 3 | VIEW PUSHED PREDICATE | | 70 | 1 | 13 | 3 (0)| 00:00:01 | 22 |00:00:00.01 | 343 |
|* 4 | FILTER | | 70 | | | | | 22 |00:00:00.01 | 343 |
|* 5 | TABLE ACCESS FULL | STATUS | 49 | 1 | 4 | 3 (0)| 00:00:01 | 22 |00:00:00.01 | 343 |
-----------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - filter("A1"."STATUS_CODE"<50)
5 - filter(("A2"."STATUS_CODE"="A1"."STATUS_CODE" AND "A2"."STATUS_CODE"<50))
你是对的,VIEW PUSHED PREDICATE
操作意味着"The view thus becomes correlated and must be evaluated for each row of the outer query block"。
这是一种特殊的谓词推送,join predicate pushdown 转换。该转换背后的想法是视图可能需要更频繁地执行,但是将连接谓词添加到视图可以使其 运行 快得多,因为该视图中的 table 现在可以使用索引访问。
连接谓词下推本身没有任何问题。类似于笛卡尔积,多次执行一个视图不一定是坏的。有时,大量快速的事物比少量缓慢的事物更好。
那么为什么 Oracle 在这里做出了错误的选择?没有更多的数据很难说。甲骨文正在做一些决定,使用大致如下的等式:
large number * small amount of time
<
small number * large amount of time
再详细一点:
rows returned by outer query * time for index-accessed view
<
1 (for a hash join) * read smaller table, create hash function, then read the other table and probe it for matches, potentially writing and reading to temporary tablespace
与大多数查询调优一样,检查基数。
也许 Oracle 明显低估了 "large number" 并认为外部 table 返回的行比实际情况要小得多。发生这种情况的原因有很多,比如糟糕的统计数据、使用优化器无法估计的大量令人困惑的函数、使用 Oracle 无法理解的关系的相关列(除非你创建多列直方图)等。
也许 Oracle 大大低估了 "small amount of time"。它可能认为视图的索引访问路径比实际要快得多。这可能是由于上述原因之一,也可能是因为有人弄乱了一些关键参数。不幸的是,人们认为这太普遍了,"indexes are fast, I should tell Oracle to use them more often by changing parameter defaults"。 运行 此查询并确保值为默认值 0 和 100。
select * from v$parameter where name in ('optimizer_index_cost_adj', 'optimizer_index_caching');
在实践中,优化器问题几乎总是由 Oracle 低估某些东西而不是高估造成的。所以我会关注等式的左边。 Oracle 总是试图丢弃尽可能多的行,并且总是寻找一种方法将基数降低到 1。如果真的有一条路径可以只从 table 中获取一行,那可以节省大量工作。但是,如果只有一种方法 看起来 像是通往一行的快速路径,但实际上并非如此,它可能会搞乱整个计划。
如果这是一个包含大量细节的巨大查询,放弃尝试查找根本原因并简单地使用提示或额外的 rownum
伪列来强制 Oracle 停止转换并不是没有道理的东西。
Oracle 提供了大量数据结构和算法来访问数据。这为优化器提供了很多方法来找到更快的 运行 查询方法。但这也给了它更多犯错的机会。没有风险就没有回报,但没有必要在每个查询上都赌一把。如果您有一个外部查询和一个内部查询,它们可以单独工作,但不能很好地协同工作,请务必将它们分开,不要让 Oracle 尝试以奇怪的方式组合它们。
下面是一个快速示例,使用的 tables 与您查询中的相似。它使用 VIEW PUSHED PREDICATE
操作错误地显示 Oracle。
首先,创建一些小的 tables,插入数据,并收集统计信息。到目前为止一切看起来都很好。
drop table status_detail;
drop table status;
create table status_detail(status_code number, status_desc varchar2(100));
insert into status_detail select level, level from dual connect by level <= 10;
create table status(status_code number);
create index status_idx on status(status_code);
insert into status select level from dual connect by level <= 100000;
begin
dbms_stats.gather_table_stats(user, 'status_detail');
dbms_stats.gather_table_stats(user, 'status');
end;
/
错误来了。假设有人在 STATUS_DETAIL table 中加载了 100,000 行,但忘记重新收集统计信息。 Oracle 认为外部 table 只有 10 行但实际上有 100,000。
insert into status_detail select 1, level from dual connect by level <= 100000;
commit;
alter system flush shared_pool;
运行 STATUS_DETAIL 和使用 STATUS 的内联视图之间的查询。为了防止视图合并,我在查询中添加了一个 join
和一个 distinct
以使其难以集成 A1 和 V.
explain plan for
select count(*)
from status_detail a1,
(
select distinct a2.status_code
from status a2
join status a3
on a2.status_code=a3.status_code
) v
where a1.status_code = v.status_code;
下面是错误的执行计划。 Oracle 认为 STATUS_DETAIL 只有 returns 10 行,因为优化器统计数据不佳。那个 STATUS table 很大,将它连接到自身会很昂贵。 Oracle 可以使用连接谓词下推,而不是连接大型 table。通过将 STATUS_CODE 谓词传递到视图中,现在它可以对大型 STATUS table 使用简单的 INDEX RANGE SCAN 操作。 10 个小索引范围扫描听起来比散列连接两个大 tables 更快。
select * from table(dbms_xplan.display);
Plan hash value: 3172146404
------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 5 | 23 (0)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | 5 | | |
| 2 | NESTED LOOPS SEMI | | 10 | 50 | 23 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL | STATUS_DETAIL | 10 | 30 | 3 (0)| 00:00:01 |
| 4 | VIEW PUSHED PREDICATE | | 1 | 2 | 2 (0)| 00:00:01 |
| 5 | NESTED LOOPS SEMI | | 1 | 10 | 2 (0)| 00:00:01 |
|* 6 | INDEX RANGE SCAN | STATUS_IDX | 1 | 5 | 1 (0)| 00:00:01 |
|* 7 | INDEX RANGE SCAN | STATUS_IDX | 1 | 5 | 1 (0)| 00:00:01 |
------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
6 - access("A2"."STATUS_CODE"="A1"."STATUS_CODE")
7 - access("A3"."STATUS_CODE"="A1"."STATUS_CODE")
filter("A2"."STATUS_CODE"="A3"."STATUS_CODE")
如果我们收集统计数据并告诉 Oracle STATUS table 的 真实 大小,情况就会大不相同。 100,000 次索引扫描是访问 table 中每一行的缓慢方法。相反,新计划散列将 STATUS table 连接在一起,然后散列将结果与 STATUS_DETAIL 连接起来。 运行 我电脑上的时间从 0.5 秒减少到 0.1 秒。
begin
dbms_stats.gather_table_stats(user, 'status_detail');
dbms_stats.gather_table_stats(user, 'status');
end;
/
explain plan for
select count(*)
from status_detail a1,
(
select distinct a2.status_code
from status a2
join status a3
on a2.status_code=a3.status_code
) v
where a1.status_code = v.status_code;
select * from table(dbms_xplan.display);
Plan hash value: 3579559806
---------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | | 556 (2)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | | | | |
| 2 | VIEW | VM_NWVW_1 | 10 | | | 556 (2)| 00:00:01 |
| 3 | HASH UNIQUE | | 10 | 190 | | 556 (2)| 00:00:01 |
|* 4 | HASH JOIN | | 100K| 1855K| 2056K| 552 (2)| 00:00:01 |
| 5 | TABLE ACCESS FULL | STATUS_DETAIL | 100K| 878K| | 69 (2)| 00:00:01 |
|* 6 | HASH JOIN SEMI | | 100K| 976K| 1664K| 277 (2)| 00:00:01 |
| 7 | INDEX FAST FULL SCAN| STATUS_IDX | 100K| 488K| | 57 (2)| 00:00:01 |
| 8 | INDEX FAST FULL SCAN| STATUS_IDX | 100K| 488K| | 57 (2)| 00:00:01 |
---------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("A1"."STATUS_CODE"="A2"."STATUS_CODE")
6 - access("A2"."STATUS_CODE"="A3"."STATUS_CODE")