当前位置:网站首页>SQL调优指南笔记6:Explaining and Displaying Execution Plans

SQL调优指南笔记6:Explaining and Displaying Execution Plans

2022-06-12 21:30:00 dingdingfish

本文为SQL Tuning Guide第6章“解释和显示执行计划”的笔记。

了解如何解释SQL语句并显示其计划对于 SQL 调优至关重要。

重要基本概念

  • row source tree
    A collection of row sources produced by the row source generator. The row source tree for a SQL statement shows information such as table order, access methods, join methods, and data operations such as filters and sorts.
    行源树
    由行源生成器生成的行源集合。 SQL 语句的行源树显示表顺序、访问方法、连接方法和数据操作(如过滤器和排序)等信息。

  • adaptive query plan
    An execution plan that changes after optimization because run-time conditions indicate that optimizer estimates are inaccurate. An adaptive query plan has different built-in plan options. During the first execution, before a specific subplan becomes active, the optimizer makes a final decision about which option to use. The optimizer bases its choice on observations made during the execution up to this point. Thus, an adaptive query plan enables the final plan for a statement to differ from the default plan.
    由于运行时条件表明优化器估计不准确而在优化后更改的执行计划。 自适应查询计划具有不同的内置计划选项。 在第一次执行期间,在特定子计划激活之前,优化器会最终决定使用哪个选项。 优化器的选择基于执行期间到当前为止的观察结果。 因此,自适应查询计划使语句的最终计划不同于默认计划。

  • final plan
    In an adaptive plan, the plan that executes to completion. The default plan can differ from the final plan.
    在自适应计划中,执行完成的计划。 默认计划可能与最终计划不同。

  • default plan
    For an adaptive plan, the execution plan initially chosen by the optimizer using the statistics from the data dictionary. The default plan can differ from the final plan.
    对于自适应计划,优化器最初使用数据字典中的统计信息选择的执行计划。 默认计划可能与最终计划不同。

  • automatic reoptimization
    The ability of the optimizer to automatically change a plan on subsequent executions of a SQL statement. Automatic reoptimization can fix any suboptimal plan chosen due to incorrect optimizer estimates, from a suboptimal distribution method to an incorrect choice of degree of parallelism.
    优化器在 SQL 语句的后续执行中自动更改计划的能力。 自动重新优化可以修复由于不正确的优化器估计而选择的任何次优计划,从次优分布方法到错误选择并行度。

6.1 Introduction to Execution Plans

执行计划是数据库为运行 SQL 语句而执行的操作序列。

6.1.1 Contents of an Execution Plan

单独的执行计划操作无法区分调整良好的语句和执行不理想的语句。

以下计划由一系列步骤组成。 每个步骤要么从数据库物理检索数据行,要么为发出语句的用户准备它们(数据行)。 以下计划显示了员工和部门表的连接:

SQL_ID  g9xaqjktdhbcd, child number 0
-------------------------------------
SELECT employee_id, last_name, first_name, department_name from
employees e, departments d WHERE e.department_id = d.department_id and
last_name like 'T%' ORDER BY last_name

Plan hash value: 1219589317

----------------------------------------------------------------------------------------
| Id | Operation                    | Name        |Rows | Bytes |Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|  0 | SELECT STATEMENT             |             |     |       |    5 (100)|          |
|  1 |  NESTED LOOPS                |             |   5 |   190 |    5   (0)| 00:00:01 |
|  2 |   TABLE ACCESS BY INDEX ROWID| EMPLOYEES   |   5 |   110 |    2   (0)| 00:00:01 |
|* 3 |    INDEX RANGE SCAN          | EMP_NAME_IX |   5 |       |    1   (0)| 00:00:01 |
|* 4 |   TABLE ACCESS FULL          | DEPARTMENTS |   1 |    16 |    1   (0)| 00:00:01 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("LAST_NAME" LIKE 'T%')
       filter("LAST_NAME" LIKE 'T%')
   4 - filter("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")

行源树是执行计划的核心。 此树显示以下信息:

  • 语句引用的表的连接顺序
    在以上计划中,员工是外行源(外围表,outer table,因为位于上方),部门是内行源(inner table)。
  • 语句中提到的每个表的访问路径
    在以上计划中,优化器选择使用索引扫描访问员工,使用全表扫描访问部门。
  • 语句中受连接操作影响的表的连接方法
    在以上计划中,优化器选择了一个嵌套循环连接。
  • 过滤、排序或聚合等数据操作
    在以上计划中,优化器过滤以 T 开头并匹配 department_id 的姓氏。

除了行源树之外,计划表还包含有关以下内容的信息:

  • 优化,例如每个操作的成本和基数
  • 分区,例如访问的分区集
  • 并行执行,比如join输入的分布方式

6.1.2 Why Execution Plans Change

执行计划可以并且确实会随着底层优化器输入的变化而变化。

注意:为避免执行计划更改可能导致的 SQL 性能回归,请考虑使用 SQL 计划管理。

6.1.2.1 Different Schemas

由于各种原因,schema可能会有所不同。

主要原因包括以下几点:

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

6.1.2.2 Different Costs

即使模式相同,优化器也可以在成本不同时选择不同的执行计划。

影响成本的一些因素包括:

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

6.2 Generating Plan Output Using the EXPLAIN PLAN Statement

EXPLAIN PLAN 语句使您能够检查优化器为 SQL 语句选择的执行计划(但不是实际的执行计划)。

6.2.1 About the EXPLAIN PLAN Statement

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

EXPLAIN PLAN 输出显示在解释语句时数据库将如何运行 SQL 语句。 由于执行环境和解释计划环境的不同,解释计划可能与语句执行期间使用的实际计划不同。

当 EXPLAIN PLAN 语句发出时,优化器选择一个执行计划,然后将描述执行计划每个步骤的行插入到指定的计划表中。 您还可以发出 EXPLAIN PLAN 语句作为 SQL 跟踪工具的一部分。

EXPLAIN PLAN 语句是 DML 语句而不是 DDL 语句。 因此,Oracle 数据库不会隐式提交 EXPLAIN PLAN 语句所做的更改。

6.2.1.1 About PLAN_TABLE

PLAN_TABLE 是 EXPLAIN PLAN 语句插入描述执行计划的行的默认示例输出表。

Oracle 数据库自动在 SYS 模式中创建一个全局临时表 PLAN_TABLE$,并创建 PLAN_TABLE 作为同义词。 PLAN_TABLE 的所有必要权限都授予 PUBLIC。 因此,每个会话都会在其临时表空间中获得自己的 PLAN_TABLE 私有副本。

您可以使用 SQL 脚本 catplan.sql 手动创建全局临时表和 PLAN_TABLE 同义词。 此脚本的名称和位置取决于您的操作系统。 在 UNIX 和 Linux 上,该脚本位于 $ORACLE_HOME/rdbms/admin 目录中。 例如,启动 SQL*Plus 会话,以 SYSDBA 权限连接,然后运行脚本,如下所示:

@$ORACLE_HOME/rdbms/admin/catplan.sql

示例输出表 PLAN_TABLE 的定义在您的分发媒体上的 SQL 脚本中可用。 您的输出表必须与该表具有相同的列名和数据类型。 此脚本的通用名称是 utlxplan.sql。 确切的名称和位置取决于您的操作系统。

6.2.1.2 EXPLAIN PLAN Restrictions

对于执行日期绑定变量的隐式类型转换的语句,Oracle 数据库不支持 EXPLAIN PLAN。

通常使用绑定变量,EXPLAIN PLAN 输出可能不代表真正的执行计划。

从 SQL 语句的文本中,TKPROF 无法确定绑定变量的类型。 它假定类型是 VARCHAR,否则给出错误消息。 您可以通过在 SQL 语句中进行适当的类型转换来避免此限制。

6.2.2 Explaining a SQL Statement: Basic Steps

使用 EXPLAIN PLAN 将 SQL 语句的计划存储在 PLAN_TABLE 中。

先决条件

此任务假定您的模式中存在一个名为 PLAN_TABLE 的示例输出表。 如果此表不存在,则运行 SQL 脚本 catplan.sql。

要执行 EXPLAIN PLAN,您必须具有以下权限:

  • 您必须具有将行插入到您指定用于保存执行计划的现有输出表中所需的权限
  • 您还必须具有执行要为其确定执行计划的 SQL 语句所需的权限。 如果 SQL 语句访问视图,则您必须具有访问该视图所基于的任何表和视图的权限。 如果视图基于另一基于表的视图,则您必须具有访问另一个视图及其所基于表的权限。

要检查 EXPLAIN PLAN 语句生成的执行计划,您必须具有查询输出表所需的权限。

EXPLAIN PLAN FOR <SQL 语句>;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(format => 'ALL')); -- format有很多设置

解释一个语句

EXPLAIN PLAN 输出中的执行顺序从向右缩进最远的行开始(大多数情况如此,但不一定)下一步执行的是该行的父级。 如果两行缩进相等,则通常首先执行上方的行。

