当前位置: 首页 > ai >正文

Oracle SQL 性能调优的基石:深入解读与驾驭执行计划

在浩瀚的数据海洋中,Oracle 数据库如同一位强大的舵手,承载着企业的核心业务航行。而 SQL 语句,则是我们向这位舵手下达的指令。有时,指令清晰明确,航程一帆风顺;有时,指令却含混不清,导致航船在数据的惊涛骇浪中艰难前行,性能瓶颈随之而来。如何洞察指令被执行的每一个细节,找出慢的原因?答案就在于揭开 执行计划(Execution Plan) 的神秘面纱。

执行计划是 Oracle SQL 性能调优工作中最核心、最强大的工具,没有之一。它不仅是诊断问题的“CT 扫描仪”,更是优化工作的“导航图”。本篇博客将带你从零开始,深入浅出地全面掌握 Oracle 执行计划的获取、解读与调优实践,助你成为一名真正的数据库性能侦探。

第一部分:初识执行计划——数据库的“作战地图”

1.1 什么是执行计划?

简单来说,执行计划就是 Oracle 数据库优化器(Cost-Based Optimizer, CBO)为执行一条 SQL 语句而制定的一套详尽的“作战方案”。它精确地描述了为了得到最终的查询结果,数据库需要经历哪些步骤,每个步骤具体做什么,以及它们之间的先后顺序和依赖关系。

这个方案回答了以下几个关键问题:

  • 数据从哪里取? 是从表中直接读取(全表扫描),还是通过索引间接查找(索引扫描)?

  • 多个表如何连接? 是用像两层循环一样的嵌套循环,还是像制作哈希表一样高效的哈希连接?

  • 数据如何排序和汇总? 是否需要显式的排序操作,能否利用索引的有序性?

  • 每一步的成本是多少? 优化器基于统计信息预估的每个操作的 CPU、I/O 开销是多少?

1.2 为什么执行计划至关重要?

理解执行计划的重要性是进行性能调优的第一步。

  1. 性能瓶颈定位的“显微镜”:当一条 SQL 运行缓慢时,猜测是徒劳的。执行计划能让你精确地看到时间主要消耗在哪个环节。是某个全表扫描处理了上亿条数据?还是一个哈希连接耗尽了内存?执行计划一目了然。

  2. 理解优化器思维的“窗口”:优化器为何选择了 A 索引而忽略了 B 索引?为何用了嵌套循环而不是哈希连接?通过执行计划,你可以窥见优化器的决策过程。如果它的选择是错误的,那可能就是问题的根源(例如,统计信息过时)。

  3. 所有调优手段的“效果验金石”:无论是添加索引、改写 SQL、使用 Hint 还是刷新统计信息,其最终效果都必须通过对比调优前后的执行计划来验证。执行计划的优化是性能提升最直接的体现。

第二部分:获取执行计划——掌握两大核心方法

获取执行计划主要有两种方式:预估执行计划实际执行计划。理解两者的区别至关重要。

2.1 预估执行计划 (EXPLAIN PLAN FOR)

这种方式不会真正执行 SQL 语句,它只是让优化器根据当前数据字典中的统计信息,模拟生成一个它认为最优的执行计划。

使用方法:

-- 1. 使用 EXPLAIN PLAN 命令将计划写入默认的计划表(PLAN_TABLE$)
EXPLAIN PLAN FOR
SELECT e.last_name, e.salary, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE e.salary > 10000;-- 2. 使用 DBMS_XPLAN.DISPLAY 函数格式化查询计划表
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY());

输出示例与分析:

--------------------------------------------------------------------------------------------
| Id  | Operation                    | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |             |    10 |   690 |     4  (25)| 00:00:01 |
|   1 |  NESTED LOOPS                |             |    10 |   690 |     4  (25)| 00:00:01 |
|   2 |   NESTED LOOPS               |             |    10 |   690 |     4  (25)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL         | EMPLOYEES   |    10 |   370 |     3   (0)| 00:00:01 |
|*  4 |    INDEX UNIQUE SCAN         | DEPT_ID_PK  |     1 |       |     0   (0)| 00:00:01 |
|   5 |   TABLE ACCESS BY INDEX ROWID| DEPARTMENTS |     1 |    32 |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------3 - filter("E"."SALARY">10000)4 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
  • Id: 操作的序列号,执行顺序从内到外,从上到下。Id=3 最先执行。

  • Operation: 关键操作类型。本例中出现了 TABLE ACCESS FULL(全表扫描)、INDEX UNIQUE SCAN(唯一索引扫描)和 NESTED LOOPS(嵌套循环连接)。

  • Name: 操作对象,如表名 EMPLOYEES 或索引名 DEPT_ID_PK

  • Rows: 优化器预估该步骤将返回的行数。这里是它猜的。

  • Cost (%CPU): 优化器预估的该步骤的相对成本,以及 CPU 开销的占比。这是一个无量纲的值,用于比较不同计划的优劣。

