当前位置:首页 > 技术文章 > 正文内容

Oracle优化器三大核心:查询从写法到执行计划,是如何炼成的?

导语

在你写下一条SQL的那一刻,Oracle优化器就已悄然启动了它的“军师”本能。它不仅是SQL的执行总导演,更是影响查询快慢、生死攸关的幕后推手。

这篇文章,带你深入解密优化器背后的三大“智囊”——查询转换器、估算器、计划生成器,看看一条SQL是如何被层层打磨、筛选、计算,最终走上“最优路径”的!


01查询转换器:把SQL“翻译”得更聪明

在 Oracle 数据库中,查询优化器是核心组件之一,负责选择最佳执行计划,从而确保 SQL 查询以最高效的方式执行。

Oracle 的查询优化器由三个主要组件组成:查询转换器 (Query Transformer)、估算器 (Estimator) 和 计划生成器 (Plan Generator)。

每个组件在生成查询执行计划时都起着重要作用,接下来将深入解析这些组件的作用、原理及其协作方式。


以下图表展示了这些组件。


一组查询块代表一个解析后的查询,它是优化器的输入。

下表描述了优化器操作:

表1-1 优化器操作


02

查询转换器

查询转换器是优化器的第一步,它的主要任务是对输入的 SQL 查询进行变换,以寻找更高效的执行路径。

具体来说,查询转换器会根据已知的优化规则对查询进行重写,确保查询能够使用更有效的操作。


工作原理

  • 语义等效转换
  • 查询转换器会判断是否有可能将原始 SQL 查询转换为语义等效但成本更低的查询。例如,Oracle 可能会将 OR 条件重写为 UNION ALL,从而优化查询的执行路径。
  • 替代方案
  • 当查询有多种执行方式时,查询转换器会生成多个可行的替代方案,并为每个方案计算预期的成本。最后,它会选择成本最低的方案。

示例

图1-2 查询变换器


假设原始 SQL 查询使用了 OR 条件:

SELECT *

FROM sales

WHERE promo_id = 33

OR prod_id = 136;


查询转换器可能将其重写为:

SELECT *

FROM sales

WHERE prod_id = 136

UNION ALL

SELECT *

FROM sales

WHERE promo_id = 33

AND LNNVL(prod_id = 136);


这种转换可能会降低查询的执行成本,特别是当 UNION ALL 可以避免额外的排序操作时。


03

估算器

估算器负责根据表的统计信息估算每个执行计划的成本。

这一步骤对于确保查询优化器选择最佳执行计划至关重要。

估算器通过计算选择性、基数和成本等度量,来为每个执行计划提供成本评估。


主要度量

  • 选择性 (Selectivity)
  • 选择性表示查询条件从表中筛选出的行的比例。选择性值接近0表示谓词过滤了大部分行,而接近1则表示过滤效果较弱。
  • 基数 (Cardinality)
  • 基数是执行计划中每个操作返回的行数。准确的基数估算对于优化器决定连接策略、排序方式等至关重要。
  • 成本 (Cost)
  • 成本是优化器用于评估执行计划的资源消耗度量,主要包括磁盘I/O、CPU 使用和内存使用等资源的消耗。


如下图所示,如果有统计数据,那么估计器会使用这些数据来计算各项指标。

这些统计数据提高了指标的准确程度。


图 1- 3 估计器


对于示例4 - 1中所示的查询,估计器使用选择性、估计基数(总共返回10行)和成本度量来得出其总成本估计值3:


---------------------------------------------------------------------------

|Id| Operation |Name |Rows|Bytes|Cost %CPU|Time|

---------------------------------------------------------------------------

| 0| SELECT STATEMENT | |10|250|3 (0)|00:00:01|

| 1| NESTED LOOPS | | | | | |

| 2| NESTED LOOPS | |10|250|3 (0)|00:00:01|

|*3| TABLE ACCESS FULL |DEPARTMENTS | 1| 7|2 (0)|00:00:01|

|*4| INDEX RANGE SCAN |EMP_DEPARTMENT_IX|10| |0 (0)|00:00:01|

| 5| TABLE ACCESS BY INDEX ROWID|EMPLOYEES |10|180|1 (0)|00:00:01|

---------------------------------------------------------------------------


1

选择性(Selectivity)

选择性表示行集中行的一个比例,对于等值查询的场景(非等值的计算公式相对复杂,后面再讨论),公式如下:


