Oracle 执行计划的阅读方法(二叉树结构来理解和阅读)

Oracle 执行计划的阅读方法(二叉树结构来理解和阅读)

一、概述

Oracle输出的执行计划一般都是纯文本缩进格式,层级关系靠空格缩进来体现。当 SQL 涉及多表连接、子查询嵌套时,缩进层级变深,仅靠肉眼很难判断"谁先执行、谁后执行"。

将执行计划以二叉树图形结构表示后,每个操作的父子关系、兄弟关系一目了然,执行顺序也变成了标准的二叉树遍历问题,非常容易理解和阅读!

而且,此方法(二叉树图形结构理解和阅读执行计划),适用于大部分数据库的执行计划理解和阅读

二、构建二叉树的核心原则

Oracle 输出的执行计划本质上是一棵缩进树,每一行是一个节点,缩进深度决定了它在树中的层级。

我们将文本形式的执行计划(通常以缩进表示层次)转换为二叉树的确定规则如下:

序号 原则 解释
1 自顶而下确定层次 从根节点(最外层操作,如SELECT STATEMENT)开始,向下逐层展开子节点。缩进越深的节点,层级越低。
2 父节点通常只有两个直接子节点 实际计划中,极少情况下,同一个父节点可能有多个子操作(如UNION-ALL),但我们将它们按执行顺序两两合并为左-右 结构;若只有一个子节点,则该子节点作为左子节点,右子节点为空。
3 每个节点只有一个父节点 除根节点外,每个节点有且仅有一个父节点。
4 同一缩进层级中,按执行顺序确定左右 对于同一个父节点的多个直接子节点,先执行的放左边(左子节点),后执行的放右边(右子节点)。在文本计划中,通常先出现的子操作即为先执行者。
5 递归推导 对每个子节点,重复上述规则,直到所有叶子节点(如表访问、索引扫描)处理完毕,即得到完整的二叉树。

注:实际上,执行计划中父节点(如NESTED LOOPS)的左子节点通常是驱动表 (先被访问),右子节点是被驱动表(后访问),这正好对应了先左后右的执行顺序。

三、二叉树遍历阅读顺序(执行顺序)

二叉树构建完成后,执行顺序就是一个标准的后序遍历问题。规则极其简单,只需记住一句话:

左 → 右 → 根,递归执行。

展开来说:

  • 先走完左子树:从根节点的左子节点出发,递归地"左 → 右 → 根",直到叶子节点。
  • 再走完右子树:左子树全部执行完毕后,从根节点的右子节点出发,同样递归地"左 → 右 → 根"。
  • 最后执行当前节点本身:左右子树都执行完毕后,才执行当前节点。

具体到执行计划:

  • 左子节点 对应的操作(如全表扫描驱动表)先执行,产生中间结果集。
  • 右子节点 对应的操作(如索引扫描被驱动表)后执行,通常依赖左子节点的输出。
  • 父节点 (如连接、排序、聚合等)最后执行,处理左右子节点的输出,生成最终结果。

四、案例一

假设有如下执行计划文本(简化):

复制代码
SELECT STATEMENT
  NESTED LOOPS
    TABLE ACCESS FULL EMPLOYEES
    TABLE ACCESS BY INDEX ROWID DEPARTMENTS
      INDEX UNIQUE SCAN DEPT_ID_PK
步骤1:确定根节点

根节点是SELECT STATEMENT(最外层)。

步骤2:逐层构建
  • 根节点只有一个直接子节点:NESTED LOOPS → 作为左子节点(右子节点为空)。
  • NESTED LOOPS有两个直接子节点(缩进相同):
    • TABLE ACCESS FULL EMPLOYEES(先出现)→ 左子节点
    • TABLE ACCESS BY INDEX ROWID DEPARTMENTS(后出现)→ 右子节点
  • 右子节点TABLE ACCESS BY INDEX ROWID DEPARTMENTS有一个直接子节点:INDEX UNIQUE SCAN DEPT_ID_PK → 作为其左子节点(右为空)。
步骤3:形成二叉树结构(示意)
复制代码
         SELECT STATEMENT
              |
          NESTED LOOPS
          /          \
    FULL扫描(EMP)  ROWID扫描(DEPT)
                      |
                  INDEX UNIQUE
步骤4:按后序遍历确定执行顺序
  1. 访问左子树:先执行TABLE ACCESS FULL EMPLOYEES(扫描员工表)。
  2. 访问右子树:先执行INDEX UNIQUE SCAN DEPT_ID_PK(获取部门ID对应的ROWID),再执行TABLE ACCESS BY INDEX ROWID DEPARTMENTS(根据ROWID回表取部门信息)。
  3. 最后执行父节点NESTED LOOPS,将员工的每一行与匹配的部门行连接。
  4. 最后执行根节点SELECT STATEMENT,返回最终结果。