优点:简单快捷,不产生实际执行开销,适合在测试环境分析大型查询。
缺点:由于是“预估”,它可能和实际运行的计划不符(如果统计信息不准),并且缺少运行时真实的资源消耗数据。

2.2 实际执行计划 (DBMS_XPLAN.DISPLAY_CURSOR) - 强烈推荐

这是生产环境调优的首选方法。它从库缓存(Library Cache)中抓取刚刚被实际执行的 SQL 语句的真实计划,并包含丰富的运行时统计信息。

使用方法:

-- 首先,在执行SQL时添加提示,让其收集详细的执行统计信息
SELECT /*+ GATHER_PLAN_STATISTICS */ e.last_name, e.salary, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE e.salary > 10000;-- 然后,使用DBMS_XPLAN.DISPLAY_CURSOR查看刚刚执行语句的计划
SELECT t.*
FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(sql_id => NULL, -- NULL 表示获取上一条语句的信息format => 'ALLSTATS LAST' -- 关键:显示所有实际统计信息)
) t;

输出示例与分析:

SQL_ID  8z5h5qwgvuvuv, child number 0
-------------------------------------
SELECT /*+ GATHER_PLAN_STATISTICS */ e.last_name, e.salary,
d.department_name FROM employees e JOIN departments d ON
e.department_id = d.department_id WHERE e.salary > 10000Plan hash value: 1343509718----------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name        | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |  OMem |  1Mem | Used-Mem |
----------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |             |      1 |        |      5 |00:00:00.01 |      12 |      1 |       |       |          |
|   1 |  NESTED LOOPS                |             |      1 |      5 |      5 |00:00:00.01 |      12 |      1 |       |       |          |
|   2 |   NESTED LOOPS               |             |      1 |     10 |      5 |00:00:00.01 |       7 |      1 |       |       |          |
|*  3 |    TABLE ACCESS FULL         | EMPLOYEES   |      1 |     10 |      5 |00:00:00.01 |       5 |      1 |       |       |          |
|*  4 |    INDEX UNIQUE SCAN         | DEPT_ID_PK  |      5 |      1 |      5 |00:00:00.01 |       2 |      0 |       |       |          |
|   5 |   TABLE ACCESS BY INDEX ROWID| DEPARTMENTS |      5 |      1 |      5 |00:00:00.01 |       5 |      0 |       |       |          |
----------------------------------------------------------------------------------------------------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------3 - filter("E"."SALARY">10000)4 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")

相较于预估计划,实际计划多了几个极其关键的真实数据列:

  • Starts: 该操作被执行的次数。

  • E-Rows: 优化器预估返回的行数(和预估计划中的 Rows 一样)。

  • A-Rows: 该操作实际返回的行数。这是最重要的字段之一!

  • A-Time: 执行该步骤所累计的实际时间

  • Buffers: 该步骤累计的逻辑读(缓冲区获取)次数。这是衡量I/O消耗的核心指标。逻辑读越多,消耗的内存和CPU越多。

核心对比:E-Rows vs. A-Rows
这是诊断性能问题的“黄金法则”。如果 E-Rows 和 A-Rows 差异巨大(例如,预估100行,实际返回10万行),几乎可以肯定优化器基于错误的统计信息制定了一个糟糕的计划。刷新统计信息往往是解决这类问题的第一选择

第三部分:深度解读执行计划——成为性能侦探

看懂执行计划的结构和顺序是基本功。

3.1 阅读顺序:从内到外,从上到下

执行计划是一个树状结构。读取时应找到缩进最深Id 最小的步骤,那是执行的起点。然后逐步向外向上推进。

以上述计划为例:

  1. Id 3TABLE ACCESS FULL (EMPLOYEES) 是起点。它根据 WHERE e.salary > 10000 条件过滤表数据。

  2. Id 4:对于全表扫描返回的每一行(A-Rows=5),根据 department_id 去 DEPT_ID_PK 索引上进行一次INDEX UNIQUE SCAN(扫描了5次)。

  3. Id 5:利用索引扫描得到的 ROWID,回表访问 DEPARTMENTS 获取需要的列(department_name)。

  4. Id 1 & 2:完成 NESTED LOOPS 连接,将最终结果(A-Rows=5)返回。

3.2 常见操作类型解析

  • 数据访问路径:

    • TABLE ACCESS FULL:全表扫描。处理大量数据时成本高,是首要优化对象。

    • TABLE ACCESS BY INDEX ROWID:通过索引找到ROWID后再回表取数据。

    • INDEX RANGE SCAN:索引范围扫描。最常见的高效访问方式。

    • INDEX UNIQUE SCAN:索引唯一扫描,效率最高。

  • 表连接方式:

    • NESTED LOOPS:适合驱动表(外部循环)结果集小,内表有高效索引的情况。

    • HASH JOIN:适合处理大数据集的等值连接。它会在PGA中为驱动表构建哈希表。

    • MERGE JOIN:适合非等值连接,但通常需要先做排序,开销较大。

  • 其他操作:

    • SORT ORDER BYSORT GROUP BY:显式排序操作,消耗CPU和内存,甚至可能引发磁盘排序(TempSpc),应尽量避免。

    • VIEW:表示正在处理一个视图。

    • filter:在连接后应用过滤条件,效率较低,应尽量在连接前过滤(谓词推入)。