Selectivity = 谓词条件过滤之后返回的结果集 / 未经谓词条件过滤之后返回的结果集

谓词从行集中筛选出特定数量的行。

因此,谓词的选择性表明有多少行通过谓词测试。选择性的范围是0.0到1.0。

选择性为0.0意味着没有从行集中选择任何行,而选择性为1.0则意味着选择了所有行。

当该值接近0.0时,谓词的选择性更强;当该值接近1.0时,谓词的选择性更弱(或更不具选择性)。


当目前列没有直方图且没有NULL值的情况下,用目标列做等值查询的可选择率:

Selectivity = 1 / NUM_DISTINCT



优化器根据统计信息是否可用来估计选择性:

1)统计数据不可用

根据 OPTIMIZER_DYNAMIC_SAMPLING 初始化参数的值,优化器会使用动态统计信息或内部默认值。

数据库根据谓词类型使用不同的内部默认值。

例如,等值谓词(last_name = 'Smith')的内部默认值低于范围谓词(last_name > 'Smith')的内部默认值,因为预计等值谓词返回的行占比更小。

2)有可用的统计信息

当有可用的统计信息时,估计器会使用它们来估计选择性。

假设存在150个不同的员工姓氏。对于等值谓词last_name = 'Smith',选择性是last_name不同值数量n的倒数,在此示例中为0.006,因为该查询选择的行包含150个不同值中的1个。

如果last_name列上存在直方图,那么估计器将使用直方图而非不同值的数量。

直方图记录了列中不同值的分布,因此它能得出更好的选择性估计,特别是对于存在数据倾斜的列。


示例1:有直方图计算sales.prod_id列的Selectivity


  • 查询sales.prod_id的distinct数量及sales总数据量

方法1:适用于测试环境

select count(distinct PROD_ID),count(1) from sh.sales;



COUNT(DISTINCTPROD_ID) COUNT(1)

---------------------- ----------

72 918843



方法2:适用于生产环境

SQL> SELECT num_rows, sample_size

FROM dba_tables

WHERE owner='SH' AND table_name='SALES';



NUM_ROWS SAMPLE_SIZE

___________ ______________

918843 918843



SELECT column_name, num_distinct, histogram

FROM dba_tab_columns

WHERE owner='SH' AND table_name='SALES' AND column_name='PROD_ID';

COLUMN_NAME NUM_DISTINCT HISTOGRAM

______________ _______________ ____________

PROD_ID 72 FREQUENCY


  • 计算选择性

select distinct prod_id from sh.sales where rownum<3;



PROD_ID

__________

13



SQL> select count(1) from sh.sales where prod_id=13;



COUNT(1)

___________

6002



SQL>

SQL> select 6002/918843;



6002/918843

_______________________________________________

0.006532127904331860829325575751243683632568


示例2:无直方图计算sales.prod_id列的Selectivity


  • 删除列上的直方图

exec dbms_stats.gather_table_stats(ownname => 'SH',tabname => 'SALES',estimate_percent => 100,cascade => true, method_opt=>'for columns prod_id size 1',no_invalidate => false);


  • 查看直方图信息

SELECT column_name, num_distinct, histogram

FROM dba_tab_columns

WHERE owner='SH' AND table_name='SALES' AND column_name='PROD_ID';

COLUMN_NAME NUM_DISTINCT HISTOGRAM

______________ _______________ ____________

PROD_ID 72 NONE


  • 计算选择性

Selectivity

select 1/72;



1/72

_____________________________________________

0.0138888888888888888888888888888888888889


这里补充一个计算sh.sales表中各列的统计信息的查询脚本

SELECT a.column_name,

b.num_rows,

a.num_distinct ,

ROUND(a.num_distinct / b.num_rows * 100, 2) selectivity,

a.histogram,

a.num_buckets

FROM dba_tab_col_statistics a, dba_tables b

WHERE a.owner = b.owner

AND a.table_name = b.table_name

AND a.owner = 'SH'

AND a.table_name = 'SALES';


2 基数(Cardinality)

基数是执行计划中每个操作返回的行数。

例如,如果优化器对全表扫描返回的行数估计为100,那么此操作的基数估计就是100。基数估计显示在执行计划的“Rows(行数)”列中。

优化器根据一组复杂的公式确定每个操作的基数,这些公式使用表级和列级统计信息或动态统计信息作为输入。

