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

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.dtcms.com/a/356651.html

相关文章:

  • 商家协同生态下的复购革命:跨店收益如何激活12%增量消费
  • 【新启航】3D 逆向抄数的工具技术与核心能力:基于点云处理的扫描设备操作及模型重建方法论
  • 【活动回顾】“智驱未来,智领安全” AI+汽车质量与安全论坛
  • OpenEuler部署LoganaLyzer
  • 【开题答辩全过程】以 基于SpringBootVue的智能敬老院管理系统为例,包含答辩的问题和答案
  • 黑芝麻智能与云深处科技达成战略合作,共推具身智能平台全球市场应用
  • 基于互补素数与最小素因子性质的哥德巴赫猜想证明-陈墨仙
  • ROS2学习打卡表
  • 外卖大战之后,再看美团的护城河
  • vue3获取子组件
  • PostgreSQL15——子查询
  • 采用机器学习的苗期棉株点云器官分割与表型信息提取
  • GPT-Realtime架构与Token成本控制深度解析
  • Linux/UNIX系统编程手册笔记:基本概念
  • Redis面试题--介绍下Redis几种集群模式
  • Shell 秘典(卷二)——号令延展秘术 与 流程掌控心法・if 天机判语篇精解
  • 力扣p1011在D天送达包裹的能力 详解
  • docker-相关笔记
  • 网站加载慢,linux服务器接口请求响应变慢,怎么排查,一般是什么原因
  • 【算法】78.子集--通俗讲解
  • 开源网盘聚合工具Alist:统一管理20+云存储的技术实践
  • vue常见的指令都有哪些,有什么作用
  • 内存页和TLB缓存
  • 计算机专业考研备考建议
  • 前端缓存问题详解
  • CollageIt:简单易用的照片拼贴工具
  • 代理IP服务器平台推荐,企业级动态住宅IP资源哪里找?
  • 在做题中学习(93):最长公共子序列的长度
  • 1.6 GPU Usage简介
  • 算法题打卡力扣第1004. 最大连续1的个数 III(mid)