四、案例二

如下执行语句和执行计划

复制代码
SQL> set autotrace traceonly;
SQL> set linesize 200 pagesize 999
SQL> SELECT e.last_name, j.job_title, d.department_name 
  2  FROM hr.employees e, hr.departments d, hr.jobs j
  3  WHERE e.department_id = d.department_id
  4  AND e.job_id = j.job_id
  5  AND e.last_name LIKE 'A%' ;
Execution Plan
----------------------------------------------------------
Plan hash value: 293711507
-----------------------------------------------------------------------------------------------
| Id  | Operation                       | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                |             |     3 |   189 |     8  (13)| 00:00:01 |
|   1 |  NESTED LOOPS                   |             |     3 |   189 |     8  (13)| 00:00:01 |
|   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_NAME_IX |     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"."LAST_NAME" LIKE 'A%')
       filter("E"."LAST_NAME" LIKE 'A%')
   9 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
步骤1:构建二叉树(依据缩进与执行顺序)

从根节点开始,逐层确定父子关系与左右分支:

复制代码
                         [0] SELECT STATEMENT
                              |
                         [1] NESTED LOOPS
                        /                \
                   [2] NESTED LOOPS      [10] TABLE ACCESS BY INDEX ROWID (DEPARTMENTS)
                  /             \
             [3] MERGE JOIN    [9] INDEX UNIQUE SCAN (DEPT_ID_PK)
            /           \
      [4] TABLE ACCESS  [6] SORT JOIN
       (JOBS)           /          
      /                [7] TABLE ACCESS BY INDEX ROWID (EMPLOYEES)
 [5] INDEX FULL SCAN   /
 (JOB_ID_PK)      [8] INDEX RANGE SCAN (EMP_NAME_IX)

构建原则:

  • 同一父节点下,先出现的子操作作为左子节点 ,后出现的作为右子节点(例如节点1下,节点2先出现,节点10后出现)。
  • 每个节点只有一个父节点,根节点(0)无父节点。
  • 若操作只有一个子节点,则作为左子节点(如节点4的左子是5,节点6的左子是7,节点7的左子是8)。

步骤2:后序遍历确定实际执行顺序

后序遍历规则:左子树 → 右子树 → 父节点 。按此顺序遍历上述二叉树,得到如下执行序列(数字代表Id):

  1. 先遍历节点0的左子树(节点1),继续深入:
    • 节点1的左子树(节点2),继续:
      • 节点2的左子树(节点3),继续:
        • 节点3的左子树(节点4),继续:
          • 节点4的左子树(节点5):执行 5(INDEX FULL SCAN JOB_ID_PK)
        • 访问节点4自身:执行 4(TABLE ACCESS BY INDEX ROWID JOBS)
        • 节点3的右子树(节点6),继续:
          • 节点6的左子树(节点7),继续:
            • 节点7的左子树(节点8):执行 8(INDEX RANGE SCAN EMP_NAME_IX)
          • 访问节点7自身:执行 7(TABLE ACCESS BY INDEX ROWID EMPLOYEES)
        • 访问节点6自身:执行 6(SORT JOIN)
      • 访问节点3自身:执行 3(MERGE JOIN)
      • 节点2的右子树(节点9):执行 9(INDEX UNIQUE SCAN DEPT_ID_PK)
    • 访问节点2自身:执行 2(NESTED LOOPS)
    • 节点1的右子树(节点10):执行 10(TABLE ACCESS BY INDEX ROWID DEPARTMENTS)
  • 访问节点1自身:执行 1(NESTED LOOPS)
  • 最后访问根节点:执行 0(SELECT STATEMENT)

五、执行计划中每个节点的理解

我们知道SQL语言是一种结构化语言,结构化语言也可以理解为是一种集合语言(多行集合为单位,来进行处理的语言);

执行计划中每个节点(二叉树)都可以看作一个集合,每个节点(执行计划)要么从数据库中检索行,要么接受一个或多个集合(行源)中的行作为输入。

那么,SQL引擎中每个节点的集合,可以分为两种处理类型:

1、获取数据集合的节点,物理地从数据库中检索对象的数据(获取集合的初始数据),这些步骤统称为访问路径节点(或者访问路径计划)

2、操作数据集合的节点(例如各个表的连接方法嵌套、哈希、merge;筛选、 排序、或聚合等等),并且该节点具有迭代控制功能

