2. 执行计划阅读基础

上一节我们知道了执行计划是优化器给出的“执行配方”,本节要解决的是:拿到一份 DBMS_XPLAN 输出后,如何逐列读懂它,并把这张表格还原成一棵可以手工绘制的执行计划树。这是后续判断“优化器选错路径”还是“统计信息失真”的基本功。

一、先拿到一份可读的执行计划

最常用的入口是 DBMS_XPLAN.DISPLAY_CURSOR,它读取游标缓存中的真实执行统计,比 EXPLAIN PLAN FOR 更贴近实际运行。典型调用如下:

-- 先执行目标 SQL,让游标进入缓存
SELECT /*+ gather_plan_statistics */ o.order_id, o.customer_id, o.amount
FROM   orders o
WHERE  o.customer_id = 1024
AND    o.order_date >= DATE '2024-01-01';

-- 再查看带运行时统计的执行计划
SELECT * FROM TABLE(
  DBMS_XPLAN.DISPLAY_CURSOR(
    sql_id      => 'a1b2c3d4e5f6g',
    cursor_child_no => 0,
    format      => 'ALLSTATS LAST +COST +BYTES +PREDICATE +ALIAS'
  )
);

这里 format 参数决定了显示的信息量。BASIC 只给操作符和对象名;TYPICAL(默认)加上成本、基数、字节数;ALLSTATS LAST 追加 A-Rows、A-Time、Buffers 等实际执行数据;+PREDICATE 显示谓词过滤位置;+ALIAS 显示查询块别名,便于定位子查询。建议日常阅读至少使用 ALLSTATS LAST +PREDICATE。

二、逐列读懂输出

一份典型输出如下(示例数据,非实测):

| Id | Operation                    | Name         | Starts | E-Rows | A-Rows | A-Time     | Buffers | Cost (%CPU) |
|----|------------------------------|--------------|--------|--------|--------|------------|---------|-------------|
|  0 | SELECT STATEMENT             |              |      1 |        |     12 | 00:00:00.02|      45 |     8 (13)  |
|  1 |  TABLE ACCESS BY INDEX ROWID | ORDERS       |      1 |     12 |     12 | 00:00:00.02|      45 |     8 (13)  |
|* 2 |   INDEX RANGE SCAN           | IDX_ORD_CUST |      1 |     12 |     12 | 00:00:00.01|       3 |     3 (0)   |

关键列含义:

  • Id:行号,配合缩进表示层级,是还原执行计划树的骨架。
  • Operation:操作符,如 TABLE ACCESS、INDEX RANGE SCAN、HASH JOIN、SORT ORDER BY,决定这一步“做什么”。
  • Name:操作对象,表名、索引名或视图名,决定“对谁做”。
  • Starts:该操作被启动的次数。嵌套循环内层若 Starts 很大,说明被反复驱动。
  • E-Rows:优化器估算的返回行数(基数),是成本计算的输入。
  • A-Rows:实际返回行数。E-Rows 与 A-Rows 差距大,往往意味着统计信息或谓词选择率估算有问题。
  • A-Time:该步骤累计耗时,用于定位真正的热点。
  • Buffers:逻辑读块数,衡量 I/O 代价。
  • Cost (%CPU):优化器估算成本,括号内是 CPU 占比。

注意 Id 前带 * 表示该行有谓词被下推或过滤,具体内容由 +PREDICATE 段落给出,例如 filter("O"."CUSTOMER_ID"=1024) 或 access("O"."ORDER_DATE">=...)。区分 access(用于定位数据,能走索引)与 filter(取到行后再过滤,不走索引)是判断索引是否被有效利用的关键。

三、从缩进还原执行计划树

执行计划本质是一棵树:数据从叶子节点向上流动,根节点是最终结果。缩进越深,越靠近叶子。手工绘制时遵循三条规则:

  1. 按 Id 顺序扫描,缩进增加表示进入子节点,缩进减少表示回到父节点。
  2. 父节点是子节点的消费者,子节点先执行,结果传给父节点。
  3. 同一父节点下的多个子节点,按 Id 从小到大从左到右排列,先执行的放左边。

以两表连接为例:

| Id | Operation          | Name    | E-Rows |
|----|--------------------|---------|--------|
|  0 | SELECT STATEMENT   |         |    100 |
|  1 |  HASH JOIN         |         |    100 |
|  2 |   TABLE ACCESS FULL| CUSTOMER|     20 |
|  3 |   TABLE ACCESS FULL| ORDERS  |   5000 |

Id=1 是 HASH JOIN,缩进为 1;Id=2、3 缩进为 2,是它的两个子节点。因此树形为:根 SELECT STATEMENT → HASH JOIN → 左子 CUSTOMER 全表扫描、右子 ORDERS 全表扫描。哈希连接通常以较小的行源构建哈希表,这里 CUSTOMER 估算 20 行,作为构建侧放在左侧。

执行计划树还原(数据自叶向根流动) Id 0 SELECT STATEMENT Cost 8 E-Rows 100 Id 1 HASH JOIN E-Rows 100 A-Rows 100 Id 2 TABLE ACCESS FULL CUSTOMER E-Rows 20 构建侧(较小行源) Id 3 TABLE ACCESS FULL ORDERS E-Rows 5000 探测侧(较大行源) 行源汇入 读图要点 • 缩进 = 层级,Id 越小越靠近根;子节点先执行,结果向上传递 • E-Rows 与 A-Rows 偏差大 → 检查统计信息与谓词选择率;Buffers 高 → 关注 I/O 热点

四、谓词、成本与基数的联动阅读

单独看某一列容易误判,要把它们串起来。例如某步骤 E-Rows=1 但 A-Rows=5000,说明优化器严重低估了返回行数,可能因此选择了嵌套循环而非哈希连接,导致内层被驱动 5000 次。此时应检查该表统计信息是否过期、是否有函数包裹列导致索引失效、绑定变量是否引起选择率偏差。

再看成本:Cost 是优化器基于系统统计(单块读耗时、多块读耗时、CPU 速度)和对象统计(行数、块数、列分布)算出的相对值,不是毫秒。比较两个计划的 Cost 只在同一环境、同一优化器模式下有意义。真正判断性能要看 A-Time 和 Buffers。

阅读顺序建议:先看根节点确认语句类型,再沿缩进找最深的叶子节点(通常是表或索引访问),然后自下而上核对每一步的 E-Rows 与 A-Rows 是否吻合,最后看 A-Time 和 Buffers 定位热点。谓词段落用来确认过滤发生在哪一层。

五、手工绘制执行计划树的练习方法

拿一份真实输出,按以下步骤在纸上或白板上画:

  1. 把每行写成一个节点卡片,标注 Id、Operation、Name、E-Rows、A-Rows。
  2. 从 Id=0 开始,遇到缩进增加就向下连线,缩进减少就回到上一层。
  3. 同一父节点下的多个子节点,按 Id 升序从左到右摆放。
  4. 在每条边上标注传递的行数(可用 A-Rows),观察数据量在哪一步骤膨胀或收缩。
  5. 用红笔圈出 E-Rows 与 A-Rows 偏差超过一个数量级的节点,这些就是后续优化的重点。