查询执行计划 -- 生成和显示执行计划

参考文档:《Oracle® Database SQL Tuning Guide》

1,执行计划简介

        Oracle数据库用于执行语句的步骤组合为执行计划。这些步骤要么从数据库中物理地检索数据行,要么为执行用户准备数据行。执行计划包含了SQL访问表时的访问路径,以及使用适当的联接方法对表进行排序(联接顺序)。

2,执行计划的生成和显示

        EXPLAIN PLAN语句显示优化器为SELECT、UPDATE、INSERT、DELETE语句选择的执行计划。

2.1,执行计划的说明

        一条语句的执行计划是数据库为运行语句而执行的操作序列。行源树是执行计划的核心。该树显示了以下信息:

- 语句引用的表的顺序
- 语句中涉及到的每个表的访问方法              
- 语句中受联接操作影响的表的联接方法              
- 数据操作,如筛选、排序或聚合 

        除了行源树外,plan表还包含以下信息:

- 优化,例如每个操作的成本和基数             
- 分区,例如访问的分区集            
- 并行执行,例如连接输入的分布方法

        可以使用EXPLAIN PLAN结果来确定优化器是否选择了特定的执行计划,例如嵌套循环连接。这些结果可以帮助理解优化器的决策,比如为什么优化器选择嵌套循环连接而不是散列连接。

2.2,执行计划改变的原因

        执行计划可以随着底层优化器输入的更改而更改。EXPLAIN PLAN显示的是在解释语句时数据库将如何运行SQL语句。由于执行环境和explain plan环境的不同,此计划可能与SQL语句实际使用的执行计划不同。

        注:为了避免执行计划更改可能导致的SQL性能退化,可以考虑使用SQL计划管理。

2.2.1,schema不同

        造成schema不同的原因主要有以下几种:

- 执行和解析计划发生在不同的数据库上。
- 解析语句的用户与运行语句的用户不同。
两个用户可能指向同一个数据库中的不同对象,从而导致不同的执行计划。
- 两个操作之间的schema更改(通常是索引的更改)。

2.2.2,costs不同

        即使schema相同,优化器也可以在成本不同时选择不同的执行计划。影响成本的因素包括:

- 数据量和统计信息
- 绑定变量类型和值
- 初始化参数在全局或会话级别的设置

2.3,数据行的丢弃

        通过执行计划可以看到数据行丢弃的现象。数据库在如下情况下会丢弃数据行:

- 全扫描
- 非选择性范围扫描
- 后期谓词过滤
- 连接顺序错误
- 后期过滤操作

        在示例6-1所示的计划中,最后一步是一个非选择性的范围扫描,执行了76563次,访问了11432983行数据,丢弃了99%的数据行,只保留76563行。为什么会出现只需要76563行数据,却访问了11432983行数据的情况呢?

Example 6-1    Looking for Thrown-Away Rows in an Explain Plan
Rows      Execution Plan
--------  ----------------------------------------------------
      12  SORT AGGREGATE
       2   SORT GROUP BY
   76563    NESTED LOOPS
   76575     NESTED LOOPS
      19      TABLE ACCESS FULL CN_PAYRUNS_ALL
   76570      TABLE ACCESS BY INDEX ROWID CN_POSTING_DETAILS_ALL
   76570       INDEX RANGE SCAN (object id 178321)
   76563     TABLE ACCESS BY INDEX ROWID CN_PAYMENT_WORKSHEETS_ALL
11432983      INDEX RANGE SCAN (object id 186024)

2.4,使用EXPLAIN PLAN评估执行计划

        仅通过执行计划无法判断SQL语句执行性能的优劣。比如,一条SQL的执行计划显示它走了索引,但这并不意味着这条SQL就是高效的。有时候SQL走索引反而低效,为了确认走索引是否高效,可以做如下检查:

- 正在使用的索引列
- 它们的选择性(访问表的数据量与总量的比例)

         最好使用EXPLAIN PLAN来确定访问计划,然后通过测试证明它是最优的计划。在评估计划时,检查语句的实际资源消耗。