在某些执行计划中的步骤是迭代的,例如SQL 引擎多次在索引、表中迭代来获取最终数据;嵌套连接在被驱动表中多次迭代访问来获取连接结果集,等等。

各个步骤的执行计划最终组合成一个整体的执行计划,提供给SQL引擎执行,具有如下特点:

1、每个节点(执行计划)返回一个数据行集合。

2、该数据行集合中的行可以在下一步被使用,或在最后一步返回给发出 SQL 语句的应用程序。

2、操作数据集合的节点, 接收子节点执行计划中所返回的数据行集合,并且该节点具有迭代控制(数据行集合)的功能

4、行数据集合可以是表、 视图、或联接操作或分组操作的结果。

案例说明

复制代码
SELECT STATEMENT
  NESTED LOOPS
    TABLE ACCESS FULL EMPLOYEES
    TABLE ACCESS BY INDEX ROWID DEPARTMENTS
      INDEX UNIQUE SCAN DEPT_ID_PK
      
SELECT STATEMENT
          |
      NESTED LOOPS
      /          \
FULL扫描(EMP)  ROWID扫描(DEPT)
                  |
              INDEX UNIQUE

FULL扫描(EMP)      -->获取数据集合的节点(访问路径节点),物理扫描EMP表中的所有数据
INDEX UNIQUE      -->获取数据集合的节点(访问路径节点),物理扫描DEPT_ID_PK索引中的所有数据
ROWID扫描(DEPT)    -->获取数据集合的节点(访问路径节点),通过子节点物理索引扫描中获取的rowid,来物理访问DEPT表中的数据

NESTED LOOPS      -->操作数据集合的节点,执行一个NESTED LOOPS联接,它将FULL扫描(EMP)节点获取的数据,作为驱动表,驱动访问ROWID扫描(DEPT)获取的数据集合,将每一行进行相应的行连接,并将结果行返回给父节点
SELECT STATEMENT  ->操作数据集合的节点,将子节点获取的结果,返回给发出 SQL 语句的应用程序

六、访问路径简单补充

访问路径是从数据库检索数据的方式。例如,使用索引的查询与不使用索引的查询具有不同的访问路径。

从表中检索数据的访问路径主要如下几种:

全表扫描

这种类型的扫描从一个表读取所有行,并滤掉那些不符合选择条件的行。数据库顺序扫描段中的所有数据块,包括那些高水位标记以下的块,高水位标记用来分隔已使用和未使用的空间 。

Rowid 扫描

行的 rowid 指定包含行的数据文件和数据块,以及行在该块中的位置。数据库通过语句的 WHERE 子句或一个索引扫描,首先获取所选的行的 rowids,然后基于这些 rowid 查找每个选定的行。

索引扫描

扫描被 SQL 语句访问的索引列的值。如果该语句仅访问已被索引的列,则数据库直接从索引读取索引的列值。

簇扫描

簇扫描用来检索存储在一个索引表簇中的一个表中的数据,具有相同的簇键的所有行都存储在同一个数据块中 。Oracle 数据库首先通过扫描簇索引来获取所选行的rowid。然后基于此 rowid 查找相应行。

哈希扫描

哈希扫描用于查找哈希群集中的行,其中具有相同哈希值的所有行都存储在同一个数据块中 。Oracle 数据库首先通过将哈希函数应用于由该语句指定的簇键值,以获得哈希值。然后扫描包含具有此哈希值的行的数据块。

相关推荐
SelectDB1 小时前
Doris 向量检索上线踩坑记录:七个排查现场与可直接复制的命令
大数据·数据库·数据分析
数据库小学妹1 小时前
MySQL深分页优化:LIMIT大偏移的根因分析与五种解法对比
数据库·mysql·性能优化
SelectDB1 小时前
一条大查询把集群打挂之后:Apache Doris 稳定性处置与资源隔离速查
大数据·数据库·数据分析
这个DBA有点耶1 小时前
数据库部署架构深潜:集中式、分布式、云原生的技术原理、演进逻辑与选型框架
数据库·架构
鸽芷咕1 小时前
MySQL 数据库管理工具用惯了?切金仓数据库,这篇帮你把家伙事儿配齐
数据库
这个DBA有点耶1 小时前
从异步复制到MGR:MySQL复制机制的三层演进与选型框架
数据库·mysql·代码规范
SelectDB1 小时前
把 JSON 埋点表迁到 Apache Doris VARIANT:一次完整排错记录
大数据·数据库·数据分析
小林ixn1 小时前
用 SQLite + 大模型做一个 Text2SQL 小助手:从建表到自然语言查询的完整实战
数据库·sqlite