在这系列的前两个部分中,我们列举了一些生成或者找回执行计划的方法,也学习了一些额外的步骤可以帮助我们模拟真实的环境来判断执行计划是否存在问题。 这部分我们会熟悉整体解释执行计划的基本准则。 在第四部分前,我们不用考虑使用谓词的部分。
The Shape of a plan
我们会从一个简单的例子开始 – 创建几张表,连接它们,在我们检查执行计划的时候需要回答提出的问题。这里是数据生成的脚本:
create table t1 as with generator as ( select --+ materialize rownum id from dual connect by level <= 1e4 ) select rownum id, mod(rownum,1000) n_1000, lpad(rownum,6,'0') v1, rpad('x',100,'x') padding from generator ; alter table t1 add constraint t1_pk primary key(id); create index t1_i1 on t1(n_1000); begin dbms_stats.gather_table_stats( ownname => user, tabname =>'T1', method_opt => 'for all columns size 1' ); end; /
我按照同样的脚本新建了一张 t2 表,这里就不重复了。接下来我会使用 explain plan 解释一条简单的 SQL 语句来介绍执行计划中的要点。
explain plan for select t1.v1,t2.v1 from t1,t2 where t1.n_1000= 1 andt2.id = t1.id andt2.n_1000= 100 ; select * from table(dbms_xplan.display); select id,parent_id, position, depth,level ? 1 old_depth, rpad('',level - 1) || operation|| ' ' || lower(options)|| ' ' || object_name text_line from plan_table start with id= 0 connect by parent_id= prior id order siblings by id,position ;
我用两种不同的方法生成执行计划 — 一种是对 dbms_xplan.display() 的调用,另一种是在 9i 或者更早版本种对于 plan table 的简化查询。
以下是两种方法生成的结果 :
-------------------------------------------------------------------------------------- | 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 byoperation id): --------------------------------------------------- 1- access("T2"."ID"="T1"."ID") 3- access("T1"."N_1000"=1) 5- access("T2"."N_1000"=100) Id Par Pos DEPTH OLD_DEPTH TEXT_LINE ---- ---- ---- ----- ----------------------------------------------------------- 0 22 0 0 SELECT STATEMENT 1 0 1 1 1 HASH JOIN 2 1 1 2 2 TABLE ACCESS by index rowid T1 3 2 1 3 3 INDEX range scan T1_I1 4 1 2 2 2 TABLE ACCESS by index rowid T2 5 4 1 3 3 INDEX range scan T2_I1
我之所以列出较早版本的获取执行计划的方法,是因为这样可以让你将调用dbms_xplan.display 的结果的直观展示与plan table 中的被隐藏的细节相关联。我们总是看到执行计划都是呈锯齿状,这种方式是为了告诉我们执行计划中操作的关系。也直观的展示id ,parent_id 和position 列之间的关系。
计划中每个操作都有 id ,这个实际上告诉我们每行输出的顺序。每行都有可能是 1 个或多个子操作的父操作,每行的 parent_id 列都会指向这个 id 的父 id 。这里例子中我们看 Id 为 2 跟 4 的行,都是 Id 为 1 的子操作。如果一行有多个子操作, position 列就会列出子操作的顺序,我们看到 Id 为 2 的行的 position 是 1 ,代表他是 Id 为 1 的行下第 1 个子操作, Id 为 4 则是第 2 个子操作。在 dbms_xplan 生成的执行计划中我们看不到 parent_id 和 position 列,我们需要在很多操作中根据顺序和锯齿的形状来判断父 / 子关系。
根据id 顺序列出的行总是能给我们正确计划的顺序( 但是这并不是获取跟操作数据的顺序); 而且计算在锯齿状中的行的方法在9i 跟10g 中也不一样。以前的版本中,来自分级connect by 查询的派生列级别允许我们向文本添加适当大小的缩进— 但是当Oracle 允许我们从内存中获取执行计划的版本(v$sql_plan) 时,这种方法就变得十分低效,因此,动态性能视图中包含了一个预先计算的level 列( 与level 相差1 ,称为depth) ,其值在语句被优化时获得,并最终将其添加到plan 表中( 即使在12c 中也有一些生成的值不正确的情况,所以记住怎么根据connect by 查询获得执行计划是很有帮助的)。
First Rule for Reading Plans
这篇文章我们不涉及谓词部分,即使它们对于执行计划来说也很重要,我们重点在怎么遍历执行计划主体,并了解 Oracle 获取和操作数据的顺序。
计划中的每一行都代表一个生成一些 “rowsource”( 行源 ) 的行操作。这里的行也可能只是从索引中获取到的 rowid 。一个操作需要几个动作生成一个 rowsource ,之后将 rowsource 传递给父操作。如果一个父操作有多个子操作,它会依次调用每个子操作,并通过一些工作将它们提供的 rowsource 进行合并。你需要学习的最重要的一点是每个操作做了什么,以及该操作 “ 合并 “rowsources 意味着什么。还有一个稍微复杂的问题是,尽管父操作“依次”调用其子操作,但它可能会多次调用每个子进程,而且重复调用的方式会随着父进程的操作而变化。
这篇文章不可能包含所有的变化和异常情况,但是如果我们暂时忽略特殊情况,那么阅读执行计划的基本方法总结为 :” 子操作按先后顺序执行,递归向下 “ 。我们通过一个 hash join 的执行计划来看看是如何工作的。
第 0 行告诉我们这是一个 select 语句。我们需要定义它的子集和操作顺序来为这个 select 语句生成 rowsource 。在用 SQL 语句输出的执行计划中可以看到,第 1 行是第一个也是唯一的属于第 0 行的子行。如果我们没有 parent_id 和 position 列帮助我们判断,我们也可以用直观的方法,一个操作的第一个子操作都是在下一行 ( 它会缩进一格 ) ,之后通过该行画条垂直的线,直到执行计划底部,有相同缩进的行就是这个操作的子操作,并且行的顺序就是子操作的顺序。
通过第 1 行我们知道这是个 hash join 操作,这里我们可能需要去查看手册来了解什么是 hash join 以及它是如何工作的。从 SQL 生成的执行计划中可以很方便的看到第 2 行和第 4 行是 hash join 的子项,从直观展示中也能看到,第 2 行是一个子操作,之后垂直对齐到第 4 行的 “table access” 的 T 字母,说明它是第二个子操作。这些信息足够告诉我们,将要在内存里根据 t1( 第一个子操作 ) 中的一些行建立 hash 表,并根据 t2( 第 2 个子操作 ) 中的行对 hash 表进行探测,如果符合条件,则构造一个结果集,并返回给在第 0 行 ( 第 1 行的父行 ) 。这一系列的 hash join 操作是一个很好的例子,告诉我们为什么需要考虑子操作的顺序,这些物理上的操作顺序会告诉我们哪些表是作为 build 表 ( 即需要在内存中建立的表 ) ,哪些是作为 probe 表 ( 即探测表,不需要放到内存里 ) 。
此刻我们还不知道如何判断 t1 , t2 中所需要的行,我们所做的就是将计划的最顶层的一部分作为开始,了解执行计划整体的工作。我们还不能说 : “这是 Oracle 获得的第一个数据集 / 这是 Oracle 访问的第一个表”。但是我们可以通过重复我们目前所采用的方法来达到这一点。
我们将用第 2 行中的 rowsource 构建一个内存中的 hash 表,然后用第 4 行中的 rowsource 探测 hash 表 ; 我们先来看看第一个子项。从第 2 行开始,我们可以确定整个“子计划”,其中 rowsource 正是我们需要的最终结果:
-------------------------------------------------------------------------------------- | Id | Operation |Name | Rows | Bytes | Cost (%CPU)| Time | -------------------------------------------------------------------------------------- | 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 | --------------------------------------------------------------------------------------
从上面分离出来部分的执行计划可以看到,第 2 行有个单独的子操作进行调用,这个子操作是 index range scan 。我们可以很容易的看到,通过 index range scan 生成的 rowsource 可能包含 rowid ,而且第 2 行根据 rowid 进行表扫描 — 当试图理解更复杂的执行计划时,类似这样的完整性检查 ( 将父级的需求与子级的提供的数据进行比较 ) 非常有用。
同样的我们也可以看看第 4 行的子计划:
-------------------------------------------------------------------------------------- | Id | Operation |Name | Rows | Bytes | Cost (%CPU)| Time | -------------------------------------------------------------------------------------- | 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 | --------------------------------------------------------------------------------------
同样这也是一个很简单的计划,我们调用第 5 行的操作进行 index range scan ,并且在第 4 行中根据第 5 行提供的 rowid 进行访问表。
将所有的片段整合一起,我们可以将执行步骤标识出来 :
------------------------------------------------------ | Id | Operation |Name | Order | ------------------------------------------------------ | 0| SELECT STATEMENT | | 6 | |* 1| HASH JOIN | | 5 | | 2| TABLE ACCESS BY INDEX ROWID| T1 | 2 | |* 3| INDEX RANGE SCAN | T1_I1 | 1 | | 4| TABLE ACCESS BY INDEX ROWID| T2 | 4 | |* 5| INDEX RANGE SCAN | T2_I1 | 3 | ------------------------------------------------------
我们根据如下顺序解读执行计划:
第 0 行调用第 1 行 ( 第 1 个子操作 )。
第 1 行调用第 2 行 ( 第 1 个子操作 )。
第 2 行调用第 3 行 ( 第 1 个子操作 )。
第 3 行通过 index range scan 生成一些 rowsource ,并将其传递到第 2 行。
第 2 行根据 rowid 访问 t1 表并生成 rowsource ,并传递到第 1 行。
第 1 行用这些行源建立内存里的 hash 表,之后调用第 4 行 ( 第 2 个子操作 ) 开始提供用作探测表的 rowsource。
第 4 行调用第 5 行 ( 第 1 个子操作 )。
第 5 行根据 index range scan 生成 rowsource ,并传递给第 4 行。
第 4 行根据 rowid 访问 t2 表并生成 rowsource ,传递给第 1 行。
第 1 行探测 hash 表,并找出符合条件的行,根据结果集生成第 5 个 rowsource ,之后传递给第 0 行,这也就是传递给客户端程序的结果。
计划中还有比这更多的内容——特别是我们需要更多地考虑操作的时机: 有些是“批量”处理,有些是“单行”处理; 我们需要引入谓词部分并考虑“访问”和“筛选器”的相关性; 这些细节我们将在下一篇文章中看到。
Closing thoughts
我想强调的是,将复杂的执行计划分解成简单的部分是非常方便的。我们例子中的执行计划很短,所以分块处理的好处并不是特别明显,但是想想我们是如何从整体看执行计划,并选出最前两行,之后查看一些子计划。我们可以在任何计划中使用这个方法,不管它有多复杂,并单独检查计划的小部分。
原文链接 :
https://www.red-gate.com/simple-talk/sql/oracle/execution-plans-part-3-the-rule/
原文作者 : Jonathan Lewis
| 译者简介
林锦森·沃趣科技数据库技术专家
沃趣科技数据库工程师,多年从事Oracle数据库,较丰富的故障处理、性能调优、数据迁移及备份恢复经验