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:按后序遍历确定执行顺序
- 访问左子树:先执行
TABLE ACCESS FULL EMPLOYEES(扫描员工表)。 - 访问右子树:先执行
INDEX UNIQUE SCAN DEPT_ID_PK(获取部门ID对应的ROWID),再执行TABLE ACCESS BY INDEX ROWID DEPARTMENTS(根据ROWID回表取部门信息)。 - 最后执行父节点
NESTED LOOPS,将员工的每一行与匹配的部门行连接。 - 最后执行根节点
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):
- 先遍历节点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的左子树(节点7),继续:
- 访问节点6自身:执行 6(SORT JOIN)
- 节点3的左子树(节点4),继续:
- 访问节点3自身:执行 3(MERGE JOIN)
- 节点2的右子树(节点9):执行 9(INDEX UNIQUE SCAN DEPT_ID_PK)
- 节点2的左子树(节点3),继续:
- 访问节点2自身:执行 2(NESTED LOOPS)
- 节点1的右子树(节点10):执行 10(TABLE ACCESS BY INDEX ROWID DEPARTMENTS)
- 节点1的左子树(节点2),继续:
- 访问节点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 数据库首先通过将哈希函数应用于由该语句指定的簇键值,以获得哈希值。然后扫描包含具有此哈希值的行的数据块。