嵌套循环将外部数据集连接到内部数据集。
对于外部数据集中与单表谓词匹配的每一行,数据库将检索内部数据集中满足连接谓词的所有行。如果索引可用,则数据库可以使用它来访问rowid设置的内部数据集。
- 当优化器考虑嵌套循环联接时,当数据库联接小的数据子集,数据库将优化器模式设置为的大型数据集联接时
FIRST_ROWS,或者联接条件是访问内部表的有效方法时,嵌套循环联接非常有用。 - 嵌套循环如何连接 :从概念上讲,嵌套循环等效于两个嵌套
for循环。 - 嵌套的嵌套循环嵌套循环 :外部循环本身可以是由不同的嵌套循环生成的行源。
- 嵌套循环的最新实现加入 :Oracle数据库11g引入了嵌套循环的新实现,从而减少了物理I / O的总体延迟。
- 嵌套循环联接的原始实现 : 在当前版本中,嵌套循环的新实现和原始实现都是可能的。
- 嵌套循环控件 : 对于某些SQL语句,数据足够小,以使优化程序更喜欢全表扫描和哈希联接。但是,您可以添加
USE_NL提示,以指示优化器将指定的表用作内部表,从而将每个指定的表连接到具有嵌套循环连接的另一个行源。
9.2.1.1当优化器考虑嵌套循环联接时
当数据库联接小的数据子集,数据库以优化器模式设置为联接大型数据集FIRST_ROWS或联接条件是访问内部表的有效方法时,嵌套循环联接将非常有用。
注意:连接期望的行数是驱动优化程序决策的因素,而不是基础表的大小。例如,一个查询可能会联接两个表,每个表有十亿行,但是由于有过滤器,优化器期望每个表有5行。
通常,嵌套循环联接在具有联接条件索引的小型表上最有效。如果行源只有一行,例如对主键值进行相等查找(例如WHERE employee_id=101),则联接是简单的查找。优化器始终尝试将最小的行源放在首位,使其成为驱动表。
各种因素都进入了使用嵌套循环的优化器决策。例如,数据库可以批量从外部行源读取几行。根据检索到的行数,优化器可以选择嵌套循环或哈希联接到内部行源。例如,如果查询加入departments到drive table employees,并且谓词在中指定值employees.last_name,则数据库可能会读取索引中的足够条目last_name以确定是否通过了内部阈值。如果未通过阈值,那么优化器将选择一个嵌套循环联接departments,如果通过了阈值,则数据库将执行哈希联接,这意味着读取其余部分employees,将其哈希到内存中,然后联接到departments。
如果内部循环的访问路径不依赖于外部循环,则结果可以是笛卡尔乘积:对于外部循环的每次迭代,内部循环都会产生相同的行集。为避免此问题,请使用其他联接方法联接两个独立的行源。
9.2.1.2嵌套循环如何工作
从概念上讲,嵌套循环等效于两个嵌套for循环。
例如,如果查询加入employees和departments,则伪代码中的嵌套循环可能是:
FOR erow IN (select * from employees where X=Y) LOOP
FOR drow IN (select * from departments where erow is matched) LOOP
output values from erow and drow
END LOOP
END LOOP
内部循环对外部循环的每一行执行。该employees表是“外部”数据集,因为它在外部for循环中。外部桌子有时被称为驾驶台。该departments表是“内部”数据集,因为它位于内部for循环中。
嵌套循环联接涉及以下基本步骤:
- 优化器确定驱动行源并将其指定为外部循环。
外循环产生一组行来驱动连接条件。行源可以是使用索引扫描,全表扫描或任何其他生成行的操作访问的表。
内部循环的迭代次数取决于在外部循环中检索到的行数。例如,如果从外部表中检索了10行,则数据库必须在内部表中执行10个查找。如果从外部表中检索了10,000,000行,则数据库必须在内部表中执行10,000,000查找。
- 优化器将另一行源指定为内部循环。
在执行计划中,外循环出现在内循环之前,如下所示:
NESTED LOOPS outer_loop inner_loop - 对于客户端的每个获取请求,基本过程如下:
- 从外部行源获取一行
- 探查内部行源以查找与谓词条件匹配的行
- 重复前面的步骤,直到通过获取请求获得所有行
有时,数据库会对rowid进行排序以获得更有效的缓冲区访问模式。
9.2.1.3嵌套嵌套循环
嵌套循环的外部循环本身可以是由其他嵌套循环生成的行源。
数据库可以嵌套两个或多个外部循环,以根据需要连接尽可能多的表。每个循环都是一种数据访问方法。以下模板显示了数据库如何通过三个嵌套循环进行迭代:
SELECT STATEMENT
NESTED LOOPS 3
NESTED LOOPS 2 - Row source becomes OUTER LOOP 3.1
NESTED LOOPS 1 - Row source becomes OUTER LOOP 2.1
OUTER LOOP 1.1
INNER LOOP 1.2
INNER LOOP 2.2
INNER LOOP 3.2
数据库按以下顺序对循环进行排序:
- 数据库通过
NESTED LOOPS 1以下方式进行迭代:NESTED LOOPS 1 OUTER LOOP 1.1 INNER LOOP 1.2的输出
NESTED LOOP 1是行源。 - 数据库
NESTED LOOPS 2使用生成的行源NESTED LOOPS 1作为其外部循环,通过进行迭代:NESTED LOOPS 2 OUTER LOOP 2.1 - Row source generated by NESTED LOOPS 1 INNER LOOP 2.2的输出
NESTED LOOPS 2是另一个行源。 - 数据库
NESTED LOOPS 3使用生成的行源NESTED LOOPS 2作为其外部循环,通过进行迭代:NESTED LOOPS 3 OUTER LOOP 3.1 - Row source generated by NESTED LOOPS 2 INNER LOOP 3.2
示例9-2嵌套嵌套循环联接
假设您按如下所示加入employees和departments表:
SELECT /*+ ORDERED USE_NL(d) */ e.last_name, e.first_name, d.department_name
FROM employees e, departments d
WHERE e.department_id=d.department_id
AND e.last_name like 'A%';
该计划表明,优化器选择了两个嵌套循环(步骤1和步骤2)来访问数据:
SQL_ID ahuavfcv4tnz4, child number 0
-------------------------------------
SELECT /*+ ORDERED USE_NL(d) */ e.last_name, d.department_name FROM
employees e, departments d WHERE e.department_id=d.department_id AND
e.last_name like 'A%'
Plan hash value: 1667998133
----------------------------------------------------------------------------------
|Id| Operation |Name |Rows|Bytes|Cost(%CPU)|Time|
----------------------------------------------------------------------------------
| 0| SELECT STATEMENT | | | |5 (100)| |
| 1| NESTED LOOPS | | | | | |
| 2| NESTED LOOPS | | 3|102|5 (0)|00:00:01|
| 3| TABLE ACCESS BY INDEX ROWID BATCHED| EMPLOYEES | 3| 54|2 (0)|00:00:01|
|*4| INDEX RANGE SCAN | EMP_NAME_IX | 3| |1 (0)|00:00:01|
|*5| INDEX UNIQUE SCAN | DEPT_ID_PK | 1| |0 (0)| |
| 6| TABLE ACCESS BY INDEX ROWID | DEPARTMENTS | 1| 16|1 (0)|00:00:01|
----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("E"."LAST_NAME" LIKE 'A%')
filter("E"."LAST_NAME" LIKE 'A%')
5 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
在此示例中,基本过程如下:
- 数据库开始通过内部嵌套循环(步骤2)进行迭代,如下所示:
- 数据库在中搜索
emp_name_ix以所有开头的姓氏的行标识A(第4步)。例如:
Abel,employees_rowid Ande,employees_rowid Atkinson,employees_rowid Austin,employees_rowid - 使用上一步中的rowid,数据库从
employees表中检索一批行(步骤3)。例如:Abel,Ellen,80 Abel,John,50这些行成为最内部嵌套循环的外部行源。
批处理步骤通常是自适应执行计划的一部分。为了确定嵌套循环是否比哈希联接更好,优化器需要确定从行源返回的许多行。如果返回太多行,则优化器将切换到其他联接方法。
- 对于外部行源中的每一行,数据库将扫描
dept_id_pk索引以获取departments匹配部门ID 的rowid in (第5步),并将其加入employees行中。例如:Abel,Ellen,80,departments_rowid Ande,Sundar,80,departments_rowid Atkinson,Mozhe,50,departments_rowid Austin,David,60,departments_rowid这些行成为外部嵌套循环的外部行源(步骤1)。
- 数据库在中搜索
- 数据库通过外部嵌套循环进行迭代,如下所示:
- 数据库读取外部行源中的第一行。
例如:
Abel,Ellen,80,departments_rowid - 数据库使用
departmentsrowid从中检索相应的行departments(步骤6),然后将结果合并以获得请求的值(步骤1)。例如:
Abel,Ellen,80,Sales - 数据库读取外部行源中的下一行,使用
departmentsrowid从中检索对应的行departments(步骤6),并循环遍历直到检索到所有行。结果集具有以下形式:
Abel,Ellen,80,Sales Ande,Sundar,80,Sales Atkinson,Mozhe,50,Shipping Austin,David,60,IT
- 数据库读取外部行源中的第一行。
9.2.1.4嵌套循环联接的当前实现
Oracle Database 11g引入了一种针对嵌套循环的新实现,可减少物理I / O的总体延迟。
当索引或表块不在缓冲区高速缓存中并且需要用于处理联接时,则需要物理I / O。数据库可以批处理多个物理I / O请求,并使用矢量I / O(数组)而不是一次处理它们。数据库将Rowid数组发送给操作系统,操作系统执行读取操作。
作为新实现的一部分,NESTED LOOPS执行计划中可能会出现两个联接行源,而以前的版本中只会出现一个。在这种情况下,Oracle数据库会分配一个NESTED LOOPS联接行源,以将联接外侧的表中的值与内侧的索引联接。分配第二行源以将第一次联接的结果与表放在联接的内侧联接在一起,第一次联接的结果包括存储在索引中的行标识。
考虑查询“ 对于嵌套循环,原来实行加盟 ”。在当前实现中,此查询的执行计划可能如下:
-------------------------------------------------------------------------------------
| Id | Operation | Name |Rows|Bytes|Cost%CPU| Time |
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 19 | 722 | 3 (0)|00:00:01|
| 1 | NESTED LOOPS | | | | | |
| 2 | NESTED LOOPS | | 19 | 722 | 3 (0)|00:00:01|
|* 3 | TABLE ACCESS FULL | DEPARTMENTS | 2 | 32 | 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 | 220 | 1 (0)|00:00:01| <--在回表时,在次通过NL 可以批量提取数据
------------------------------------------------------------------------------------- 使用向量IO后,ORACLE将原先一批单块读所需要耗费的物理IO组合起来
然后用一个向量IO去批量处理它们,
Predicate Information (identified by operation id): 这样实现了单块读数量不降低的情况下,减少这些单块读所需要耗费的物理IO数量,也就提交 了NL的执行效率
---------------------------------------------------
3 - filter("D"."DEPARTMENT_NAME"='Marketing' OR "D"."DEPARTMENT_NAME"='Sales')
4 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
在这种情况下,hr.departments表中的行构成内部嵌套循环(步骤2)的外部行源(步骤3)。索引emp_department_ix是内部嵌套循环的内部行源(步骤4)。内部嵌套循环的结果形成外部嵌套循环(行1)的外部行源(行2)。该hr.employees表是外部嵌套循环的外部行源(行5)。
对于每个获取请求,基本过程如下:
- 数据库遍历内部嵌套循环(第2步)以获取提取中请求的行:
- 数据库读取的第一行,
departments以获取名为Marketing或的部门的部门IDSales(步骤3)。例如:Marketing,20该行集是外部循环。数据库将数据缓存在PGA中。
- 数据库进行扫描
emp_department_ix(这是employees表上的索引)以查找employees与此部门ID对应的行ID(步骤4),然后将结果合并(步骤2)。结果集具有以下形式:
Marketing,20,employees_rowid Marketing,20,employees_rowid Marketing,20,employees_rowid - 数据库读取的下一行
departments,进行扫描emp_department_ix以查找employees与此部门ID相对应的rowid,然后遍历循环直到满足客户请求。在此示例中,数据库仅对外部循环进行两次迭代,因为只有两行来自
departments满足谓词过滤器。从概念上讲,结果集具有以下形式:Marketing,20,employees_rowid Marketing,20,employees_rowid Marketing,20,employees_rowid . . . Sales,80,employees_rowid Sales,80,employees_rowid Sales,80,employees_rowid . . .这些行成为外部嵌套循环的外部行源(步骤1)。该行集被缓存在PGA中。
- 数据库读取的第一行,
- 数据库组织在上一步中获得的rowid,以便它可以更有效地在缓存中访问它们。
- 数据库开始通过外部嵌套循环进行迭代,如下所示:
- 数据库从上一步获得的行集中检索第一行,如以下示例所示:
Marketing,20,employees_rowid - 数据库使用rowid从中检索行
employees以获得请求的值(步骤1),如以下示例所示:Michael,Hartstein,13000,Marketing - 数据库从行集中检索下一行,使用rowid探测
employees匹配的行,然后遍历循环直到检索到所有行。结果集具有以下形式:
Michael,Hartstein,13000,Marketing Pat,Fay,6000,Marketing John,Russell,14000,Sales Karen,Partners,13500,Sales Alberto,Errazuriz,12000,Sales . . .
- 数据库从上一步获得的行集中检索第一行,如以下示例所示:
在某些情况下,未分配第二个连接行源,并且执行计划看起来与Oracle Database 11g之前的执行计划相同。以下列表描述了这种情况:
- 联接内部所需的所有列都在索引中,并且不需要表访问。在这种情况下,Oracle数据库仅分配一个联接行源。
- 返回的行的顺序可能与Oracle Database 12c之前的版本中返回的顺序不同。因此,当Oracle数据库尝试保留行的特定顺序(例如为了消除对
ORDER BY排序的需要)时,Oracle数据库可能会将原始实现用于嵌套循环连接。 - 该
OPTIMIZER_FEATURES_ENABLE初始化参数设置为之前释放Oracle数据库11g。在这种情况下,Oracle数据库将原始实现用于嵌套循环连接。
父主题: 嵌套循环联接
9.2.1.5嵌套循环联接的原始实现
在当前版本中,嵌套循环的新实现和原始实现都是可能的。
有关原始实现的示例,请考虑hr.employees和hr.departments表的以下联接:
SELECT e.first_name, e.last_name, e.salary, d.department_name
FROM hr.employees e, hr.departments d
WHERE d.department_name IN ('Marketing', 'Sales')
AND e.department_id = d.department_id;
在Oracle Database 11g之前的版本中,此查询的执行计划可能如下所示:
------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 19 | 722 | 3 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | 10 | 220 | 1 (0)| 00:00:01 | <--传统的方式,是每回一行, 提取一行
| 2 | NESTED LOOPS | | 19 | 722 | 3 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL | DEPARTMENTS | 2 | 32 | 2 (0)| 00:00:01 |
|* 4 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX | 10 | | 0 (0)| 00:00:01 |
------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - filter("D"."DEPARTMENT_NAME"='Marketing' OR "D"."DEPARTMENT_NAME"='Sales')
4 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
#我的测试:alter session set optimizer_features_enable='10.2.0.1'; # OPTIMIZER_FEATURES_ENABLESYS@orcl1>SELECT /*+ ORDERED use_nl(d) */e.ENAME, d.DNAME FROM emp e ,dept d WHERE d.DNAME IN ('RESEARCH', 'SALES') AND e.DEPTNO = d.DEPTNO;11 rows selected.Execution Plan----------------------------------------------------------Plan hash value: 351108634----------------------------------------------------------------------------------------| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |----------------------------------------------------------------------------------------| 0 | SELECT STATEMENT | | 6666K| 139M| 10M (1)| 33:20:36 || 1 | NESTED LOOPS | | 6666K| 139M| 10M (1)| 33:20:36 || 2 | TABLE ACCESS FULL | EMP | 10M| 85M| 101 (98)| 00:00:02 ||* 3 | TABLE ACCESS BY INDEX ROWID| DEPT | 1 | 13 | 1 (0)| 00:00:01 ||* 4 | INDEX UNIQUE SCAN | PK_DEPT | 1 | | 0 (0)| 00:00:01 |----------------------------------------------------------------------------------------Statistics---------------------------------------------------------- 1 recursive calls 0 db block gets 13 consistent gets 0 physical reads 0 redo size 850 bytes sent via SQL*Net to client 499 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 11 rows processed对于每个获取请求,基本过程如下:
- 数据库遍历循环以获取提取中请求的行:
- 数据库读取的第一行,
departments以获取名为Marketing或的部门的部门IDSales(步骤3)。例如:市场营销20
该行集是外部循环。数据库将行缓存在PGA中。
- 数据库进行扫描
emp_department_ix(这是该employees.department_id列的索引),以查找employees与此部门ID相对应的行ID(步骤4),然后将结果合并(步骤2)。从概念上讲,结果集具有以下形式:
Marketing,20,employees_rowid Marketing,20,employees_rowid Marketing,20,employees_rowid - 数据库读取的下一行
departments,进行扫描emp_department_ix以查找employees与此部门ID相对应的rowid,并在循环中进行迭代,直到满足客户请求为止。在此示例中,数据库仅对外部循环进行两次迭代,因为只有两行来自
departments满足谓词过滤器。从概念上讲,结果集具有以下形式:Marketing,20,employees_rowid Marketing,20,employees_rowid Marketing,20,employees_rowid . . . Sales,80,employees_rowid Sales,80,employees_rowid Sales,80,employees_rowid . . .
- 数据库读取的第一行,
- 根据情况,数据库可以组织在上一步中获得的缓存行标识符,以便它可以更有效地访问它们。
- 对于
employees由嵌套循环生成的结果集中的每个rowid,数据库从中检索一行employees以获得请求的值(步骤1)。因此,基本过程是读取rowid并检索匹配的
employees行,读取下一个rowid并检索匹配的employees行,依此类推。从概念上讲,结果集具有以下形式:Michael,Hartstein,13000,Marketing Pat,Fay,6000,Marketing John,Russell,14000,Sales Karen,Partners,13500,Sales Alberto,Errazuriz,12000,Sales . . .
父主题: 嵌套循环联接
9.2.1.6嵌套循环控件
对于某些SQL语句,数据足够小,以使优化器更喜欢全表扫描和哈希联接。但是,您可以添加USE_NL提示,以指示优化器将指定的表用作内部表,从而将每个指定的表连接到具有嵌套循环连接的另一个行源。
相关的提示提示指示优化器使用指定的表作为内部表,通过嵌套循环连接将指定的表连接到另一个行源。索引是可选的。如果未指定索引,则嵌套循环联接将使用至少具有一个联接谓词的索引作为索引键。 USE_NL_WITH_INDEX(table index)
示例9-3嵌套循环提示
假设优化器为以下查询选择哈希联接:
SELECT e.last_name, d.department_name
FROM employees e, departments d
WHERE e.department_id=d.department_id;
该计划如下所示:
------------------------------------------------------------------------------
|Id | Operation | Name | Rows | Bytes |Cost(%CPU)| Time |
------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 5 (100)| |
|*1 | HASH JOIN | | 106 | 2862 | 5 (20)| 00:00:01 |
| 2 | TABLE ACCESS FULL| DEPARTMENTS | 27 | 432 | 2 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL| EMPLOYEES | 107 | 1177 | 2 (0)| 00:00:01 |
------------------------------------------------------------------------------
要强制使用嵌套循环联接departments作为内部表,请USE_NL在以下查询中添加提示:
SELECT /*+ ORDERED USE_NL(d) */ e.last_name, d.department_name
FROM employees e, departments d
WHERE e.department_id=d.department_id;
该计划如下所示:
--------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |Cost (%CPU)| Time |
--------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 34 (100)| |
| 1 | NESTED LOOPS | | 106 | 2862 | 34 (3)| 00:00:01 |
| 2 | TABLE ACCESS FULL| EMPLOYEES | 107 | 1177 | 2 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL| DEPARTMENTS | 1 | 16 | 0 (0)| |
--------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - filter("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
数据库获得结果集,如下所示:
- 在嵌套循环中,数据库读取
employees以获得雇员的姓氏和部门ID(步骤2)。例如:De Haan,90 - 对于上一步中获得的行,数据库进行扫描
departments以找到与employees部门ID 匹配的部门名称(步骤3),并将结果联接起来(步骤1)。例如:De Haan,Executive - 数据库检索中的下一行
employees,从中检索匹配的行departments,然后重复此过程,直到检索到所有行。结果集具有以下形式:
De Haan,Executive Kochnar,Executive Baer,Public Relations King,Executive . . .




