
这个系列的开头,我就强调过规则不是适用于所有的场景,下一部分(第5部分)里我们会来看几个我们需要特别注意的场景。但是在这部分中,我们将继续使用更简单的例子,来了解应用规则时谓词的选择时机和使用的一些细节。
Basics
--------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 300 | 22 (0)| 00:00:01 |
|* 1 | HASH JOIN | | 10 | 300 | 22 (0)| 00:00:01 |
| 2 | TABLE ACCESS BY INDEX ROWID| T1 | 10 | 150 | 11 (0)| 00:00:01 |
|* 3 | INDEX RANGE SCAN | T1_I1 | 10 | | 1 (0)| 00:00:01 |
| 4 | TABLE ACCESS BY INDEX ROWID| T2 | 10 | 150 | 11 (0)| 00:00:01 |
|* 5 | INDEX RANGE SCAN | T2_I1 | 10 | | 1 (0)| 00:00:01 |
--------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - access("T2"."ID"="T1"."ID")
3 - access("T1"."N_1000"=1)
5 - access("T2"."N_1000"=100)
通过"子操作按先后顺序执行"这个规则,我们可以知道运行引擎会从第3行的范围扫描索引t1_i1开始,之后第2行根据第3行获取到的rowid从t1表中选择行列,之后第1行根据第2行中的数据在内存里建立hash表;然后是第5行的范围扫描索引t1_i2,第4行根据第5行获取到的rowid从表t2中选择行列,最后在第1行中根据T2表中的行列去探测内存中的hash表,如果有匹配项则建立一个新的含有结果集的rowsource,最后传递给客户端程序。
---------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 300 | 24 (9)| 00:00:01 |
| 1 | MERGE JOIN | | 10 | 300 | 24 (9)| 00:00:01 |
| 2 | SORT JOIN | | 10 | 150 | 12 (9)| 00:00:01 |
| 3 | TABLE ACCESS BY INDEX ROWID| T1 | 10 | 150 | 11 (0)| 00:00:01 |
|* 4 | INDEX RANGE SCAN | T1_I1 | 10 | | 1 (0)| 00:00:01 |
|* 5 | SORT JOIN | | 10 | 150 | 12 (9)| 00:00:01 |
| 6 | TABLE ACCESS BY INDEX ROWID| T2 | 10 | 150 | 11 (0)| 00:00:01 |
|* 7 | INDEX RANGE SCAN | T2_I1 | 10 | | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("T1"."N_1000"=1)
5 - access("T2"."ID"="T1"."ID")
filter("T2"."ID"="T1"."ID")
7 - access("T2"."N_1000"=100)
这个计划里我们可以看到第1行的merge join操作有两个子操作,分别是第2行的sort join(第1个子操作)和第5行的sort join(第2个子操作)。运用"子操作按先后顺序执行"的规则,我们可以知道Oracle从范围扫描索引t1_i1开始,从t1中获取可能需要的数据并且在第2行中对它们进行排序(对id列进行排序,因为它们是连接列)。如果运气好的话,第1行中排序后的数据集会在内存中(在会话的PGA中):第一个子操作是一个阻塞操作,所以在排序完成前我们不能调用第二个子操作。
alter session set statistics_level = all;
set linesize 156
set trimspool on
set pagesize 60
set serveroutput off
select
/*+
leading(t1, t2)
use_merge(t2)
*/
t1.v1, t2.v1
from
t1, t2
where
t1.n_1000 = 1
and t2.id = t1.id
and t2.n_1000 = 100
;
select * from table(dbms_xplan.display_cursor(null,null,'iostats last'));
--------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 0 |
| 1 | MERGE JOIN | | 1 | 10 | 0 |
| 2 | SORT JOIN | | 1 | 10 | 10 |
| 3 | TABLE ACCESS BY INDEX ROWID| T1 | 1 | 10 | 10 |
|* 4 | INDEX RANGE SCAN | T1_I1 | 1 | 10 | 10 |
|* 5 | SORT JOIN | | 10 | 10 | 0 |
| 6 | TABLE ACCESS BY INDEX ROWID| T2 | 1 | 10 | 10 |
|* 7 | INDEX RANGE SCAN | T2_I1 | 1 | 10 | 10 |
--------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("T1"."N_1000"=1)
5 - access("T2"."ID"="T1"."ID")
filter("T2"."ID"="T1"."ID")
7 - access("T2"."N_1000"=100)输出中我们应该关注Starts列,第5行的starts列的值可能是会引起问题的一个小细节(第二个排序操作)。显然Oracle把第二个表中的数据排序了10次,但由于Oracle的开发人员是相当聪明的,所以我们有理由认为这不是真实发生的,我们需要对sort join操作有更好的解释,并更清楚的理解它是如何在执行计划中展示的。
第二个sort join操作其实包含两部分,一部分根据提供的值探测排序好的数据集,另一部分确实是对数据集进行排序。可能这个说明更适合这个操作,'探测内存中已经排序好的数据集,但如果数据集不在内存中,则获取并排序它'。执行计划中的行可能包含类似"如果满足条件X,则执行A,否则执行B"的高级逻辑,sort join具备做或者不做的能力,排序就是一个这样的例子。
--------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 0 |
| 1 | MERGE JOIN | | 1 | 10 | 0 |
|* 2 | TABLE ACCESS BY INDEX ROWID | T1 | 1 | 10 | 10 |
| 3 | INDEX FULL SCAN | T1_PK | 1 | 10000 | 10000 |
|* 4 | SORT JOIN | | 10 | 10 | 0 |
|* 5 | TABLE ACCESS BY INDEX ROWID| T2 | 1 | 10 | 10 |
| 6 | INDEX FULL SCAN | T2_PK | 1 | 10000 | 10000 |
--------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter("T1"."N_1000"=1)
4 - access("T2"."ID"="T1"."ID")
filter("T2"."ID"="T1"."ID")
5 - filter("T2"."N_1000"=100)
例子中"PK"索引是基于id列,表示第3行和第6行中第index full scan操作访问数据的顺序正是我们所需要的排序的顺序,这也避免了排序操作。我们从计划中可以看到,Oracle并没有对第一个数据集进行排序,只是简单的按照顺序读取行,之后在第2行中使用filter谓词过滤了所有不需要的行。
比较第3行中A-rows和第2行中A-rows的值,我们可以看到先生成了包含10000行的rowsource,之后在根据filter谓词舍弃掉了大部分只留下10行数据,这显然是一个十分低效的行为。同样低效的行为也发生在第5行和第6行中,我们执行index full scan,返回10000个rowid,通过rowid访问表后根据filter舍弃了9990行。然而,关于表t2的处理最奇怪的事情是,我们在第4行中对产生的rowsource进行排序——尽管我们应该知道它已经按照merge join的正确排序顺序排序了。对于这种明显的冗余排序的解释是,它是一种将数据从缓冲区缓存中取出并放入私有工作区域的简便方法,这并不是为了将数据按正确的顺序重新排列。





