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(取到行后再过滤,不走索引)是判断索引是否被有效利用的关键。
三、从缩进还原执行计划树
执行计划本质是一棵树:数据从叶子节点向上流动,根节点是最终结果。缩进越深,越靠近叶子。手工绘制时遵循三条规则:
- 按 Id 顺序扫描,缩进增加表示进入子节点,缩进减少表示回到父节点。
- 父节点是子节点的消费者,子节点先执行,结果传给父节点。
- 同一父节点下的多个子节点,按 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 行,作为构建侧放在左侧。
四、谓词、成本与基数的联动阅读
单独看某一列容易误判,要把它们串起来。例如某步骤 E-Rows=1 但 A-Rows=5000,说明优化器严重低估了返回行数,可能因此选择了嵌套循环而非哈希连接,导致内层被驱动 5000 次。此时应检查该表统计信息是否过期、是否有函数包裹列导致索引失效、绑定变量是否引起选择率偏差。
再看成本:Cost 是优化器基于系统统计(单块读耗时、多块读耗时、CPU 速度)和对象统计(行数、块数、列分布)算出的相对值,不是毫秒。比较两个计划的 Cost 只在同一环境、同一优化器模式下有意义。真正判断性能要看 A-Time 和 Buffers。
五、手工绘制执行计划树的练习方法
拿一份真实输出,按以下步骤在纸上或白板上画:
- 把每行写成一个节点卡片,标注 Id、Operation、Name、E-Rows、A-Rows。
- 从 Id=0 开始,遇到缩进增加就向下连线,缩进减少就回到上一层。
- 同一父节点下的多个子节点,按 Id 升序从左到右摆放。
- 在每条边上标注传递的行数(可用 A-Rows),观察数据量在哪一步骤膨胀或收缩。
- 用红笔圈出 E-Rows 与 A-Rows 偏差超过一个数量级的节点,这些就是后续优化的重点。