2.5,使用V$SQL_PLAN视图来评估执行计划

        除了运行EXPLAIN PLAN命令并显示计划之外,还可以通过查询V$SQL_PLAN视图来显示计划。V$SQL_PLAN包含存储在共享SQL区域中的所有语句的执行计划。它的定义类似于PLAN_TABLE。

        与EXPLAIN PLAN相比,V$SQL_PLAN的优势在于不需要知道用于执行特定语句的编译环境。而EXPLAIN PLAN就要设置一个相同的环境来获得相同的计划。

        V$SQL_PLAN_STATISTICS视图为计划中的每个操作提供了实际的执行统计信息,比如输出行数和运行时间。所有统计数据(输出行数除外)都是累积的。例如,join操作的统计信息就包括其两个输入的统计信息。在STATISTICS_LEVEL初始化参数设置为ALL时编译的游标可以使用V$SQL_PLAN_STATISTICS中的统计信息。

        V$SQL_PLAN_STATISTICS_ALL视图支持同时比较优化器提供的行数和运行时间的估计值。这个视图结合了所有游标的V$SQL_PLAN和V$SQL_PLAN_STATISTICS信息。

2.6,EXPLAIN PLAN限制

        当数据绑定变量使用了类型隐式转换时,数据库就不支持该SQL语句执行EXPLAIN PLAN。对于一般的绑定变量,EXPLAIN PLAN输出可能并不代表真正的执行计划。

        TKPROF不能通过SQL语句文本来确定绑定变量的类型,因此它会先假定绑定变量类型为VARCHAR,当VARCHAR这个假设有问题时它才报错。可以通过在SQL语句中使用适当的类型转换来避免这种限制。

2.7,创建 PLAN_TABLE

        PLAN_TABLE是全局临时表SYS.PLAN_TABLE$的同义词,这个同义词是数据库自动创建的。这个表可以存所有用户的EXPLAIN PLAN语句的输出。而且EXPLAIN PLAN语句输出的执行计划信息默认是写入表PLAN_TABLE的。

        虽然PLAN_TABLE是数据库自动设置给所有用户使用的,但也是可以通过数据库脚本$ORACLE_HOME/rdbms/admin/catplan.sql(文件名和路径可能因操作系统不同而有差异)来手动创建全局临时表和同义词PLAN_TABLE的。例如,开起一个 SQL*Plus进程,使用 SYSDBA权限登录,执行如下语句即可:

@$ORACLE_HOME/rdbms/admin/catplan.sql

          Oracle建议在升级数据库版本之后重建本地PLAN_TABLE表,因为列可能会发生更改。如果列发生更改,就可能导致在使用PLAN_TABLE表时脚本执行失败或TKPROF失败。当然,如果不想使用PLAN_TABLE 这个同义词,可以在执行完catplan.sql脚本后创建一个新的同义词:

CREATE OR REPLACE PUBLIC SYNONYM my_plan_table for sys.plan_table$

3,用 EXPLAIN PLAN语句生成执行计划信息

        可以通过EXPLAIN PLAN语句查看优化器为SQL语句选择的执行计划。

3.1,解析SQL语句:基本步骤

        EXPLAIN PLAN会将为SQL语句解析的计划信息存放在PLAN_TABLE表中,如果没有这个表,可以执行脚本catplan.sql来生成。执行EXPLAIN PLAN语句需要的权限:

- 需要具有将计划信息数据行插入到输出表的权限。
- 需要有执行要查看执行计划信息的SQL的权限,而且要有SQL中所级联的表和视图的权限,如果级联的视图中还有级联,仍然需要有那些级联对象的权限。

        另外,查询存计划信息的表时也需要有相应的查询权限。

        解析SQL语句的步骤:

1)启动SQL*Plus或SQL Developer,并以具有必要权限的用户身份登录到数据库。

2)在SQL语句前加上EXPLAIN PLAN FOR来解析SQL语句,如下为解析查询employees表的SQL的例子:

EXPLAIN PLAN FOR
  SELECT e.last_name, d.department_name, e.salary
  FROM   employees e, departments d
  WHERE  salary < 3000
  AND    e.department_id = d.department_id
  ORDER BY salary DESC;