注意:本章中 EXPLAIN PLAN 输出中的步骤可能与您的数据库不同。 优化器可能会根据数据库配置选择不同的执行计划。

6.2.3 Specifying a Statement ID in EXPLAIN PLAN: Example

对于多个语句,您可以指定一个语句标识符并使用它来标识您的特定执行计划。

在使用 SET STATEMENT ID 之前,删除该语句 ID 的所有现有行。 在以下示例中,st1 被指定为语句标识符。

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

6.2.4 Specifying a Different Location for EXPLAIN PLAN Output: Example

EXPLAIN PLAN 的 INTO 子句指定了一个不同的表来存储输出。

如果您不想使用名称 PLAN_TABLE,请在运行 catplan.sql 脚本后创建一个新的同义词。 例如:

CREATE OR REPLACE PUBLIC SYNONYM my_plan_table for plan_table$

EXPLAIN PLAN
  INTO my_plan_table FOR
  SELECT last_name FROM employees;

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

6.2.5 EXPLAIN PLAN Output for a CONTAINERS Query: Example

CONTAINERS 子句可用于查询用户创建的和 Oracle 提供的表和视图。 它使您能够跨所有容器查询这些表和视图。

并且默认都会启用并行和分区。

6.3 Displaying Execution Plans

显示执行计划的最简单方法是使用 DBMS_XPLAN display函数或 V$ 视图。

6.3.1 About the Display of PLAN_TABLE Output

要显示计划表输出,您可以使用 SQL 脚本或 DBMS_XPLAN 包。

解释完计划后,使用 Oracle 数据库提供的以下 SQL 脚本或 PL/SQL 包显示最近的计划表输出:

  • DBMS_XPLAN.DISPLAY 表函数
  • utlxpls.sql
    此脚本显示串行处理的计划表输出
  • utlxplp.sql
    此脚本显示计划表输出,包括并行执行列。

以上3种方法的示例为:

explain plan for select count(*) from employees;
select * from table(dbms_xplan.display);
-- or
@?/rdbms/admin/utlxpls
-- or
@?/rdbms/admin/utlxplp

6.3.1.1 DBMS_XPLAN Display Functions

您可以使用 DBMS_XPLAN 显示函数来显示计划。

显示函数接受用于显示计划表输出的选项。 您可以指定:

  • 计划表名称,如果您使用不同于 PLAN_TABLE 的表
  • 语句 ID,如果您使用 EXPLAIN PLAN 设置了语句 ID
  • 确定详细程度的格式选项:BASIC、SERIAL、TYPICAL、ALL,在某些情况下为 ADAPTIVE

DBMS_XPLAN 显示函数如下表:

显示函数说明
DISPLAY此表函数显示计划表的内容。
此外,您可以使用此表函数显示存储在表中的任何计划(有或没有统计信息),只要此表的列与计划表的列命名相同(如果包含统计信息,则为 V$SQL_PLAN_STATISTICS_ALL )。 您可以在指定的表上应用谓词来选择要显示的计划行。
格式参数控制计划的级别。 它接受值 BASIC、TYPICAL、SERIAL 和 ALL。
DISPLAY_AWR此表函数显示存储在 AWR 中的执行计划的内容。格式参数同上。
DISPLAY_CURSOR此表函数显示游标缓存中加载的任何游标的解释计划。 除了解释计划之外,还可以报告各种计划统计信息(如 I/O、内存和计时)(基于 V$SQL_PLAN_STATISTICS_ALL VIEWS)。

格式参数控制计划的级别。 它接受值 BASIC、TYPICAL、SERIAL、ALL 和 ADAPTIVE。 当您指定 ADAPTIVE 时,输出包括:
- 最后的计划。 如果执行尚未完成,则输出显示当前计划。 本节还包括有关影响计划的运行时优化的注释。
- 推荐计划。 在报告模式下,输出包括将根据执行统计信息选择的计划。
- 动态计划。 输出总结了计划中与优化器选择的默认计划不同的部分。
- 重新优化。 输出显示由于重新优化而将在后续执行中选择的计划。
DISPLAY_PLAN此表函数以各种格式显示计划表的内容,具有 CLOB 输出类型。

格式参数控制计划的级别。 它接受值 BASIC、TYPICAL、SERIAL、ALL 和 ADAPTIVE。 当您指定 ADAPTIVE 时,输出包括默认计划。 对于每个动态子计划,该计划显示原始行源中可能被替换的行源的列表,以及将替换它们的行源。

如果格式参数指定大纲显示,则函数显示动态子计划中每个选项的提示。 如果计划不是自适应查询计划,则该函数显示默认计划。 当您未指定 ADAPTIVE 时,计划按原样显示,但在注释部分中显示任何动态行源的附加注释。
DISPLAY_SQL_PLAN_BASELINE此表函数显示 SQL 计划基线的指定 SQL 句柄的一个或多个执行计划。

此功能使用存储在计划基线中的计划信息来解释和显示计划。 SQL管理库中存储的plan_id可能与生成计划的plan_id不匹配。 存储的 plan_id 和生成的 plan_id 不匹配意味着它是一个不可重现的计划。 这样的计划被认为是无效的,并在 SQL 编译期间被优化器绕过。
DISPLAY_SQLSET此表函数显示存储在 SQL 调整集中的给定语句的执行计划。

格式参数控制计划的级别。 它接受值 BASIC、TYPICAL、SERIAL 和 ALL。

6.3.1.2 Plan-Related Views

您可以通过查询动态性能和数据字典视图来获取有关执行计划的信息。

  • V$SQL
    列出游标的统计信息,并为输入的原始 SQL 文本的每个子项包含一行。

    从 Oracle Database 19c 开始,V$SQL.QUARANTINED 指示语句是否已被资源管理器终止,因为该语句消耗了太多资源。 Oracle 数据库记录并标记隔离的计划,并阻止执行使用这些计划的语句执行。 AVOIDED_EXECUTIONS 列指示由于隔离语句而尝试但被阻止的执行次数。

  • V$SQL_SHARED_CURSOR
    解释为什么特定的子游标不与现有的子游标共享。 每列标识不能共享游标的特定原因。

    USE_FEEDBACK_STATS 列显示子游标是否由于重新优化而无法匹配。

  • V$SQL_PLAN
    包含存储在共享 SQL 区域中的每个语句的计划。

    视图定义类似于 PLAN_TABLE。 该视图包括出现在所有最终计划中的所有行的超集。 PLAN_LINE_ID 是连续编号的,但对于单个最终计划,ID 可能不连续。

    作为 EXPLAIN PLAN 的替代方法,您可以通过查询 V$SQL_PLAN 来显示计划。 V$SQL_PLAN 优于 EXPLAIN PLAN 的优点是您不需要知道用于执行特定语句的编译环境。 对于 EXPLAIN PLAN,您需要设置相同的环境以在执行语句时获得相同的计划。

  • V$SQL_PLAN_STATISTICS
    提供计划中每个操作的实际执行统计信息,例如输出行数和经过的时间。 除输出行数外,所有统计信息都是累计的。 例如,联结操作的统计信息还包括其两个输入的统计信息。 V$SQL_PLAN_STATISTICS 中的统计信息可用于在 STATISTICS_LEVEL 初始化参数设置为 ALL 的情况下编译的游标。

  • V$SQL_PLAN_STATISTICS_ALL
    包含使用 SQL 内存(排序或散列连接)的行源的内存使用统计信息。 此视图将 V$SQL_PLAN 中的信息与来自 V$SQL_PLAN_STATISTICSV$SQL_WORKAREA 的执行统计信息连接起来。

    V$SQL_PLAN_STATISTICS_ALL 允许并排比较优化器为行数和经过时间提供的估计值。 这个视图结合了每个游标的 V$SQL_PLANV$SQL_PLAN_STATISTICS 信息。

6.3.2 Displaying Execution Plans: Basic Steps

DBMS_XPLAN.DISPLAY 函数是一种显示解释计划的简单方法。

默认情况下,DISPLAY 功能使用 TYPICAL 格式设置。 在这种情况下,计划与计划中最相关的信息:操作id、名称和选项、行、字节和优化器成本。 修剪、并行和谓词信息仅在适用时显示。

例如:

-- 例1
explain plan 
	for <SQL>;
select * from table(dbms_xplan.display);

-- 例2
explain plan 
  SET statement_id = 'ex_plan2'
  for <SQL>;;

SELECT * from from table(DBMS_XPLAN.DISPLAY(NULL, 'ex_plan2','BASIC'));

6.3.3 Displaying Adaptive Query Plans: Tutorial

自适应优化器是优化器的一项功能,它可以根据运行时统计信息调整计划。 所有自适应机制都可以为语句执行不同于默认计划的最终计划。

自适应查询计划在当前语句执行期间对子计划进行选择。 相反,自动重新优化仅在当前语句执行之后发生的执行上更改计划。