第四部分:从计划到调优——实战案例精讲

问题场景:一个分页查询随着页数变深,速度越来越慢。

原始SQL:

SELECT * FROM (SELECT rownum rn, a.* FROM large_table a ORDER BY create_date DESC
) WHERE rn BETWEEN 100000 AND 100020;

获取其实际执行计划后,发现:

  • 第一步是对 LARGE_TABLE 进行 TABLE ACCESS FULL

  • 紧接着是一个昂贵的 SORT ORDER BY 操作,处理了整张表的数据。

  • 最后才从排序后的结果中取第10万到100020条。

病因分析:这条SQL为了取20条记录,先排序了整个表(可能几百万行),效率极低。

优化方案:利用索引的有序性避免排序,并将过滤条件提前。

优化后SQL:

SELECT * FROM (SELECT /*+ FIRST_ROWS(20) */ rownum rn, a.* FROM large_table a WHERE rownum <= 100020 ORDER BY create_date DESC -- 此排序可能因索引而避免,或仅排序100020行
) WHERE rn >= 100000;

同时,在 create_date 列上创建降序索引:

CREATE INDEX idx_large_table_date ON large_table(create_date DESC);

优化后计划分析
新的执行计划很可能显示通过 INDEX RANGE SCAN (DESCENDING) 访问数据。数据库直接从索引的“尽头”(最新日期)开始快速读取前100020行,这是一个非常快速的操作。然后在内存中对这100020行进行排序(如果需要的话),最后取出第10万到100020条。数据访问量从全表扫描变成了仅扫描100020行,性能提升是数量级的。

第五部分:总结与最佳实践

驾驭执行计划是一门艺术,更是一门科学。以下是总结出的最佳实践:

  1. 首选实际计划:始终以 DBMS_XPLAN.DISPLAY_CURSOR 获取的实际执行计划和真实运行时统计信息为准。

  2. 聚焦最大开销:遵循“二八定律”,找到消耗 Buffers 最多、A-Time 最长的操作步骤,集中火力优化它。

  3. 紧盯 E-Rows vs A-Rows:这是判断优化器是否“眼瞎”(统计信息问题)的最直接证据。定期收集统计信息 (DBMS_STATS) 是维持数据库健康的基础。

  4. 理解操作含义:深刻理解各种访问路径和连接方式的适用场景,才能做出正确的优化判断。

  5. 迭代与验证:调优是一个“修改 -> 验证 -> 再修改”的循环过程。每次修改后,务必再次检查执行计划,确认优化是否生效,避免负优化。

执行计划就是数据库专家与优化器对话的语言。掌握它,你就能穿透SQL语句的表象,直抵性能问题的核心,从被动的“救火队员”蜕变为主动的“系统架构师”,真正让Oracle数据库这艘巨轮在你的指挥下,于数据的海洋中稳健高效地破浪前行。

http://www.xdnf.cn/news/18956.html

相关文章:

  • SpringMVC相关梳理
  • 使用 Wheel Variants 简化 CUDA 加速 Python 安装和打包工作流
  • PyTorch 机器学习基础(选择合适优化器)
  • MTK Linux DRM分析(二十四)- MTK mtk_drm_plane.c
  • 如何为在线医疗问诊小程序实现音视频通话功能?
  • uniapp跨平台开发---uni.request返回int数字过长精度丢失
  • OpsManage:基于Django的企业级AWS云资源运维管理平台
  • 绿幕电商直播为什么要用专业抠图软件.
  • React 状态丢失:组件 key 用错引发的渲染异常
  • 【Linux系统】线程控制
  • 安装Docker Desktop报错WSL needs updating
  • AAA服务器
  • VS2022+QT6.7+NetWork(TCP服务器多客户端助手)
  • 【若依】RuoYi-Vue-springboot3分离版
  • 专业的储存数据的结构:数据库
  • (笔记)Android ANR检测机制深度分析
  • 第1记 cutlass examples 00 的认真调试分析
  • Ubuntu 22.04 安装 向日葵远程Client端
  • 并发编程——06 JUC并发同步工具类的应用实战
  • sr04模块总结
  • Scala面试题及详细答案100道(41-50)-- 模式匹配
  • MySQL底层数据结构与算法浅析
  • 捡捡java——2、基础05
  • 部署2.516.2版本的jenkins,同时适配jdk8
  • 【Windows】netstat命令解析及端口状态解释
  • React过渡更新:优化渲染性能的秘密
  • Vue3组件加载顺序
  • MySQL 索引
  • THM Whats Your Name WP
  • SDK、JDK、JRE、JVM的区别