谓词推入(Pushing Predicate):当 SQL 语句中包含不能合并的视图,同时视图有谓词过滤(也就是 where 过滤条件),CBO 会将谓词过滤条件推入视图中,这个过程就叫作谓词推入。谓词推入的主要目的就是让 Oracle 尽可能早地过滤掉无用的数据,从而提升查询性能。
为什么谓词推入必须要有不能被合并的视图呢?因为一旦视图被合并了,执行计划中根本找不到视图,这个时候谓词往哪里推呢?所以谓词推入的必要前提是 SQL 中要有不能合并的视图。
我们先创建一个不能被合并的视图(视图中有 union all)。
create or replace view v_pushpredicate as
select * from test
union all
select * from test where rownum>=1;
View created.
然后我们运行下面的 SQL,同时查看执行计划。
select * from v_pushpredicate where object_id<10;
16 rows selected.
Execution Plan
----------------------------------------------------------
Plan hash value: 669161224
-------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 72470 | 14M| 238 (1)|
|* 1 | VIEW | V_PUSHPREDICATE | 72470 | 14M| 238 (1)|
| 2 | UNION-ALL | | | | |
| 3 | TABLE ACCESS BY INDEX ROWID| TEST | 8 | 776 | 3 (0)|
|* 4 | INDEX RANGE SCAN | IDX_ID | 8 | | 2 (0)|
| 5 | COUNT | | | | |
|* 6 | FILTER | | | | |
| 7 | TABLE ACCESS FULL | TEST | 72462 | 6864K| 235 (1)|
-------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("OBJECT_ID"<10)
4 - access("OBJECT_ID"<10)
6 - filter(ROWNUM>=1)
SQL 语句中,where 过滤条件是针对视图过滤的,但是从执行计划中(Id=4)我们可以看到,where 过滤条件跑到视图中的表中进行过滤了,这就是谓词推入。因为视图中第二个表有 rownum,rownum 会阻止谓词推入,所以第二个表走的是全表扫描,需要到视图上进行过滤(Id=1)。
我们在看执行计划的时候,如果 VIEW 前面有「*」号,这就说明有谓词没有推入到视图中。
一般情况下,常量的谓词推入对性能的提升都是有益的。那么什么是常量的谓词推入呢?常量的谓词推入就是谓词是正常的过滤条件,而非连接列。
常量谓词推入优化例子参考博客:http://blog.csdn.net/robinson1988/article/details/6613851。
还有一种谓词推入,是把连接列当作谓词推入到视图中,这种谓词推入我们一般叫作连接列谓词推入,此类谓词推入最容易产生性能问题。
现在我们将上面视图中的 rownum 去掉(为了使连接列能推入视图)。
create or replace view v_pushpredicate as
select * from test
union all
select * from test;
View created.
我们添加 HINT:push_pred 提示将连接列推入到视图中。
select /*+ push_pred(b) */ *
from test a, v_pushpredicate b
where a.object_id = b.object_id
and a.owner = 'SCOTT';
14 rows selected.
Execution Plan
----------------------------------------------------------
Plan hash value: 2131469559
-------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost(%CPU)|
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 4997 | 1444K| 10073 (1)|
| 1 | NESTED LOOPS | | 4997 | 1444K| 10073 (1)|
| 2 | TABLE ACCESS BY INDEX ROWID | TEST | 2499 | 236K| 73 (0)|
|* 3 | INDEX RANGE SCAN | IDX_OWNER | 2499 | | 6 (0)|
| 4 | VIEW | V_PUSHPREDICATE | 1 | 199 | 4 (0)|
| 5 | UNION ALL PUSHED PREDICATE | | | | |
| 6 | TABLE ACCESS BY INDEX ROWID| TEST | 1 | 97 | 2 (0)|
|* 7 | INDEX RANGE SCAN | IDX_ID | 1 | | 1 (0)|
| 8 | TABLE ACCESS BY INDEX ROWID| TEST | 1 | 97 | 2 (0)|
|* 9 | INDEX RANGE SCAN | IDX_ID | 1 | | 1 (0)|
-------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access("A"."OWNER"='SCOTT')
7 - access("OBJECT_ID"="A"."OBJECT_ID")
9 - access("OBJECT_ID"="A"."OBJECT_ID")
将连接列推入到视图中这种谓词推入,一般在执行计划中都能看到 PUSHED PREDICATE 或者 VIEW PUSHED PREDICATE,而且视图一般作为嵌套循环的被驱动表,同时视图中谓词被推入列有索引。这种谓词推入对性能有好有坏。为什么连接列谓词推入,被推入的视图一般都作为嵌套循环的被驱动表呢?这是因为连接列谓词推入需要传值(传值到视图里面),而有传值操作的表连接方法只有嵌套循环或者 FILTER。FILTER 是专门针对半连接或者反连接的(where 后面的子查询),谓词推入是专门针对 from 后面的子查询,所以连接列谓词推入,被推入的视图一般都作为嵌套循环的被驱动表。
在本书示例中,连接列谓词推入的执行计划是最优执行计划。驱动表 test 过滤之后(owner=’SCOTT’)只返回 7 行数据,然后通过连接列传值 7 次,传入视图中,视图里面的表走的是索引扫描,因为驱动表 7 次传值,所以被驱动表(视图)一共被扫描了 7 次,但是每次扫描都是索引扫描。
现在我们去掉 HINT:push_pred
。
select *
from test a, v_pushpredicate b
where a.object_id = b.object_id
and a.owner = 'SCOTT';
14 rows selected.
Execution Plan
----------------------------------------------------------
Plan hash value: 1745523384
-------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 4997 | 1483K| 544 (1)|
|* 1 | HASH JOIN | | 4997 | 1483K| 544 (1)|
| 2 | TABLE ACCESS BY INDEX ROWID| TEST | 2499 | 236K| 73 (0)|
|* 3 | INDEX RANGE SCAN | IDX_OWNER | 2499 | | 6 (0)|
| 4 | VIEW | V_PUSHPREDICATE | 144K| 28M| 470 (1)|
| 5 | UNION-ALL | | | | |
| 6 | TABLE ACCESS FULL | TEST | 72462 | 6864K| 235 (1)|
| 7 | TABLE ACCESS FULL | TEST | 72462 | 6864K| 235 (1)|
-------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - access("A"."OBJECT_ID"="B"."OBJECT_ID")
3 - access("A"."OWNER"='SCOTT')
在本书示例中,我们如果不将连接列推入到视图中,视图里面的表就只能全表扫描,这时性能远不如索引扫描,所以本书示例最佳执行计划就是连接列谓词推入的执行计划。
笔者经常遇到连接列谓词推入引起 SQL 性能问题。大家在工作中,如果遇到执行计划中 VIEW PUSHED PREDICATE 一定要注意,如果 SQL 执行很快,不用理会;如果 SQL 执行很慢,可以先关闭连接列谓词推入(**alter session set '_push_join_predicate' = false**
)功能,再逐步分析为什么连接列谓词推入之后,SQL 性能很差。连接列谓词推入性能变差一般是 CBO 将驱动表 Rows 计算错误(算少),导致视图作为嵌套循环被驱动表,然后一直反复被扫描;也有可能是视图太过复杂,视图本身存在性能问题,这时需要单独优化视图。例如视图单独执行耗时 1 秒,在进行谓词推入之后,视图会被扫描多次,假设扫描 1 000 次,每次执行时间从 1 秒提升到了 0.5 秒,但是视图被执行了 1 000 次,总的耗时反而多了,这时谓词推入反而降低性能。
一定要注意,当视图中有 rownum 会导致无法谓词推入,所以一般情况下,我们不建议在视图中使用 rownum。为什么 rownum 会导致无法谓词推入呢?这是因为当谓词推入之后,rownum 的值已经发生改变,已经改变了 SQL 结果集,任何查询变换必须是在不改变 SQL 结果集的前提下才能进行。