我们利用如下 SQL 讲解(基于 Oracle11gR2 scott)。

    1. explain plan for select e.ename,e.job,d.dname from emp e,dept d
    2. where e.deptno=d.deptno and e.sal<2000;
    1. Explained.
    1. select * from table(dbms_xplan.display);
    1. PLAN_TABLE_OUTPUT
    2. ----------------------------------------------------------------------------
    3. Plan hash value: 615168685
    4. ---------------------------------------------------------------------------
    5. | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
    6. ---------------------------------------------------------------------------
    7. | 0 | SELECT STATEMENT | | 8 | 488 | 7 (15)| 00:00:01 |
    8. |* 1 | HASH JOIN | | 8 | 488 | 7 (15)| 00:00:01 |
    9. | 2 | TABLE ACCESS FULL| DEPT | 4 | 88 | 3 (0)| 00:00:01 |
    10. |* 3 | TABLE ACCESS FULL| EMP | 8 | 312 | 3 (0)| 00:00:01 |
    11. ---------------------------------------------------------------------------
    12. Predicate Information (identified by operation id):
    13. ---------------------------------------------------
    14. 1 - access("E"."DEPTNO"="D"."DEPTNO")
    15. 3 - filter("E"."SAL"<2000)
    16. Note
    17. -----
    18. - dynamic sampling used for this statement (level=2)

    执行计划分为两部分,Plan hash value 和 Predicate Information 之间这部分主要是表的访问路径以及表的连接方式。关于访问路径以及表连接方式会在之后章节详细解释。另外一部分是谓词过滤信息,这部分信息位于 Predicate Information 下面,谓词过滤信息非常重要。一些老 DBA 因为之前接触的是 Oracle8i 或者 Oracle9i,那个时候执行计划还没有谓词信息,所以就遗留了一个传统,看执行计划只看访问路径和表连接方式了,而不关心谓词过滤信息。还有些人做 SQL 优化喜欢用 10 046 trace 或者 10 053 trace,如果仅仅是优化一个 SQL,根本就不需要使用上面两个工具,直接分析 SQL 语句以及执行计划即可。当然,如果是为了深入研究为什么不走索引,为什么走了嵌套循环而没走 HASH 连接等,这个时候我们可以用 10 053 trace;如果想研究访问路径是单块读或者是多块读,可以使用 10 046 trace。

    我们这里先不讲怎么阅读执行计划,后面会讲利用光标移动大法阅读执行计划。

    注意观察 Id 这列,有些 Id 前面有”“号,这表示发生了谓词过滤,或者发生了 HASH 连接,或者是走了索引。Id=1前面有”“号,它是 HASH 连接的”“号,我们观察对应的谓词过滤信息就能知道是哪两个表进行的 HASH 连接,而且能知道是对哪些列进行的 HASH 连接,这里是 e 表(emp 表的别名)的 deptno 列与 d 表(dept 的别名)deptno 列进行 HASH 连接的。Id=3前面有”“号,这里表示表 emp 有谓词过滤,它的过滤条件就是Id=3对应的谓词过滤信息,也就是e.sal<2000Id=2前面没有「*」号,那么说明 dept 表没有谓词过滤条件。

    提问:TABLE ACCESS FULL 前面没有「*」号怎么办?

    回答:如果表很小,那么不需理会,小表不会产生性能问题。如果表很大,那么我们要询问开发人员是不是忘了写过滤条件,当然了一般也不会遇到这种情况。如果真的是没过滤条件呢?比如一个表有 10GB,但是没有过滤条件,那么它就会成为整个 SQL 的性能瓶颈。这个时候我们需要查看 SQL 语句中该表访问了多少列,如果访问的列不多,就可以把这些列组合起来,建立一个组合索引,索引的大小可能就只有 1GB 左右。我们利用 INDEX FAST FULL SCAN 代替 TABLE ACCESS FULL。在访问列不多的情况,索引的大小(Segment Size)肯定比表的大小(Segment Size)小,那么就不需要扫描 10GB 了,只需要扫描 1GB,从而达到优化目的。如果 SQL 语句里面要访问表中大部分列,这时就不应该建立组合索引了,因为此时索引大小比表更大,可以通过其他方法优化,比如开启并行查询,或者更改表连接方式,让大表作为嵌套循环的被驱动表,同时在大表的连接列上建立索引。关于表连接方式,我们会在后面章节详细介绍。

    提问:TABLE ACCESS FULL 前面有「*」号怎么办?

    回答:如果表很小,那么我们不需理会;如果表很大,可以使用select count(*)from 表,查看有多少行数据,然后通过select count(*) from 表 where *对应的谓词过滤条件,查看返回多少行数据。如果返回的行数在表总行数的 5% 以内,我们可以在过滤列上建立索引。如果已经存在索引,但是没走索引,这时我们要检查统计信息,特别是直方图信息。如果统计信息已经收集过了,我们可以用 HINT 强制走索引。如果有多个谓词过滤条件,我们需要建立组合索引并且要将选择性高的列放在前面,选择性低的列在后面。如果返回的行数超过表总行数的 5%,这个时候我们要查看 SQL 语句中该表访问了多少列,如果访问的列少,同样可以把这些列组合起来,建立组合索引,建立组合索引的时候,谓词过滤列在前面,连接列在中间,select 部分的列在最后。如果访问的列多,这个时候就只能走全表扫描了。

    提问:TABLE ACCESS BY INDEX ROWID 前面有「*」号怎么办?

    回答:我们利用如下 SQL 讲解(基于 Oracle11gR2 scott)。

    1. grant dba to scott;

    授权成功。

    1. create table test as select * from dba_objects;

    表已创建。

    1. create index idx_name on test(object_name);

    索引已创建。

    1. set autot trace
    2. select /*+ index(test) */ * from test where object_name like 'V_$%'
    3. and owner='SCOTT';
    1. 未选定行
    2. 执行计划
    3. ----------------------------------------------------------
    4. Plan hash value: 461797767
    5. -------------------------------------------------------------------------------------
    6. | Id | Operation | Name | Rows | Bytes | Cost(%CPU)| Time |
    7. -------------------------------------------------------------------------------------
    8. | 0 | SELECT STATEMENT | | 38 | 7866 | 334 (0)| 00:00:05 |
    9. |* 1 | TABLE ACCESS BY INDEX ROWID| TEST | 38 | 7866 | 334 (0)| 00:00:05 |
    10. |* 2 | INDEX RANGE SCAN | IDX_NAME | 672 | | 6 (0)| 00:00:01 |
    11. -------------------------------------------------------------------------------------
    12. Predicate Information identified by operation id):
    13. ---------------------------------------------------
    14. 1 - filter(「OWNER」='SCOTT』)
    15. 2 - access(「OBJECT_NAME」 LIKE 『V_$%』)
    16. filter(「OBJECT_NAME」 LIKE 『V_$%』)
    17. Note
    18. -----
    19. - dynamic sampling used for this statement (level=2)

    统计信息

    1. ----------------------------------------------------------
    2. 0 recursive calls
    3. 0 db block gets
    4. <strong> 332 consistent gets</strong>
    5. 0 physical reads
    6. 0 redo size
    7. 1191 bytes sent via SQL*Net to client
    8. 409 bytes received via SQL*Net from client
    9. 1 SQL*Net roundtrips to/from client
    10. 0 sorts (memory)
    11. 0 sorts (disk)
    12. 0 rows processed

    TABLE ACCESS BY INDEX ROWID 前面有”*”号,表示回表再过滤。回表再过滤说明数据没有在索引中过滤干净。当 TABLE ACCESS BY INDEX ROWID 前面有”“号时,**可以将「」号下面的过滤条件包含在索引中**,这样可以减少回表次数,提升查询性能。

    1. create index idx_ownername on testowner,object_name);
    1. 索引已创建
    1. select /*+ index(test) */ * from test where object_name like 'V_$%'
    2. and owner='SCOTT';
    1. 未选定行
    2. 执行计划
    3. ----------------------------------------------------------
    4. Plan hash value: 3756723214
    5. -------------------------------------------------------------------------------------
    6. | Id |Operation |Name |Rows | Bytes | Cost(%CPU)| Time |
    7. -------------------------------------------------------------------------------------
    8. | 0 |SELECT STATEMENT | | 38| 7866 | 5 (0)| 00:00:01 |
    9. | 1 | TABLE ACCESS BY INDEX ROWID|TEST | 38| 7866 | 5 (0)| 00:00:01 |
    10. |* 2 | INDEX RANGE SCAN |IDX_OWNERNAME| 3| | 3 (0)| 00:00:01 |
    11. -------------------------------------------------------------------------------------
    12. Predicate Information identified by operation id):
    13. ---------------------------------------------------
    14. 2 - access(「OWNER」='SCOTT』 AND 「OBJECT_NAME」 LIKE 『V_$%』)
    15. filter(「OBJECT_NAME」 LIKE 『V_$%』)
    16. Note
    17. -----
    18. - dynamic sampling used for this statement (level=2)
    19. 统计信息
    20. ----------------------------------------------------------
    21. 0 recursive calls
    22. 0 db block gets
    23. <strong> 3 consistent gets</strong>
    24. 0 physical reads
    25. 0 redo size
    26. 1191 bytes sent via SQL*Net to client
    27. 409 bytes received via SQL*Net from client
    28. 1 SQL*Net roundtrips to/from client
    29. 0 sorts (memory)
    30. 0 sorts (disk)
    31. 0 rows processed

    如果索引返回的数据本身很少,即使 TABLE ACCESS BY INDEX ROWID 前面有「*」号,也可以不用理会,因为索引本身返回的数据少,回表也没有多少次,因此可以不用再创建组合索引。

    通过上面的讲解,相信大家也明白了为什么我们不推荐使用工具查看执行计划,因为有些工具看不到「*」号,看不到谓词过滤信息。