当单表查询中出现单个等值谓词且没有直方图时,优化器会使用最简单的公式之一。

在这种情况下,优化器假定为均匀分布,并通过将表中的总行数除以WHERE子句谓词中使用的列中的不同值的数量来计算查询的基数。


计算公式如下:

Computed Cardinality = Original Cardinality * Selectivity

下面的示例是无直方图的场景利用Selectivity估算Cardinality的场景,有直方图的场景在后面的章节再详细介绍。


示例1:用户sh对销售表进行如下查询:


SQL> select /*+full(sales)*/ * from sh.sales where prod_id=13;

6002 rows selected.

Execution Plan

----------------------------------------------------------

Plan hash value: 1550251865

---------------------------------------------------------------------------------------------

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop |

---------------------------------------------------------------------------------------------

| 0 | SELECT STATEMENT | | 12762 | 398K| 3550 (1)| 00:00:01 | | |

| 1 | PARTITION RANGE ALL| | 12762 | 398K| 3550 (1)| 00:00:01 | 1 | 15 |

|* 2 | TABLE ACCESS FULL | SALES | 12762 | 398K| 3550 (1)| 00:00:01 | 1 | 15 |

---------------------------------------------------------------------------------------------


Predicate Information (identified by operation id):

---------------------------------------------------


2 - filter("PROD_ID"=13)


销售表包含918843 行数据。

当前的数据库统计信息表明,prod_id列中的不同值数量为72。

因此,selectivity=1/72,优化器使用公式918843*( 1/72)= 12761.7,将结果集的基数估计为12762。

基数估计必须尽可能准确,因为它们会影响执行计划的各个方面。

当优化器确定连接的成本时,基数很重要。

例如,在员工表和部门表的嵌套循环连接中,员工表中的行数决定了数据库必须查询部门表的频率。

基数对于确定排序成本也很重要。


3 成本

优化器成本模型考虑了预测查询将使用的机器资源。

成本是一种内部数值度量,代表对某个计划估计的资源使用情况。成本特定于优化器环境中的某个查询。

为了估计成本,优化器会考虑以下因素:

  • 系统资源,包括估计的输入/输出、CPU 和内存
  • 估计返回的行数(基数)
  • 初始数据集的大小
  • 数据的分布情况
  • 访问结构

执行时间是成本的一个函数,但成本并不直接等同于时间。

例如,如果查询A的计划成本低于查询B的计划成本,那么可能会出现以下结果:

  • A的执行速度比B快。
  • A的执行速度比B慢。
  • A的执行时间与B相同。

因此,不能将不同查询的成本相互比较。

此外,也不能比较使用不同优化器模式的语义等效查询的成本。

关于成本的计算规则,我们会在后面的章节中详细介绍。


04

计划生成器

计划生成器负责根据查询块和查询变换器提供的信息,生成多种可能的执行计划,并评估每个计划的成本。

优化器会通过评估各种访问路径、连接方法和连接顺序来选择成本最低的执行计划。



工作原理

  • 评估多种连接方法
  • 计划生成器会尝试不同的连接方法,例如嵌套循环连接 (Nested Loops)、排序合并连接 (Merge Join)、哈希连接 (Hash Join) 等。
  • 考虑不同的访问路径
  • 例如,优化器可能会选择使用全表扫描、索引扫描或索引范围扫描来访问表数据。
  • 连接顺序
  • 优化器会考虑不同表连接的顺序,因为不同的连接顺序会导致不同的执行计划成本。

以下图表展示了优化器为输入查询测试不同的计划。


图3-1 计划生成器


以下是优化器跟踪文件中的一段代码片段,展示了优化器执行的一些计算:

GENERAL PLANS

***************************************

Considering cardinality-based initial join order.

Permutations for Starting Table :0

Join order[1]: DEPARTMENTS[D]#0 EMPLOYEES[E]#1



***************

Now joining: EMPLOYEES[E]#1

***************

NL Join

Outer table: Card: 27.00 Cost: 2.01 Resp: 2.01 Degree: 1 Bytes: 16

Access path analysis for EMPLOYEES

. . .

Best NL cost: 13.17

. . .

SM Join

SM cost: 6.08

resc: 6.08 resc_io: 4.00 resc_cpu: 2501688

resp: 6.08 resp_io: 4.00 resp_cpu: 2501688

. . .

SM Join (with index on outer)