您可以根据计划的Notes中的注释来确定数据库是否对 SQL 语句使用了自适应查询优化。 注释表明行源是否是动态的,或者自动重新优化是否适应了计划。

假设

本教程假定以下内容:

  • STATISTICS_LEVEL 初始化参数设置为 ALL。
-- 默认值为TYPICAL
-- 也可以设置gather_plan_statistics hint
alter system set STATISTICS_LEVEL = 'ALL';
  • 数据库使用默认设置进行自适应执行。
  • 作为用户 oe,您要发出以下单独的查询:
-- 查询1
SELECT o.order_id, v.product_name
FROM   orders o,
       (  SELECT order_id, product_name
          FROM   order_items o, product_information p
          WHERE  p.product_id = o.product_id
          AND    list_price < 50
          AND    min_price < 40  ) v
WHERE  o.order_id = v.order_id;

-- 查询2
SELECT product_name
FROM   order_items o, product_information p  
WHERE  o.unit_price = 15 
AND    quantity > 1
AND    p.product_id = o.product_id;
  • 在执行每个查询之前,您希望查询 DBMS_XPLAN.DISPLAY_PLAN 以查看默认计划,即优化器在应用其自适应机制之前选择的计划。
  • 执行每个查询后,您要查询 DBMS_XPLAN.DISPLAY_CURSOR 以查看最终计划和自适应查询计划。
  • SYS 已授予 oe 下列权限:
GRANT SELECT ON V_$SESSION TO oe;
GRANT SELECT ON V_$SQL TO oe;
GRANT SELECT ON V_$SQL_PLAN TO oe;
GRANT SELECT ON V_$SQL_PLAN_STATISTICS_ALL TO oe;

要查看自适应优化的结果:

  1. 以用户oe连接数据库
  2. 执行查询1(第1次)
  3. 查询游标中的执行计划,此处使用了MERGE JOIN,E-Rows和A-Rows差距很大。
SET LINESIZE 165
SET PAGESIZE 0
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(FORMAT=>'+ALLSTATS'));

Plan hash value: 1906736282
 
----------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation             | Name                | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem |  O/1/M   |
----------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |                     |      1 |        |    269 |00:00:00.01 |    1290 |       |       |          |
|   1 |  NESTED LOOPS         |                     |      1 |      1 |    269 |00:00:00.01 |    1290 |       |       |          |
|   2 |   MERGE JOIN CARTESIAN|                     |      1 |      4 |   9135 |00:00:00.01 |      17 |       |       |          |
|*  3 |    TABLE ACCESS FULL  | PRODUCT_INFORMATION |      1 |      1 |     87 |00:00:00.01 |      16 |       |       |          |
|   4 |    BUFFER SORT        |                     |     87 |    105 |   9135 |00:00:00.01 |       1 |  4096 |  4096 |     1/0/0|
|   5 |     INDEX FULL SCAN   | ORDER_PK            |      1 |    105 |    105 |00:00:00.01 |       1 |       |       |          |
|*  6 |   INDEX UNIQUE SCAN   | ORDER_ITEMS_UK      |   9135 |      1 |    269 |00:00:00.01 |    1273 |       |       |          |
----------------------------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   3 - filter(("MIN_PRICE"<40 AND "LIST_PRICE"<50))
   6 - access("O"."ORDER_ID"="ORDER_ID" AND "P"."PRODUCT_ID"="O"."PRODUCT_ID")
 

28 rows selected. 
  1. 第2次执行此查询
  2. 再次查询执行计划,这回变成了HASH JOIN,E-Rows和A-Rows比较接近,注意Notes中的信息,正是因为使用了statistics feedback,才使执行计划更优
SQL_ID  gm2npz344xqn8, child number 1
------------------------------------- 
Plan hash value: 35479787
 
-----------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation              | Name                | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem |  O/1/M   |
-----------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |                     |      1 |        |    269 |00:00:00.01 |      29 |       |       |          |
|   1 |  NESTED LOOPS          |                     |      1 |    313 |    269 |00:00:00.01 |      29 |       |       |          |
|*  2 |   HASH JOIN            |                     |      1 |    313 |    269 |00:00:00.01 |      24 |  1399K|  1399K|     1/0/0|
|*  3 |    TABLE ACCESS FULL   | PRODUCT_INFORMATION |      1 |     87 |     87 |00:00:00.01 |      15 |       |       |          |
|   4 |    INDEX FAST FULL SCAN| ORDER_ITEMS_UK      |      1 |    665 |    665 |00:00:00.01 |       9 |       |       |          |
|*  5 |   INDEX UNIQUE SCAN    | ORDER_PK            |    269 |      1 |    269 |00:00:00.01 |       5 |       |       |          |
-----------------------------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   2 - access("P"."PRODUCT_ID"="O"."PRODUCT_ID")
   3 - filter(("MIN_PRICE"<40 AND "LIST_PRICE"<50))
   5 - access("O"."ORDER_ID"="ORDER_ID")
 
Note
-----
   - statistics feedback used for this statement
 

32 rows selected. 
  1. 查询V$SQL确认性能提升
SELECT CHILD_NUMBER, CPU_TIME, ELAPSED_TIME, BUFFER_GETS
FROM   V$SQL
WHERE  SQL_ID = 'gm2npz344xqn8';

CHILD_NUMBER   CPU_TIME ELAPSED_TIME BUFFER_GETS
------------ ---------- ------------ -----------
           0      33042        35734        1563
           1       4967         7182          43
  1. 解释查询2的计划
EXPLAIN PLAN FOR
  SELECT product_name 
  FROM   order_items o, product_information p  
  WHERE  o.unit_price = 15
  AND    quantity > 1  
  AND    p.product_id = o.product_id
  1. 查看计划表中的计划。从Note部分可知,这是一个adaptive plan。
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

Plan hash value: 1255158658
 
-------------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name                   | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |                        |     4 |   128 |     7   (0)| 00:00:01 |
|   1 |  NESTED LOOPS                |                        |     4 |   128 |     7   (0)| 00:00:01 |
|   2 |   NESTED LOOPS               |                        |     4 |   128 |     7   (0)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL         | ORDER_ITEMS            |     4 |    48 |     3   (0)| 00:00:01 |
|*  4 |    INDEX UNIQUE SCAN         | PRODUCT_INFORMATION_PK |     1 |       |     0   (0)| 00:00:01 |
|   5 |   TABLE ACCESS BY INDEX ROWID| PRODUCT_INFORMATION    |     1 |    20 |     1   (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   3 - filter("O"."UNIT_PRICE"=15 AND "QUANTITY">1)
   4 - access("P"."PRODUCT_ID"="O"."PRODUCT_ID")
 
Note
-----
   - this is an adaptive plan

22 rows selected.
  1. 执行刚刚解释计划的查询2
  2. 查看游标中的计划。由于步骤4的STATISTICS COLLECTOR,优化器选择了哈希Join而非nested loop。
SET LINESIZE 165
SET PAGESIZE 0
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(FORMAT=>'+ADAPTIVE'));