3)执行完EXPLAIN PLAN语句后,使用数据库自带的脚本或者包查看存入计划表中的最新的执行计划信息。
      如下为使用DBMS_XPLAN.DISPLAY函数查看:

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(format => 'ALL'));

4)检查具体的计划信息,如下为hash join的执行计划信息:

SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(format => 'ALL'));
Plan hash value: 3556827125
-----------------------------------------------------------------------------------
| Id  | Operation           | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |             |     4 |   124 |     5  (20)| 00:00:01 |
|   1 |  SORT ORDER BY      |             |     4 |   124 |     5  (20)| 00:00:01 |
|*  2 |   HASH JOIN         |             |     4 |   124 |     4   (0)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL| EMPLOYEES   |     4 |    60 |     2   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL| DEPARTMENTS |    27 |   432 |     2   (0)| 00:00:01 |
-----------------------------------------------------------------------------------
Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------
   1 - SEL$1
   3 - SEL$1 / E@SEL$1
   4 - SEL$1 / D@SEL$1
Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
   3 - filter("SALARY"<3000)
Column Projection Information (identified by operation id):
-----------------------------------------------------------
   1 - (#keys=1) INTERNAL_FUNCTION("E"."SALARY")[22],"E"."LAST_NAME"[VARCHAR2,25], "D"."DEPARTMENT_NAME"[VARCHAR2,30]
   2 - (#keys=1) "E"."LAST_NAME"[VARCHAR2,25], "SALARY"[NUMBER,22],"D"."DEPARTMENT_NAME"[VARCHAR2,30], "D"."DEPARTMENT_NAME"[VARCHAR2,30]
   3 - "E"."LAST_NAME"[VARCHAR2,25], "SALARY"[NUMBER,22],"E"."DEPARTMENT_ID"[NUMBER,22]
   4 - "D"."DEPARTMENT_ID"[NUMBER,22], "D"."DEPARTMENT_NAME"[VARCHAR2,30]
Note
-----
   - this is an adaptive plan

        EXPLAIN PLAN输出的计划信息的执行顺序是,向右缩进越多就越先执行,缩进最多的最先执行,当有多行计划信息有同样的缩进时,先执行上面的一行计划。

        另外,由于数据库的配置不同,可能导致优化器选择的执行计划不一样。

3.2,执行EXPLAIN PLAN时使用Statement ID

        表PLAN_TABLE的第一个字段就是statement_id,当连续执行EXPLAIN PLAN生成了多条SQL的执行计划时,可以通过设置statement_id的初始值为一个标识字符串,这样便于查看不同SQL的执行计划。在SET STATEMENT_ID前需要确认PLAN_TABLE表中不存在相同的statement_id标识串,可以先删除PLAN_TABLE表中该标识串的行。如下为设置SQL的标识串为'st1':

EXPLAIN PLAN
  SET STATEMENT_ID = 'st1' FOR
  SELECT last_name FROM employees;

3.3,将执行EXPLAIN PLAN输出的计划信息指定输出到特定的表

        使用'INTO'关键字来将信息指定输出到特定表。如下为将计划信息输出到表my_plan_table:

EXPLAIN PLAN
  INTO my_plan_table FOR
  SELECT last_name FROM employees;

        指定特定表的同时,设置STATEMENT_ID标识串:

EXPLAIN PLAN
   SET STATEMENT_ID = 'st1'
   INTO my_plan_table FOR
   SELECT last_name FROM employees;

4,展示PLAN_TABLE表中的信息

        解析完计划后,使用Oracle数据库提供的SQL脚本或PL/SQL包显示计划表中最近的一条SQL的计划信息:


- utlxpls.sql
        此脚本显示串行处理的计划信息。示例6-4是使用utlxpls.sql脚本时计划表输出的一个示例。

- utlxplp.sql
        此脚本显示包含并行执行列的计划信息。

- DBMS_XPLAN.DISPLAY表函数
        此函数可以根据人为指定的选项来显示特定的信息:
1)当不使用PLAN_TABLE存放计划信息时,需要指定存放表名
2)执行EXPLAIN PLAN时,如果设置了statement_id,需要指定标识串
3)格式选项需要指定详细的级别:BASIC,SERIAL,TYPICAL,ALL

        使用DBMS_XPLAN来显示PLAN_TABLE中的计划信息:

SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY());

SELECT PLAN_TABLE_OUTPUT 
  FROM TABLE(DBMS_XPLAN.DISPLAY('MY_PLAN_TABLE', 'st1','TYPICAL'));

4.1,展示执行计划:示例

        执行 EXPLAIN PLAN生成SQL执行计划:

示例6-3:
EXPLAIN PLAN FOR
  SELECT e.employee_id, j.job_title, e.salary, d.department_name
  FROM   employees e, jobs j, departments d
  WHERE  e.employee_id < 103
  AND    e.job_id = j.job_id 
  AND    e.department_id = d.department_id;

        如下输出是优化器选择的示例6-3中SQL的执行计划信息:

示例6-4:
-----------------------------------------------------------------------------------
| Id  | Operation                     |  Name        | Rows  | Bytes | Cost (%CPU)|
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |              |     3 |   189 |    10  (10)|
|   1 |  NESTED LOOPS                 |              |     3 |   189 |    10  (10)|
|   2 |   NESTED LOOPS                |              |     3 |   141 |     7  (15)|
|*  3 |    TABLE ACCESS FULL          | EMPLOYEES    |     3 |    60 |     4  (25)|
|   4 |    TABLE ACCESS BY INDEX ROWID| JOBS         |    19 |   513 |     2  (50)|
|*  5 |     INDEX UNIQUE SCAN         | JOB_ID_PK    |     1 |       |            |
|   6 |   TABLE ACCESS BY INDEX ROWID | DEPARTMENTS  |    27 |   432 |     2  (50)|
|*  7 |    INDEX UNIQUE SCAN          | DEPT_ID_PK   |     1 |       |            |
-----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   3 - filter("E"."EMPLOYEE_ID"<103)
   5 - access("E"."JOB_ID"="J"."JOB_ID")
   7 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID"
------------------------------------------------------------------------------------------------
| Id | Operation                       | Name          | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------------
|  0 | SELECT STATEMENT                |               |     3 |   189 |     8  (13)| 00:00:01 |
|  1 |  NESTED LOOPS                   |               |       |       |            |          |
|  2 |   NESTED LOOPS                  |               |     3 |   189 |     8  (13)| 00:00:01 |
|  3 |    MERGE JOIN                   |               |     3 |   141 |     5  (20)| 00:00:01 |
|  4 |     TABLE ACCESS BY INDEX ROWID | JOBS          |    19 |   513 |     2   (0)| 00:00:01 |
|  5 |      INDEX FULL SCAN            | JOB_ID_PK     |    19 |       |     1   (0)| 00:00:01 |
|* 6 |     SORT JOIN                   |               |     3 |    60 |     3  (34)| 00:00:01 |
|  7 |      TABLE ACCESS BY INDEX ROWID| EMPLOYEES     |     3 |    60 |     2   (0)| 00:00:01 |
|* 8 |       INDEX RANGE SCAN          | EMP_EMP_ID_PK |     3 |       |     1   (0)| 00:00:01 |
|* 9 |    INDEX UNIQUE SCAN            | DEPT_ID_PK    |     1 |       |     0   (0)| 00:00:01 |
| 10 |   TABLE ACCESS BY INDEX ROWID   | DEPARTMENTS   |     1 |    16 |     1   (0)| 00:00:01 |
------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   6 - access("E"."JOB_ID"="J"."JOB_ID")
       filter("E"."JOB_ID"="J"."JOB_ID")
   8 - access("E"."EMPLOYEE_ID"<103)
   9 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")

4.2,自定义 PLAN_TABLE表的信息输出

        如果指定了STATEMENT_ID语句标识符,则可以编写自己的脚本来查询PLAN_TABLE

你可能感兴趣的:(ORACLE_SQL,Tuning)