Access Path: index(FullScan)

. . .

HA Join

HA cost: 4.57

resc: 4.57 resc_io: 4.00 resc_cpu: 678154

resp: 4.57 resp_io: 4.00 resp_cpu: 678154

Best:: JoinMethod: Hash

Cost: 4.57 Degree: 1 Resp: 4.57 Card: 106.00 Bytes: 27

. . .



***********************

Join order[2]: EMPLOYEES[E]#1 DEPARTMENTS[D]#0

. . .



***************

Now joining: DEPARTMENTS[D]#0

***************

. . .

HA Join

HA cost: 4.58

resc: 4.58 resc_io: 4.00 resc_cpu: 690054

resp: 4.58 resp_io: 4.00 resp_cpu: 690054

Join order aborted: cost > best plan cost

***********************


跟踪文件显示,优化器首先尝试将“departments”表作为连接中的外表。

优化器计算三种不同连接方法的成本:嵌套循环连接(NL)、排序合并(SM)和哈希连接(HA)。

优化器选择哈希连接作为最有效的方法:

Best:: JoinMethod: Hash

Cost: 4.57 Degree: 1 Resp: 4.57 Card: 106.00 Bytes: 27


补充10053跟踪事件

oradebug setmypid

alter session set events '10053 trace name context forever ,level 12';

SELECT e.employee_id, d.department_name

FROM hr.employees e

JOIN hr.departments d ON e.department_id = d.department_id;

alter session set events '10053 trace name context off';

oradebug close_trace

oradebug tracefile_name


然后,优化器尝试使用不同的连接顺序,将employees表作为外表。

这种连接顺序的成本比之前的连接顺序更高,因此被放弃。

优化器使用一个内部截止值,以减少在寻找成本最低的计划时尝试的计划数量。

该截止值基于当前最优计划的成本。

如果当前最优成本较高,那么优化器会探索其他替代计划,以找到成本更低的计划。

如果当前最优成本较低,那么优化器会迅速结束搜索,因为进一步降低成本的意义不大。


05

三大组件协作机制全图解

1)查询转换器 → 多个等价SQL变体

2)估算器 → 每个变体计算代价

3)计划生成器 → 从中选最优计划

它们就像是一支高效的三人小组:
重写、评估、决策,环环相扣。

而最终生成的“执行计划”,才是真正交给数据库执行的“落地方案”。

写在最后

优化器不懂业务,但比你更懂数据!

Oracle 优化器的设计,是“经验 + 统计 + 枚举”的典范。通过查询转换、成本估算、计划筛选的三步走,它让 SQL 在复杂的数据库结构中也能“跑得又稳又快”。

如果你是 DBA、开发、调优工程师——学会与优化器对话,将是你性能突破的关键!

相关文章

适合在任何地方使用的 Linux:15 个小型 Linux 发行版

如果你有一台老旧的 PC 或超小型设备,这些 Linux 发行版中的一个应该适合你。来源:https://linux.cn/article-12281-1.html作者:David Gewirtz译者...

Vue父子组件参数传递方法(vue父子组件传参方式)

在 Vue 中,父子组件之间的参数传递是常见的需求,主要通过 Props 和 自定义事件 实现。以下是详细说明和代码示例:一、父组件向子组件传递参数(Props)父组件通过 属性 向子组件传递数据,子...

学习ES6- 入门Vue(大量源代码及笔记,带你起飞)

ES6学习网站: https://es6.ruanyifeng.com/箭头函数普通函数//普通函数 this 指向调用时所在的对象(可变) let fn = function fn(a, b) {...

「 VUE3 + TS + Vite 」父子组件间如何通信?

组件之间传值,大家都很熟悉,涉及到 VUE3 +TS 好多同学就无从下手了,所以分享这篇文章,希望看完后提起 VUE3+TS 能够不慌不忙。平时使用的函数如:ref、reactive、watch、co...

用IDEA开发如何用Git快速拉取指定分支代码?

1,准备空的文件夹,git init2,关联远程仓库,git remote add origin gitlab地址3,拉取远程分支代码,git pull origin 远程分支名再用IDEA打开项目即...

Python 实现 | 通过 Gitlab API 获取项目工程、分支、commit 提交记录

前提在 gitlab 中你的工程创建 Access Token然后你会得到一个 21 位 access token,代码中需要用到。代码''' 说明: 1.登录gitlab的r...