----------------------------------------------------------------------------------------------------------
|   Id  | Operation                     | Name                   | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------------------
|     0 | SELECT STATEMENT              |                        |       |       |     7 (100)|          |
|  *  1 |  HASH JOIN                    |                        |     4 |   128 |     7   (0)| 00:00:01 |
|-    2 |   NESTED LOOPS                |                        |     4 |   128 |     7   (0)| 00:00:01 |
|-    3 |    NESTED LOOPS               |                        |     4 |   128 |     7   (0)| 00:00:01 |
|-    4 |     STATISTICS COLLECTOR      |                        |       |       |            |          |
|  *  5 |      TABLE ACCESS FULL        | ORDER_ITEMS            |     4 |    48 |     3   (0)| 00:00:01 |
|- *  6 |     INDEX UNIQUE SCAN         | PRODUCT_INFORMATION_PK |     1 |       |     0   (0)|          |
|-    7 |    TABLE ACCESS BY INDEX ROWID| PRODUCT_INFORMATION    |     1 |    20 |     1   (0)| 00:00:01 |
|     8 |   TABLE ACCESS FULL           | PRODUCT_INFORMATION    |     1 |    20 |     1   (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - access("P"."PRODUCT_ID"="O"."PRODUCT_ID")
   5 - filter(("O"."UNIT_PRICE"=15 AND "QUANTITY">1))
   6 - access("P"."PRODUCT_ID"="O"."PRODUCT_ID")
 
Note
-----
   - this is an adaptive plan (rows marked '-' are inactive)
 

33 rows selected. 

最后,将设置恢复为默认值:

alter system set STATISTICS_LEVEL = 'TYPICAL';

6.3.4 Display Execution Plans: Examples

这些示例显示了显示执行计划的不同方式。

6.3.4.1 Customizing PLAN_TABLE Output

如果您已指定语句标识符,那么您可以编写自己的脚本来查询 PLAN_TABLE。

例如:

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

SELECT  cardinality "Rows", lpad(' ',level-1) || operation
        ||' '||options||' '||object_name "Plan"
FROM    PLAN_TABLE
CONNECT BY prior id = parent_id
        AND prior statement_id = statement_id
  START WITH id = 0
        AND statement_id = 'st1'
  ORDER BY id;

Rows	Plan
107	SELECT STATEMENT  
107	 INDEX FULL SCAN EMP_NAME_IX

Rows 列若为空,表示优化器对表没有任何统计信息。
您也可以选择成本。 这对于比较执行计划或理解优化器为什么选择一个执行计划而不是另一个很有用。

6.3.4.2 Displaying Parallel Execution Plans: Example

并行查询计划在重要方面与串行查询计划不同。

6.3.4.2.1 About EXPLAIN PLAN and Parallel Queries

通过选择驱动表来调整并行查询很像非并行查询调整练习。 但是,选择的规则是不同的。

在串行情况下,最好的驱动表在应用限制条件后产生的行数最少。 数据库使用非唯一索引将少量行连接到较大的表。

例如,考虑一个由customer, account, 和transaction组成的表层次结构。
在这里插入图片描述
在这个例子中,customer 是最小的表,而 transaction 是最大的表。 典型的 OLTP 查询检索有关特定客户帐户的交易信息。 查询来自customer表。 目标是最小化逻辑 I/O,这通常会最小化其他关键资源,包括物理 I/O 和 CPU 时间。

对于并行查询,驱动表通常是最大的表。 在这种情况下使用并行查询效率不高,因为每个表中只有几行被访问。 但是,如果需要识别上个月进行过某种类型交易的所有客户怎么办? 从transaction表驱动会更有效,因为customer表上不存在限制条件。 数据库会将transaction表中的行连接到account表,然后最后将结果集连接到customer表。 在这种情况下,account 和 customer 表上使用的可能是高度选择性的主键或唯一索引,而不是第一次查询中使用的非唯一索引。 因为transaction表很大并且列没有选择性,所以使用transaction表的并行查询驱动是有益的。

并行操作包括以下内容:

  • PARALLEL_TO_SERIAL(P->S)
    PARALLEL_TO_SERIAL 操作始终是查询协调器使用并行操作中的行时发生的步骤。 此查询中未发生的另一种类型的操作是 SERIAL 操作。 如果发生这些类型的操作,请考虑使它们成为并行操作以提高性能,因为它们也是潜在的瓶颈。
  • PARALLEL_FROM_SERIAL
  • PARALLEL_TO_PARALLEL (P->P)
    如果每个步骤中的工作负载相对相等,则 PARALLEL_TO_PARALLEL 操作通常会产生最佳性能。
  • PARALLEL_COMBINED_WITH_CHILD (PCWC)
  • PARALLEL_COMBINED_WITH_PARENT (PCWP)

当数据库与父步骤同时执行该步骤时,会发生 PARALLEL_COMBINED_WITH_PARENT 操作。

如果并行步骤产生许多行,则 QC 可能无法像产生行一样快地使用这些行。 几乎无法改善这种情况。

6.3.4.2.2 Viewing Parallel Queries with EXPLAIN PLAN: Example

将 EXPLAIN PLAN 与并行查询一起使用时,数据库编译并执行一个并行计划。 该计划是通过在 QC 计划中分配特定于并行支持的行源而从串行计划派生的。

两个并行执行服务器集 PQ 模型所需的表队列行源(PX 发送和 PX 接收)、粒度迭代器和缓冲区排序直接插入并行计划。 当并行执行时,该计划对于所有并行执行服务器或串行执行时对于 QC 是相同的计划。

CREATE TABLE emp2 AS SELECT * FROM employees;

ALTER TABLE emp2 PARALLEL 2;

EXPLAIN PLAN FOR
  SELECT SUM(salary) 
  FROM   emp2 
  GROUP BY department_id;

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

------------------------------------------------------------------------------------------------------------------
| Id  | Operation                | Name     | Rows  | Bytes | Cost (%CPU)| Time     |    TQ  |IN-OUT| PQ Distrib |
------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |          |    11 |    77 |     3  (34)| 00:00:01 |        |      |            |
|   1 |  PX COORDINATOR          |          |       |       |            |          |        |      |            |
|   2 |   PX SEND QC (RANDOM)    | :TQ10001 |    11 |    77 |     3  (34)| 00:00:01 |  Q1,01 | P->S | QC (RAND)  |
|   3 |    HASH GROUP BY         |          |    11 |    77 |     3  (34)| 00:00:01 |  Q1,01 | PCWP |            |
|   4 |     PX RECEIVE           |          |    11 |    77 |     3  (34)| 00:00:01 |  Q1,01 | PCWP |            |
|   5 |      PX SEND HASH        | :TQ10000 |    11 |    77 |     3  (34)| 00:00:01 |  Q1,00 | P->P | HASH       |
|   6 |       HASH GROUP BY      |          |    11 |    77 |     3  (34)| 00:00:01 |  Q1,00 | PCWP |            |
|   7 |        PX BLOCK ITERATOR |          |   107 |   749 |     2   (0)| 00:00:01 |  Q1,00 | PCWC |            |
|   8 |         TABLE ACCESS FULL| EMP2     |   107 |   749 |     2   (0)| 00:00:01 |  Q1,00 | PCWP |            |
------------------------------------------------------------------------------------------------------------------
 
Note
-----
   - Degree of Parallelism is 2 because of table property

一组并行执行服务器并行扫描 EMP2,而第二组执行 GROUP BY 操作的聚合。 PX BLOCK ITERATOR 行源表示将表 EMP2 拆分为多个部分,以在并行执行服务器之间划分扫描工作负载。 PX SEND 和 PX RECEIVE 行源表示连接两组并行执行服务器的管道,因为行从并行扫描向上流动,通过 HASH 表队列重新分区,然后由顶部集合读取并聚合。 PX SEND QC 行源表示以随机 (RAND) 顺序发送到 QC 的聚合值。 PX COORDINATOR 行源表示 QC 或查询协调器,它控制和调度出现在计划树中的并行计划。

6.3.4.3 Displaying Bitmap Index Plans: Example

使用位图索引的索引行源出现在 EXPLAIN PLAN 输出中,其中单词 BITMAP 指示索引的类型。

6.3.4.4 Displaying Result Cache Plans: Example

当您的查询包含 result_cache 提示时,会将 ResultCache 运算符插入到执行计划中。

从 Oracle Database 21c 开始,result_cache 提示接受一个新选项:result_cache(TEMP={TRUE|FALSE})。 TRUE 值使查询溢出到磁盘,而 FALSE 防止形成 Temp 对象,Result 对象将进入“绕过”状态。

在此计划中,RESULT CACHE 操作由其缓存 ID 标识。 您可以使用此 CACHE_ID 查询 V$RESULT_CACHE_OBJECTS 视图。

6.3.4.5 Displaying Plans for Partitioned Objects: Example

使用 EXPLAIN PLAN 确定 Oracle 数据库如何访问特定查询的分区对象。

修剪后访问的分区显示在 PARTITION START 和 PARTITION STOP 列中。 范围分区的行源名称是 PARTITION RANGE。 对于散列分区,行源名称为 PARTITION HASH。

如果其中一个连接表的计划表的 DISTRIBUTION 列包含 PARTITION(KEY),则使用部分分区方式连接来实现连接。 如果其中一个联接表在其联接列上进行分区并且该表是并行化的,则可以进行部分分区联接。

如果分区行源出现在 EXPLAIN PLAN 输出中的连接行源之前,则使用完全分区连接来实现连接。 仅当两个连接表在其各自的连接列上均等分区时,才可能进行完全分区连接。 以下是几种分区类型的执行计划示例。

6.3.4.5.1 Displaying Range and Hash Partitioning with EXPLAIN PLAN: Examples
CREATE TABLE emp_range 
PARTITION BY RANGE(hire_date) 
( 
  PARTITION emp_p1 VALUES LESS THAN (TO_DATE('1-JAN-1992','DD-MON-YYYY')),
  PARTITION emp_p2 VALUES LESS THAN (TO_DATE('1-JAN-1994','DD-MON-YYYY')),
  PARTITION emp_p3 VALUES LESS THAN (TO_DATE('1-JAN-1996','DD-MON-YYYY')),
  PARTITION emp_p4 VALUES LESS THAN (TO_DATE('1-JAN-1998','DD-MON-YYYY')),
  PARTITION emp_p5 VALUES LESS THAN (TO_DATE('1-JAN-2001','DD-MON-YYYY')),
  PARTITION emp_p6 VALUES LESS THAN (MAXVALUE)
) 
AS SELECT * FROM employees; 

-- PARTITION RANGE ALL
EXPLAIN PLAN FOR 
  SELECT * FROM emp_range; 

-- PARTITION RANGE ITERATOR
EXPLAIN PLAN FOR 
  SELECT * 
  FROM   emp_range 
  WHERE  hire_date >= TO_DATE('1-JAN-1996','DD-MON-YYYY');

-- PARTITION RANGE SINGLE
EXPLAIN PLAN FOR 
  SELECT *
  FROM   emp_range
  WHERE  hire_date < TO_DATE('1-JAN-1992','DD-MON-YYYY');  
  
select * from table(dbms_xplan.display);

Oracle 数据库为散列分区对象显示相同的信息,除了分区行源名称是 PARTITION HASH 而不是 PARTITION RANGE。 此外,对于散列分区,修剪只能使用相等或 IN-list 谓词。

6.3.4.5.2 Pruning Information with Composite Partitioned Objects: Examples
-- 复合分区的例子,以雇佣日期作为范围分区,以部门ID作为子哈希分区
CREATE TABLE emp_comp PARTITION BY RANGE(hire_date) 
      SUBPARTITION BY HASH(department_id) SUBPARTITIONS 3 
( 
PARTITION emp_p1 VALUES LESS THAN (TO_DATE('1-JAN-1992','DD-MON-YYYY')),
PARTITION emp_p2 VALUES LESS THAN (TO_DATE('1-JAN-1994','DD-MON-YYYY')),
PARTITION emp_p3 VALUES LESS THAN (TO_DATE('1-JAN-1996','DD-MON-YYYY')),
PARTITION emp_p4 VALUES LESS THAN (TO_DATE('1-JAN-1998','DD-MON-YYYY')),
PARTITION emp_p5 VALUES LESS THAN (TO_DATE('1-JAN-2001','DD-MON-YYYY')),
PARTITION emp_p6 VALUES LESS THAN (MAXVALUE)
) 
AS SELECT * FROM employees; 

-- PARTITION RANGE ALL & PARTITION HASH ALL
EXPLAIN PLAN FOR 
  SELECT * FROM emp_comp; 

-- PARTITION RANGE SINGLE & PARTITION HASH ALL
EXPLAIN PLAN FOR 
  SELECT * 
  FROM   emp_comp 
  WHERE  hire_date = TO_DATE('15-FEB-1998', 'DD-MON-YYYY'); 

-- PARTITION RANGE ALL & PARTITION HASH SINGLE
EXPLAIN PLAN FOR 
  SELECT * 
  FROM   emp_comp 
  WHERE  department_id = 20;

-- PARTITION RANGE ALL & PARTITION HASH SINGLE
-- PARTITION_START 和 PARTITION_STOP 都设置为 KEY。 该值意味着 Oracle 数据库在运行时确定子分区的数量。
VARIABLE dno NUMBER; 
EXPLAIN PLAN FOR 
  SELECT * 
  FROM   emp_comp 
  WHERE  department_id = :dno; 
select * from table(dbms_xplan.display);
6.3.4.5.3 Examples of Partial Partition-Wise Joins
drop table dept2 purge;
drop table emp_range_did purge;

-- 注意,2个表中只有1个建立了分区,但2个表都指定了DOP为2
CREATE TABLE dept2 AS SELECT * FROM departments;
ALTER TABLE dept2 PARALLEL 2;

CREATE TABLE emp_range_did PARTITION BY RANGE(department_id)
   (PARTITION emp_p1 VALUES LESS THAN (150),
    PARTITION emp_p5 VALUES LESS THAN (MAXVALUE) )
  AS SELECT * FROM employees;

ALTER TABLE emp_range_did PARALLEL 2;

SET PAGES 9999

EXPLAIN PLAN FOR 
  SELECT /*+ PQ_DISTRIBUTE(d NONE PARTITION) ORDERED */ e.last_name, 
         d.department_name 
  FROM   emp_range_did e, dept2 d 
  WHERE  e.department_id = d.department_id;

-- 执行计划显示对表 dept2 进行串行扫描,所有具有相同分区列值 emp_range_did (department_id) 的行通过
-- PART (KEY) 或分区键、表队列发送到执行部分分区的同一并行执行服务器,以执行部分分区联结。
/* ------------------------------------------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop | TQ |IN-OUT| PQ Distrib | ------------------------------------------------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 106 | 2862 | 9 (0)| 00:00:01 | | | | | | | 1 | PX COORDINATOR | | | | | | | | | | | | 2 | PX SEND QC (RANDOM) | :TQ10001 | 106 | 2862 | 9 (0)| 00:00:01 | | | Q1,01 | P->S | QC (RAND) | |* 3 | HASH JOIN BUFFERED | | 106 | 2862 | 9 (0)| 00:00:01 | | | Q1,01 | PCWP | | | 4 | PX PARTITION RANGE ALL | | 107 | 1177 | 7 (0)| 00:00:01 | 1 | 2 | Q1,01 | PCWC | | | 5 | TABLE ACCESS FULL | EMP_RANGE_DID | 107 | 1177 | 7 (0)| 00:00:01 | 1 | 2 | Q1,01 | PCWP | | | 6 | PX RECEIVE | | 27 | 432 | 2 (0)| 00:00:01 | | | Q1,01 | PCWP | | | 7 | PX SEND PARTITION (KEY)| :TQ10000 | 27 | 432 | 2 (0)| 00:00:01 | | | Q1,00 | P->P | PART (KEY) | | 8 | PX BLOCK ITERATOR | | 27 | 432 | 2 (0)| 00:00:01 | | | Q1,00 | PCWC | | | 9 | TABLE ACCESS FULL | DEPT2 | 27 | 432 | 2 (0)| 00:00:01 | | | Q1,00 | PCWP | | ------------------------------------------------------------------------------------------------------------------------------------------ */

SELECT * FROM TABLE(dbms_xplan.display);
6.3.4.5.4 Example of Full Partition-Wise Join
drop table dept_hash purge;
drop table emp_comp purge;

CREATE TABLE emp_comp PARTITION BY RANGE(hire_date) 
      SUBPARTITION BY HASH(department_id) SUBPARTITIONS 3 
( 
PARTITION emp_p1 VALUES LESS THAN (TO_DATE('1-JAN-1992','DD-MON-YYYY')),
PARTITION emp_p2 VALUES LESS THAN (TO_DATE('1-JAN-1994','DD-MON-YYYY')),
PARTITION emp_p3 VALUES LESS THAN (TO_DATE('1-JAN-1996','DD-MON-YYYY')),
PARTITION emp_p4 VALUES LESS THAN (TO_DATE('1-JAN-1998','DD-MON-YYYY')),
PARTITION emp_p5 VALUES LESS THAN (TO_DATE('1-JAN-2001','DD-MON-YYYY')),
PARTITION emp_p6 VALUES LESS THAN (MAXVALUE)
) 
AS SELECT * FROM employees; 

CREATE TABLE dept_hash
   PARTITION BY HASH(department_id)
   PARTITIONS 3
   PARALLEL 2
   AS SELECT * FROM departments;

EXPLAIN PLAN FOR 
  SELECT /*+ PQ_DISTRIBUTE(e NONE NONE) ORDERED */ e.last_name,
         d.department_name
  FROM   emp_comp e, dept_hash d
  WHERE  e.department_id = d.department_id;

-- 执行计划与原文不同,记录如下
/* ---------------------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop | TQ |IN-OUT| PQ Distrib | ---------------------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 106 | 2862 | 29 (0)| 00:00:01 | | | | | | | 1 | PX COORDINATOR | | | | | | | | | | | | 2 | PX SEND QC (RANDOM) | :TQ10001 | 106 | 2862 | 29 (0)| 00:00:01 | | | Q1,01 | P->S | QC (RAND) | |* 3 | HASH JOIN | | 106 | 2862 | 29 (0)| 00:00:01 | | | Q1,01 | PCWP | | | 4 | BUFFER SORT | | | | | | | | Q1,01 | PCWC | | | 5 | PART JOIN FILTER CREATE | :BF0000 | 107 | 1177 | 17 (0)| 00:00:01 | | | Q1,01 | PCWP | | | 6 | PX RECEIVE | | 107 | 1177 | 17 (0)| 00:00:01 | | | Q1,01 | PCWP | | | 7 | PX SEND BROADCAST | :TQ10000 | 107 | 1177 | 17 (0)| 00:00:01 | | | | S->P | BROADCAST | | 8 | PARTITION RANGE ALL | | 107 | 1177 | 17 (0)| 00:00:01 | 1 | 6 | | | | | 9 | PARTITION HASH ALL | | 107 | 1177 | 17 (0)| 00:00:01 | 1 | 3 | | | | | 10 | TABLE ACCESS FULL | EMP_COMP | 107 | 1177 | 17 (0)| 00:00:01 | 1 | 18 | | | | | 11 | PX BLOCK ITERATOR ADAPTIVE| | 27 | 432 | 12 (0)| 00:00:01 |:BF0000|:BF0000| Q1,01 | PCWC | | | 12 | TABLE ACCESS FULL | DEPT_HASH | 27 | 432 | 12 (0)| 00:00:01 |:BF0000|:BF0000| Q1,01 | PCWP | | ---------------------------------------------------------------------------------------------------------------------------------------- */
SELECT * FROM TABLE(dbms_xplan.display);
6.3.4.5.5 Examples of INLIST ITERATOR and EXPLAIN PLAN

如果索引实现了 IN-list 谓词,则 INLIST ITERATOR 操作将出现在 EXPLAIN PLAN 输出中。

6.3.4.5.6 Example of Domain Indexes and EXPLAIN PLAN

您可以使用 EXPLAIN PLAN 为域索引导出用户定义的 CPU 和 I/O 成本。

EXPLAIN PLAN 在 PLAN_TABLE 的 OTHER 列中显示域索引统计信息。

6.4 Comparing Execution Plans

计划比较工具比较参考计划和任意测试计划列表,并突出显示它们之间的差异。 计划比较是逻辑上的,而不是逐行比较。

6.4.1 Purpose of Plan Comparison

计划比较报告确定了差异的来源,帮助用户对计划可重复性问题进行分类。

计划比较报告在以下情况下特别有用:

  • 您希望将性能下降的查询的当前计划与 AWR 中捕获的旧计划进行比较。
  • SQL 计划基线无法重现最初预期的计划,您想要确定新计划与预期计划之间的差异。
  • 您想确定添加提示、更改参数或创建索引将如何影响计划。
  • 您想要确定基于 SQL 配置文件或 SQL 性能分析器生成的计划与原始计划有何不同。

6.4.2 User Interface for Plan Comparison

您可以使用 DBMS_XPLAN.COMPARE_PLANS 生成文本、XML 或 HTML 格式的报告。

比较计划报告格式
报告以摘要开头。 COMPARE PLANS REPORT部分包括运行报告的用户和比较的计划数量等信息

报告的比较详细信息部分包含以下信息:

  • 计划信息
    这些信息包括计划编号、计划来源、计划属性(根据来源而有所不同)、解析模式和 SQL 文本。
  • 计划
    此部分显示计划行,包括谓词和注释。
  • 比较结果
    总结了比较结果,突出了连接顺序、连接方法、访问路径和并行分布方法等逻辑差异。

DBMS_XPLAN.PLAN_OBJECT_LIST Table Type
plan_object_list 类型允许将通用对象列表作为 DBMS_XPLAN.COMPARE_PLANS 函数的输入。
通用对象从所有计划源中抽象出计划的公共属性。 每个计划源都是 plan_object_list 超类的子类。

计划源包括:

  • Plan table
  • Cursor Cache
  • AWR
  • SQL tuning set
  • SQL plan management
  • SQL profile
  • Advisor

DBMS_XPLAN.COMPARE_PLANS Function

DBMS_XPLAN.COMPARE_PLANS(
    reference_plan        IN generic_plan_object,
    compare_plan_list     IN plan_object_list,
    type                  IN VARCHAR2 := 'TEXT',
    level                 IN VARCHAR2 := 'TYPICAL',
    section               IN VARCHAR2 := 'ALL')
RETURN CLOB;

两个例子:

VAR v_report CLOB;

BEGIN
  :v_report := DBMS_XPLAN.COMPARE_PLANS(
    reference_plan    => CURSOR_CACHE_OBJECT('8mkxm7ur07za0', 2),
    compare_plan_list => PLAN_OBJECT_LIST(CURSOR_CACHE_OBJECT('8mkxm7ur07za0', 4)));
END;
/

PRINT v_report

VAR v_report CLOB;
BEGIN
  :v_report := DBMS_XPLAN.COMPARE_PLANS( -
    reference_plan    => CURSOR_CACHE_OBJECT('8mkxm7ur07za0', 2),
    compare_plan_list => PLAN_OBJECT_LIST(SPM_OBJECT('SQL_024d0f7d21351f5d', 'SQL_PLAN_sdfjkd')));
END;

PRINT v_report

6.4.3 Comparing Execution Plans: Tutorial

要比较计划,请使用 DBMS_XPLAN.COMPARE_PLANS 函数。

在本教程中,您将比较两个不同的查询。 比较计划报告显示优化器能够在一个查询中使用联结消除转换,但不能在另一个查询中使用。

示例SQL使用SH schema,但我的输出与原文不同:

-- 63763
select count(*) 
from   products p, sales s 
where  p.prod_id = s.prod_id 
and    p.prod_min_price > 200;

-- 0
select count(*) 
from   products p, sales s 
where  p.prod_id = s.prod_id 
and    s.quantity_sold = 43;

SET LINESIZE 120
COL SQL_ID FORMAT a20
COL SQL_TEXT FORMAT a60

SELECT SQL_ID, SQL_TEXT
FROM   V$SQL
WHERE  SQL_TEXT LIKE '%products%'
AND    SQL_TEXT NOT LIKE '%SQL_TEXT%'
ORDER BY SQL_ID;

/* SQL_ID SQL_TEXT -------------------- ------------------------------------------------------------ 3b4w3kr4mjanm select count(*) from products p, sales s where p.prod_i d = s.prod_id and p.prod_min_price > 200 9d3rbfvq8gmym select count(*) from products p, sales s where p.prod_i d = s.prod_id and s.quantity_sold = 43 */

VARIABLE v_rep CLOB

BEGIN
  :v_rep := DBMS_XPLAN.COMPARE_PLANS( 
    reference_plan    => cursor_cache_object('3b4w3kr4mjanm', NULL),
    compare_plan_list => plan_object_list(cursor_cache_object('9d3rbfvq8gmym', NULL)),
    type              => 'TEXT',
    level             => 'TYPICAL', 
    section           => 'ALL');
END;
/

-- 打印报告
SET PAGESIZE 50000
SET LONG 100000
SET LINESIZE 210
COLUMN report FORMAT a200
SELECT :v_rep REPORT FROM DUAL;

/* REPORT -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- COMPARE PLANS REPORT --------------------------------------------------------------------------------------------- Current user : SH Total number of plans : 2 Number of findings : 1 --------------------------------------------------------------------------------------------- COMPARISON DETAILS --------------------------------------------------------------------------------------------- Plan Number : 1 (Reference Plan) Plan Found : Yes Plan Source : Cursor Cache SQL ID : 3b4w3kr4mjanm Child Number : 0 Plan Database Version : 21.0.0.0 Parsing Schema : "SH" SQL Text : select count(*) from products p, sales s where p.prod_id = s.prod_id and p.prod_min_price > 200 Plan ----------------------------- Plan Hash Value : 3037679890 -------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost | Time | -------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 37 | | | 1 | SORT AGGREGATE | | 1 | 13 | | | | * 2 | HASH JOIN | | 781685 | 10161905 | 37 | 00:00:01 | | * 3 | TABLE ACCESS FULL | PRODUCTS | 61 | 549 | 3 | 00:00:01 | | 4 | PARTITION RANGE ALL | | 918843 | 3675372 | 29 | 00:00:01 | | 5 | BITMAP CONVERSION TO ROWIDS | | 918843 | 3675372 | 29 | 00:00:01 | | 6 | BITMAP INDEX FAST FULL SCAN | SALES_PROD_BIX | | | | | -------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): ------------------------------------------ * 2 - access("P"."PROD_ID"="S"."PROD_ID") * 3 - filter("P"."PROD_MIN_PRICE">200) Notes ----- - This is an adaptive plan --------------------------------------------------------------------------------------------- Plan Number : 2 Plan Found : Yes Plan Source : Cursor Cache SQL ID : 9d3rbfvq8gmym Child Number : 0 Plan Database Version : 21.0.0.0 Parsing Schema : "SH" SQL Text : select count(*) from products p, sales s where p.prod_id = s.prod_id and s.quantity_sold = 43 Plan ----------------------------- Plan Hash Value : 4261227730 -------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost | Time | -------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 525 | | | 1 | SORT AGGREGATE | | 1 | 11 | | | | 2 | NESTED LOOPS | | 1 | 11 | 525 | 00:00:01 | | 3 | PARTITION RANGE ALL | | 1 | 7 | 525 | 00:00:01 | | * 4 | TABLE ACCESS FULL | SALES | 1 | 7 | 525 | 00:00:01 | | * 5 | INDEX UNIQUE SCAN | PRODUCTS_PK | 1 | 4 | 0 | | -------------------------------------------------------------------------------- Predicate Information (identified by operation id): ------------------------------------------ * 4 - filter("S"."QUANTITY_SOLD"=43) * 5 - access("P"."PROD_ID"="S"."PROD_ID") Comparison Results (1): ----------------------------- 1. Query block SEL$1: Join order is different at position 1 (reference plan: "P"@"SEL$1", current plan: "S"@"SEL$1"). --------------------------------------------------------------------------------------------- */

6.4.4 Comparing Execution Plans: Examples

本节示例使用了SH schema。

-- 首先解释计划,然后再执行一次
EXPLAIN PLAN 
  SET STATEMENT_ID='TEST' FOR
  SELECT c.cust_city, SUM(s.quantity_sold)
  FROM   customers c, sales s, products p
  WHERE  c.cust_id=s.cust_id
  AND    p.prod_id=s.prod_id
  AND    prod_min_price>100
  GROUP BY c.cust_city;

SELECT c.cust_city, SUM(s.quantity_sold)
FROM   customers c, sales s, products p
WHERE  c.cust_id=s.cust_id
AND    p.prod_id=s.prod_id
AND    prod_min_price>100
GROUP BY c.cust_city;

-- 得到SQL_ID为 6wsv429p7rka0
select * from v$sql where sql_text like '%prod_min_price>100%'

VARIABLE v_rep CLOB

BEGIN
  :v_rep := DBMS_XPLAN.COMPARE_PLANS(
    reference_plan    => plan_table_object('SH', 'PLAN_TABLE', 'TEST', NULL),
    compare_plan_list => plan_object_list(cursor_cache_object('6wsv429p7rka0')),
    type              => 'TEXT', 
    level             => 'TYPICAL',
    section           => 'ALL');
END;
/

PRINT v_rep

-- 结果显示两个计划是相同的
/* COMPARE PLANS REPORT --------------------------------------------------------------------------------------------- Current user : SH Total number of plans : 2 Number of findings : 1 --------------------------------------------------------------------------------------------- COMPARISON DETAILS --------------------------------------------------------------------------------------------- Plan Number : 1 (Reference Plan) Plan Found : Yes Plan Source : Plan Table Plan Table Owner : SH Plan Table Name : PLAN_TABLE Statement ID : TEST Plan ID : 18 Plan Database Version : 21.0.0.0 Parsing Schema : "SH" SQL Text : No SQL Text Plan ----------------------------- Plan Hash Value : 3489853474 -------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost | Time | -------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 620 | 20460 | 998 | 00:00:01 | | 1 | HASH GROUP BY | | 620 | 20460 | 998 | 00:00:01 | | * 2 | HASH JOIN | | 7059 | 232947 | 997 | 00:00:01 | | 3 | VIEW | VW_GBC_10 | 7059 | 127062 | 573 | 00:00:01 | | 4 | HASH GROUP BY | | 7059 | 148239 | 573 | 00:00:01 | | * 5 | HASH JOIN | | 852747 | 17907687 | 530 | 00:00:01 | | * 6 | TABLE ACCESS FULL | PRODUCTS | 67 | 603 | 3 | 00:00:01 | | 7 | PARTITION RANGE ALL | | 918843 | 11026116 | 522 | 00:00:01 | | 8 | TABLE ACCESS FULL | SALES | 918843 | 11026116 | 522 | 00:00:01 | | 9 | TABLE ACCESS FULL | CUSTOMERS | 55500 | 832500 | 424 | 00:00:01 | -------------------------------------------------------------------------------------- Predicate Information (identified by operation id): ------------------------------------------ * 2 - access("C"."CUST_ID"="ITEM_1") * 5 - access("P"."PROD_ID"="S"."PROD_ID") * 6 - filter("PROD_MIN_PRICE">100) Notes ----- - This is an adaptive plan --------------------------------------------------------------------------------------------- Plan Number : 2 Plan Found : Yes Plan Source : Cursor Cache SQL ID : 6wsv429p7rka0 Child Number : 0 Plan Database Version : 21.0.0.0 Parsing Schema : "SH" SQL Text : SELECT c.cust_city, SUM(s.quantity_sold) FROM customers c, sales s, products p WHERE c.cust_id=s.cust_id AND p.prod_id=s.prod_id AND prod_min_price>100 GROUP BY c.cust_city Plan ----------------------------- Plan Hash Value : 3489853474 -------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost | Time | -------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 998 | | | 1 | HASH GROUP BY | | 620 | 20460 | 998 | 00:00:01 | | * 2 | HASH JOIN | | 7059 | 232947 | 997 | 00:00:01 | | 3 | VIEW | VW_GBC_10 | 7059 | 127062 | 573 | 00:00:01 | | 4 | HASH GROUP BY | | 7059 | 148239 | 573 | 00:00:01 | | * 5 | HASH JOIN | | 852747 | 17907687 | 530 | 00:00:01 | | * 6 | TABLE ACCESS FULL | PRODUCTS | 67 | 603 | 3 | 00:00:01 | | 7 | PARTITION RANGE ALL | | 918843 | 11026116 | 522 | 00:00:01 | | 8 | TABLE ACCESS FULL | SALES | 918843 | 11026116 | 522 | 00:00:01 | | 9 | TABLE ACCESS FULL | CUSTOMERS | 55500 | 832500 | 424 | 00:00:01 | -------------------------------------------------------------------------------------- Predicate Information (identified by operation id): ------------------------------------------ * 2 - access("C"."CUST_ID"="ITEM_1") * 5 - access("P"."PROD_ID"="S"."PROD_ID") * 6 - filter("PROD_MIN_PRICE">100) Notes ----- - This is an adaptive plan Comparison Results (1): ----------------------------- 1. The plans are the same. */

又一个例子,以下2个SQL的唯一区别是NO_MERGE提示:

SELECT /* TEST_BASELINE */ c.cust_city, SUM(s.quantity_sold)
FROM   customers c, sales s, 
       (SELECT prod_id FROM products WHERE prod_min_price>100) p
WHERE  c.cust_id=s.cust_id
AND    p.prod_id=s.prod_id
GROUP BY c.cust_city;

-- 原文中SQL有错误,即少了个别名:p
SELECT /* TEST_SPM */ c.cust_city, SUM(s.quantity_sold)
FROM   customers c, sales s, 
       (SELECT /*+ NO_MERGE */ prod_id FROM products WHERE prod_min_price>100) p
WHERE  c.cust_id=s.cust_id
AND    p.prod_id=s.prod_id
GROUP BY c.cust_city;

/* 将计划导入SQL Plan Baseline */
select signature, sql_handle, sql_text, plan_name, origin, enabled, accepted, fixed, autopurge
from dba_sql_plan_baselines
where
    sql_text like '%/* TEST_BASELINE */%';

variable cnt number;
variable sqlid varchar2(20);
begin
    select distinct sql_id into :sqlid from v$sql
    where sql_text like 'SELECT /* TEST_BASELINE */%';
end;
/
-- 9rpmjttx4grv4
print sqlid;

exec :cnt := dbms_spm.load_plans_from_cursor_cache(sql_id => :sqlid);

-- SQL Handle is: SQL_90b8cd7095b17550
select signature, sql_handle, sql_text, plan_name, origin, enabled, accepted, fixed, autopurge
from dba_sql_plan_baselines
where
    sql_text like 'SELECT /* TEST_BASELINE */%';
    

/* 将计划导入SQL Tuning Set */    
exec sys.dbms_sqltune.create_sqlset( -
    sqlset_name => 'MYSTS1', sqlset_owner => 'SH' -
);

declare
    stscur dbms_sqltune.sqlset_cursor;
begin
    open stscur for
        select value(P)
        from table(
            dbms_sqltune.select_cursor_cache(
                'sql_text like ''SELECT /* TEST_SPM */%''',
                null, null, null, null, null, null, 'ALL'
            )
        ) P;
        
        -- populate the sqlset
        dbms_sqltune.load_sqlset(
            sqlset_name => 'MYSTS1',
            populate_cursor => stscur,
            sqlset_owner => 'SH'
        );
end;
/

-- 3wtcspgk2dg89
select sql_id from dba_sqlset_statements where sqlset_name = 'MYSTS1' and sqlset_owner = 'SH';

VARIABLE v_rep CLOB

BEGIN 
  :v_rep := DBMS_XPLAN.COMPARE_PLANS(
   reference_plan    => spm_object('SQL_90b8cd7095b17550'),
   compare_plan_list => plan_object_list(sqlset_object('SH', 'MYSTS1', '3wtcspgk2dg89', null)),
   type              => 'TEXT',
   level             => 'TYPICAL',
   section           => 'ALL');
END;
/

SET PAGESIZE 50000
SET LONG 100000
SET LINESIZE 210
COLUMN report FORMAT a200
SELECT :v_rep REPORT FROM DUAL;

variable cnt number;
exec :cnt := dbms_spm.drop_sql_plan_baseline('SQL_90b8cd7095b17550');
exec sys.dbms_sqltune.drop_sqlset( -
    sqlset_name => 'MYSTS1', -
    sqlset_owner =>  'SH' -
);

输出的报告如下:


PL/SQL procedure successfully completed.


V_REP
--------------------------------------------------------------------------------

COMPARE PLANS REPORT
----------------------------------------------------------


PL/SQL procedure successfully completed.


REPORT                                                                                                                                                                                                  
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

COMPARE PLANS REPORT
---------------------------------------------------------------------------------------------
  Current user           : SH  
  Total number of plans  : 2   
  Number of findings     : 1   
---------------------------------------------------------------------------------------------

COMPARISON DETAILS
---------------------------------------------------------------------------------------------
 Plan Number            : 1 (Reference Plan)                                    
 Plan Found             : Yes                                                   
 Plan Source            : SQL Plan Baseline                                     
 SQL Handle             : SQL_90b8cd7095b17550                                  
 Plan Name              : SQL_PLAN_91f6df2av2xah6d52967b                        
 Plan Database Version  : 21.0.0.0                                              
 Parsing Schema         : "SH"                                                  
 SQL Text               : SELECT /* TEST_BASELINE */ c.cust_city,               
                        SUM(s.quantity_sold) FROM customers c, sales s, (SELECT 
                        prod_id FROM products WHERE prod_min_price>100) p WHERE 
                        c.cust_id=s.cust_id AND p.prod_id=s.prod_id GROUP BY    
                        c.cust_city                                             

Plan
-----------------------------

 Plan Hash Value  : 3489853474 

--------------------------------------------------------------------------------------
| Id  | Operation                  | Name      | Rows   | Bytes    | Cost | Time     |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT           |           |        |          |  998 |          |
|   1 |   HASH GROUP BY            |           |    620 |    20460 |  998 | 00:00:01 |
| * 2 |    HASH JOIN               |           |   7059 |   232947 |  997 | 00:00:01 |
|   3 |     VIEW                   | VW_GBC_10 |   7059 |   127062 |  573 | 00:00:01 |
|   4 |      HASH GROUP BY         |           |   7059 |   148239 |  573 | 00:00:01 |
| * 5 |       HASH JOIN            |           | 852747 | 17907687 |  530 | 00:00:01 |
| * 6 |        TABLE ACCESS FULL   | PRODUCTS  |     67 |      603 |    3 | 00:00:01 |
|   7 |        PARTITION RANGE ALL |           | 918843 | 11026116 |  522 | 00:00:01 |
|   8 |         TABLE ACCESS FULL  | SALES     | 918843 | 11026116 |  522 | 00:00:01 |
|   9 |     TABLE ACCESS FULL      | CUSTOMERS |  55500 |   832500 |  424 | 00:00:01 |
--------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 2 - access("C"."CUST_ID"="ITEM_1")
* 5 - access("PROD_ID"="S"."PROD_ID")
* 6 - filter("PROD_MIN_PRICE">100)


Notes
-----
- This is an adaptive plan


---------------------------------------------------------------------------------------------
 Plan Number            : 2                                                     
 Plan Found             : Yes                                                   
 Plan Source            : SQL Tuning Set                                        
 SQL Tuning Set Owner   : SH                                                    
 SQL Tuning Set Name    : MYSTS1                                                
 SQL ID                 : 3wtcspgk2dg89                                         
 Plan Hash Value        : 582466584                                             
 Plan Database Version  : 21.0.0.0                                              
 Parsing Schema         : "SH"                                                  
 SQL Text               : SELECT /* TEST_SPM */ c.cust_city,                    
                        SUM(s.quantity_sold) FROM customers c, sales s, (SELECT 
                        /*+ NO_MERGE */ prod_id FROM products WHERE             
                        prod_min_price>100) p WHERE c.cust_id=s.cust_id AND     
                        p.prod_id=s.prod_id GROUP BY c.cust_city                

Plan
-----------------------------

 Plan Hash Value  : 582466584 

-------------------------------------------------------------------------------------
| Id | Operation                  | Name      | Rows   | Bytes    | Cost | Time     |
-------------------------------------------------------------------------------------
|  0 | SELECT STATEMENT           |           |        |          |  999 |          |
|  1 |   HASH GROUP BY            |           |    620 |    20460 |  999 | 00:00:01 |
|  2 |    HASH JOIN               |           |   7059 |   232947 |  997 | 00:00:01 |
|  3 |     VIEW                   | VW_GBC_10 |   7059 |   127062 |  573 | 00:00:01 |
|  4 |      HASH GROUP BY         |           |   7059 |   112944 |  573 | 00:00:01 |
|  5 |       HASH JOIN            |           | 855034 | 13680544 |  530 | 00:00:01 |
|  6 |        VIEW                |           |     67 |      268 |    3 | 00:00:01 |
|  7 |         TABLE ACCESS FULL  | PRODUCTS  |     67 |      603 |    3 | 00:00:01 |
|  8 |        PARTITION RANGE ALL |           | 918843 | 11026116 |  522 | 00:00:01 |
|  9 |         TABLE ACCESS FULL  | SALES     | 918843 | 11026116 |  522 | 00:00:01 |
| 10 |     TABLE ACCESS FULL      | CUSTOMERS |  55500 |   832500 |  424 | 00:00:01 |
-------------------------------------------------------------------------------------

Notes
-----
- This is an adaptive plan


Comparison Results (1):
-----------------------------
 1. Query block SEL$1: Transformation occurred is different: VIEW MERGE in the  
    reference plan (result query block: SEL$F5BB74E1), PLACE GROUP BY in the    
    current plan (result query block: SEL$83F42E9F (line: 1)).                  


---------------------------------------------------------------------------------------------

在此示例中,您将测试索引对查询计划的影响:

EXPLAIN PLAN 
  SET STATEMENT_ID='TST1' FOR 
  SELECT COUNT(*) FROM products WHERE prod_min_price>100;

CREATE INDEX newprodidx ON products(prod_min_price) nologging; 

EXPLAIN PLAN 
  SET STATEMENT_ID='TST2' FOR 
  SELECT COUNT(*) FROM products WHERE prod_min_price>100;
  
VAR v_rep CLOB

BEGIN
  :v_rep := DBMS_XPLAN.COMPARE_PLANS(
    reference_plan    => plan_table_object('SH','PLAN_TABLE','TST1',NULL),
    compare_plan_list => plan_object_list(plan_table_object('SH','PLAN_TABLE','TST2',NULL)),
    TYPE              => 'TEXT',
    level             => 'TYPICAL',
    section           => 'ALL');
END;
/

SET PAGESIZE 50000
SET LONG 100000
SET LINESIZE 210
COLUMN report FORMAT a200
SELECT :v_rep REPORT FROM DUAL;

drop index newprodidx;

输出的报告如下:


PL/SQL procedure successfully completed.


REPORT                                                                                                                                                                                                  
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

COMPARE PLANS REPORT
---------------------------------------------------------------------------------------------
  Current user           : SH  
  Total number of plans  : 2   
  Number of findings     : 1   
---------------------------------------------------------------------------------------------

COMPARISON DETAILS
---------------------------------------------------------------------------------------------
 Plan Number            : 1 (Reference Plan)                                    
 Plan Found             : Yes                                                   
 Plan Source            : Plan Table                                            
 Plan Table Owner       : SH                                                    
 Plan Table Name        : PLAN_TABLE                                            
 Statement ID           : TST1                                                  
 Plan ID                : 19                                                    
 Plan Database Version  : 21.0.0.0                                              
 Parsing Schema         : "SH"                                                  
 SQL Text               : No SQL Text                                           

Plan
-----------------------------

 Plan Hash Value  : 3421487369 

--------------------------------------------------------------------------
| Id  | Operation            | Name     | Rows | Bytes | Cost | Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |          |    1 |     5 |    3 | 00:00:01 |
|   1 |   SORT AGGREGATE     |          |    1 |     5 |      |          |
| * 2 |    TABLE ACCESS FULL | PRODUCTS |   67 |   335 |    3 | 00:00:01 |
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 2 - filter("PROD_MIN_PRICE">100)

---------------------------------------------------------------------------------------------
 Plan Number            : 2                                                     
 Plan Found             : Yes                                                   
 Plan Source            : Plan Table                                            
 Plan Table Owner       : SH                                                    
 Plan Table Name        : PLAN_TABLE                                            
 Statement ID           : TST2                                                  
 Plan ID                : 20                                                    
 Plan Database Version  : 21.0.0.0                                              
 Parsing Schema         : "SH"                                                  
 SQL Text               : No SQL Text                                           

Plan
-----------------------------

 Plan Hash Value  : 2694011010 

---------------------------------------------------------------------------
| Id  | Operation           | Name       | Rows | Bytes | Cost | Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |            |    1 |     5 |    1 | 00:00:01 |
|   1 |   SORT AGGREGATE    |            |    1 |     5 |      |          |
| * 2 |    INDEX RANGE SCAN | NEWPRODIDX |   67 |   335 |    1 | 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 2 - access("PROD_MIN_PRICE">100)


Comparison Results (1):
-----------------------------
 1. Query block SEL$1, Alias "PRODUCTS"@"SEL$1": Access path is different -     
    reference plan: FULL (line: 2), current plan: INDEX (line: 2).              


---------------------------------------------------------------------------------------------

最后2个例子比较复杂,是1对多的比较。以后再说。

原网站

版权声明
本文为[dingdingfish]所创,转载请带上原文链接,感谢
https://xiaoyu.blog.csdn.net/article/details/121151698