当前位置:首页 > 问答 > 正文

Oracle里用EXPLAIN PLAN看看SQL执行计划,帮你理解语句到底咋跑的分析示范

参考了Oracle官方文档中关于SQL调优和EXPLAIN PLAN的章节,以及多位资深DBA如Thomas Kyte在其著作和论坛讨论中强调的实践方法)

想象一下,你让一个朋友去一个陌生的城市帮你办几件事,比如先去银行取钱,再去邮局寄包裹,最后去超市买东西,他回来之后,你肯定会问他:“你是怎么走的?先去了哪里?路上顺利吗?有没有堵车?” 你问的这个问题,就有点像数据库里的“执行计划”,SQL语句就是你给数据库下达的命令,而执行计划就是数据库这个“朋友”告诉你,它打算如何一步一步地去完成这个任务。

在Oracle数据库里,EXPLAIN PLAN 就是这个让你能提前知道数据库“行动路线”的工具,你不用真的让SQL语句跑完(尤其是那些可能要跑几个小时的大语句),就能先看看数据库打算怎么执行它,从而判断这个方法是不是高效,有没有走冤枉路。

下面我们就来一步步看看怎么用这个工具,以及怎么看懂它输出的“路线图”。

第一步:创建一张“计划表”

我们需要一张桌子来记录数据库告诉我们的计划,Oracle通常已经为你准备好了一张名为 PLAN_TABLE 的表,你可以检查一下你的数据库用户下有没有这张表,如果没有,也很简单,Oracle提供了一个现成的脚本来创建它,这个脚本通常叫 utlxplan.sql,位于Oracle软件的安装目录下,你只需要以有权限的用户登录数据库,然后执行这个脚本就行了,绝大多数情况下,这张表是已经存在的。

第二步:让数据库生成计划

Oracle里用EXPLAIN PLAN看看SQL执行计划,帮你理解语句到底咋跑的分析示范

我们有一条想分析的SQL语句, SELECT e.employee_name, d.department_name FROM employees e, departments d WHERE e.department_id = d.department_id AND e.salary > 5000;

我们要让数据库为这条语句生成执行计划,并把它写入到 PLAN_TABLE 中,命令非常简单: EXPLAIN PLAN FOR SELECT e.employee_name, d.department_name FROM employees e, departments d WHERE e.department_id = d.department_id AND e.salary > 5000;

执行这条命令后,数据库并没有真正去查询数据,它只是静静地分析了一下语句,然后把自己的“作战方案”详细地记录在了 PLAN_TABLE 表里。

第三步:查看“路线图”

计划已经生成了,但它存储在表里,是一堆原始数据,可读性很差,这时候我们需要一个“翻译官”来把计划表里的内容以一种容易理解的方式展示给我们看,最常用的工具就是 Oracle 自带的 DBMS_XPLAN 包。

你只需要执行: SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

Oracle里用EXPLAIN PLAN看看SQL执行计划,帮你理解语句到底咋跑的分析示范

这条命令会从 PLAN_TABLE 中读出最近一次生成的执行计划,并以一个清晰的树形结构显示出来,我们来重点学习怎么看懂这个输出结果。

解读执行计划的核心要点

执行计划的输出通常有好几列,我们盯住几个最关键的就够了:

  1. Id & Operation (操作 ID 和操作类型):这是执行计划的骨架,告诉你数据库要执行哪些操作。TABLE ACCESS FULL(全表扫描)、INDEX RANGE SCAN(索引范围扫描)、NESTED LOOPS(嵌套循环连接)、HASH JOIN(哈希连接)等,这些名字非常直观,FULL 就是读取整个表,INDEX SCAN 就是去查索引。

  2. Rows (基数估计):这一列是数据库预估的每一步操作会返回多少行数据,这个数字非常重要!因为数据库优化器就像是公司的调度员,它根据这个预估的行数来决定走哪条路最快,如果这个预估值和实际情况差得太远(比如它以为只有10行,结果实际有100万行),那它很可能就制定了一个非常糟糕的计划,看到执行计划,第一眼就可以看看这个 Rows 的预估是否合理。

  3. Cost (成本):这是一个相对的、无量纲的数字,代表了数据库认为执行这一步操作的“代价”有多大,代价考虑了CPU、磁盘I/O等因素,我们会希望整个计划的总代价越小越好,但要注意,Cost 是用来比较不同计划孰优孰劣的,它本身绝对值的大小没有直接意义。

    Oracle里用EXPLAIN PLAN看看SQL执行计划,帮你理解语句到底咋跑的分析示范

  4. Access Predicates 和 Filter Predicates (访问条件和过滤条件):这能帮你理解数据是如何被筛选的。Access Predicates 是那些用于快速定位数据的原则(比如通过索引直接找到某几行),而 Filter Predicates 是等数据拿出来之后再进行的过滤。

让我们模拟一个简单的计划输出分析:

假设上面那条连接查询的计划输出可能是这样的:

-----------------------------------------------------------------------------------
| Id  | Operation          | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |             |   100 |  3900 |     6  (17)| 00:00:01 |
|   1 |  NESTED LOOPS      |             |   100 |  3900 |     6  (17)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| EMPLOYEES   |    10 |   190 |     3   (0)| 00:00:01 |
|*  3 |   TABLE ACCESS FULL| DEPARTMENTS |    10 |   200 |     3   (0)| 00:00:01 |
-----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   3 - filter("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")

怎么读这个计划?

  • 执行顺序是从内层(Id大的)向外层(Id小的)看,如果缩进相同,则从上往下看,所以数据库的实际执行步骤是:Id 2 -> Id 3 -> Id 1 -> Id 0。
  • Id 2: 它对 EMPLOYEES 表进行了一次全表扫描 (TABLE ACCESS FULL),预估会返回10行满足 salary > 5000 条件的数据。
  • Id 3: 对于从Id 2步骤中获取的每一行员工数据,数据库都去 DEPARTMENTS 表里做一次全表扫描,目的是为了找到与员工部门ID相匹配的部门名称,注意看下面的 Predicate Information,这里说明了连接条件 e.department_id = d.department_id 是在这一步作为过滤条件应用的。
  • Id 1: 这是 NESTED LOOPS(嵌套循环)连接操作,你可以把它想象成一个双循环:对于第一个表(员工表)的每一行,都去第二个表(部门表)里循环一遍找匹配的行,这种连接方式在驱动集(员工表)很小的时候效率很高。
  • Id 0: SELECT STATEMENT 表示最终的结果集被返回给用户。

从这个计划我们能学到什么?

这个计划看起来简单,但已经能给我们一些启示。EMPLOYEES 表非常大,但高薪员工(salary > 5000)确实只有几十个,那么这个计划是高效的,因为外层循环次数很少,如果 DEPARTMENTS 表很大,对它的每次全表扫描代价就会很高,这时,我们可能会想:能不能在 DEPARTMENTS 表的 department_id 上建个索引?这样,第二步就可以从全表扫描变成高效的索引扫描了。

通过这个简单的例子,你应该能体会到 EXPLAIN PLAN 的强大之处,它就像给你的SQL语句做了一次“X光检查”,让你能洞察数据库内部的执行逻辑,从而为优化SQL性能提供最直接的依据,核心就是看懂它的“行动路线”(Operation)、评估它的“工作量预估”(Rows)、并思考是否有更优的路径(比如使用索引)。