本文为SQL Tuning Guide第9章“Join”的笔记。

重要基本概念

  • join condition
    A condition that compares two row sources using an expression. The database combines pairs of rows, each containing one row from each row source, for which the join condition evaluates to true.
    加入条件
    使用表达式比较两个行源的条件。 数据库组合成对的行,每个行包含来自每个行源的一行,对于这些行,连接条件的计算结果为真。

  • left deep join tree
    A join tree in which the left input of every join is the result of a previous join.
    一个连接树,其中每个连接的左输入是前一个连接的结果。

  • right deep join tree
    A join tree in which the right input of every join is the result of a previous join, and the left child of every internal node of a join tree is a table.
    一个连接树,其中每个连接的右输入是前一个连接的结果,连接树的每个内部节点的左子节点是一个表。

  • left table
    In an outer join, the table specified on the left side of the OUTER JOIN keywords (in ANSI SQL syntax).
    在外连接中,在 OUTER JOIN 关键字左侧指定的表(在 ANSI SQL 语法中)。

  • right table
    In an outer join, the table specified on the right side of the OUTER JOIN keywords (in ANSI SQL syntax).
    在外连接中,在 OUTER JOIN 关键字右侧指定的表(在 ANSI SQL 语法中)。

Oracle 数据库为联结行集提供了多种优化。

  • partition-wise join
    翻译为智能分区联结

9.1 About Joins

联结将恰好来自两个行源(例如表或视图)的输出组合在一起,并返回一个行源。 返回的行源是数据集。

联结的特征在于 SQL 语句的 WHERE(非 ANSI)或 FROM … JOIN (ANSI) 子句中的多个表。 只要 FROM 子句中存在多个表,Oracle 数据库就会执行联结。

联结条件使用表达式比较两个行源。 联结条件定义了表之间的关系。 如果语句未指定联结条件,则数据库执行笛卡尔联结,将一个表中的每一行与另一个表中的每一行匹配。

9.1.1 Join Trees

通常,连接树表示为倒置的树结构。

如下图所示,table1 为左表,table2 为右表。 优化器从左到右处理连接。 例如,如果此图描绘了一个嵌套循环连接,则 table1 是外循环,而 table2 是内循环。

连接的输入可以是先前连接的结果集。 如果连接树的每个内部节点的右子节点都是一个表,那么该树就是一个左深度连接树,如下例所示。 大多数连接树都是左深连接。

如果连接树的每个内部节点的左子节点都是一张表,则该树称为右深连接树,如下图所示。

如果连接树的内部节点的左或右子节点可以是连接节点,则该树称为浓密连接树。 在下面的示例中,table4 是连接节点的右子节点,table1 是连接节点的左子节点,table2 是连接节点的左子节点。

在另一个变体中,连接的两个输入都是先前连接的结果。

9.1.2 How the Optimizer Executes Join Statements

数据库连接成对的行源。 当 FROM 子句中存在多个表时,优化器必须确定对每对最有效的连接操作。

优化器必须做出下表所示的相关决策。

  • 访问路径
    对于简单语句,优化器必须选择一个访问路径来从连接语句中的每个表中检索数据。 例如,优化器可能会在全表扫描或索引扫描之间进行选择。
  • 联结方法
    要联结每对行源,Oracle 数据库必须决定如何进行。 “如何”即连接方法。 可能的连接方法是嵌套循环、排序合并和散列连接。 笛卡尔连接需要上述连接方法之一。 每种连接方法都有比其他方法更适合的特定情况。
  • 联结类型
    联结条件决定联结类型。 例如,内连接只检索与连接条件匹配的行。 外连接检索与连接条件不匹配的行。
  • 联结顺序
    为了执行联结两个以上表的语句,Oracle 数据库连接两个表,然后将生成的行源连接到下一个表。 这个过程一直持续到所有表都加入到结果中。 例如,数据库连接两个表,然后将结果连接到第三个表,然后将此结果连接到第四个表,依此类推。

9.1.3 How the Optimizer Chooses Execution Plans for Joins

在确定连接顺序和方法时,优化器的目标是尽早减少行数,以便在整个 SQL 语句执行过程中执行更少的工作。

优化器根据可能的连接顺序、连接方法和可用的访问路径生成一组执行计划。 然后优化器估计每个计划的成本并选择成本最低的一个。 在选择执行计划时,优化器会考虑以下因素:

  • 优化器首先确定连接两个或多个表是否会导致一个最多包含一行的行源。
    优化器根据表上的 UNIQUE 和 PRIMARY KEY 约束识别这种情况。如果存在这种情况,那么优化器会将这些表放在连接顺序的首位。优化器然后优化其余表集的连接。

  • 对于具有外连接条件的连接语句,具有外连接运算符的表通常在连接顺序中位于条件中的另一个表之后。
    通常,优化器不会考虑违反此准则的连接顺序,尽管优化器在某些情况下会覆盖此排序条件。同样,当子查询已转换为反连接或半连接时,来自子查询的表必须位于与它们连接或相关的外部查询块中的那些表之后。但是,哈希反连接和半连接能够在某些情况下覆盖此排序条件。

优化器通过计算估计的 I/O 和 CPU 来估计查询计划的成本。 这些 I/O 具有与之相关的特定成本:单个块 I/O 的成本和多块 I/O 的另一个成本。 此外,不同的函数和表达式具有与之相关的 CPU 成本。 优化器使用这些指标确定查询计划的总成本。 这些指标可能会受到编译时许多初始化参数和会话设置的影响,例如 DB_FILE_MULTI_BLOCK_READ_COUNT 设置、系统统计信息等。

例如,优化器通过以下方式估算成本:

  • 嵌套循环连接的成本取决于将外部表的每个选定行和内部表的每个匹配行读入内存的成本。 优化器使用数据字典中的统计信息来估计这些成本。
  • 排序合并连接的成本很大程度上取决于将所有源读入内存并对其进行排序的成本。
  • 哈希连接的成本很大程度上取决于在连接的一个输入端构建哈希表并使用来自连接另一端的行来探测它的成本。

从概念上讲,优化器构建了一个连接顺序和方法的矩阵以及与每个相关的成本。 例如,优化器必须确定在查询中如何最好地连接 date_dim 和 lineorder 表。 下表显示了方法和订单的可能变化,以及每个的成本。 在此示例中,以 date_dim,lineorder 顺序连接 的嵌套循环成本最低。

9.2 Join Methods

连接方法是连接两个行源的机制。

根据统计数据,优化器选择估计成本最低的方法。 如图 9-5 所示,每个连接方法都有两个孩子:驱动(也称为外部)行源和驱动目标(也称为内部)行源。

9.2.1 Nested Loops Joins

嵌套循环将外部数据集连接到内部数据集。

对于外部数据集中与单表谓词匹配的每一行,数据库检索内部数据集中满足连接谓词的所有行。 如果索引可用,那么数据库可以使用它来访问由 rowid 设置的内部数据。

9.2.1.1 When the Optimizer Considers Nested Loops Joins

当数据库连接小数据子集、数据库连接大量数据且优化器模式设置为 FIRST_ROWS,或连接条件存在访问内部表的有效方法时(如内表由索引),嵌套循环连接非常有用。

注意:连接预期的行数是优化器决策的驱动因素,而不是基础表的大小。 例如,一个查询可能连接两个表,每个表有 10 亿行,但由于过滤器,优化器期望每个数据集有 5 行。

一般来说,嵌套循环连接在连接条件上有索引的小表上效果最好。 如果行源只有一行,例如在主键值上进行相等查找(例如,WHERE employee_id=101),则连接是一个简单的查找。 优化器总是尝试将最小的行源放在第一位,使其成为驱动表。

各种因素进入优化器决定使用嵌套循环。例如,数据库可以批量从外部行源中读取几行。根据检索到的行数,优化器可以选择嵌套循环或哈希连接到内部行源。例如,如果查询将部门连接到驱动表employees,并且如果谓词指定employees.last_name 中的值,则数据库可能会读取last_name 索引中的足够条目以确定是否超过内部阈值。如果未通过阈值,则优化器选择对部门进行嵌套循环连接,如果通过阈值,则数据库执行散列连接,这意味着读取其余员工,将其散列到内存中,然后加入到部门。(这其实就是adaptive plan)

如果内循环的访问路径不依赖于外循环,则结果可以是笛卡尔积:对于外循环的每次迭代,内循环都会产生相同的行集。为避免此问题,请使用其他连接方法连接两个独立的行源。

9.2.1.2 How Nested Loops Joins Work

从概念上讲,嵌套循环相当于两个嵌套的 for 循环。

例如,如果查询连接员工和部门,则伪代码中的嵌套循环可能是:

FOR erow IN (select * from employees where X=Y) LOOPFOR drow IN (select * from departments where erow is matched) LOOPoutput values from erow and drowEND LOOP
END LOOP

对外循环的每一行执行内循环。 employees 表是“外部”数据集,因为它位于外部 for 循环中。 外部表有时称为驱动表。 部门表是“内部”数据集,因为它位于内部 for 循环中。

嵌套循环连接涉及以下基本步骤:

  1. 优化器确定驱动行源并将其指定为外循环。
    外循环产生一组用于驱动连接条件的行。 行源可以是使用索引扫描、全表扫描或任何其他生成行的操作访问的表。
    内循环的迭代次数取决于在外循环中检索到的行数。 例如,如果从外部表中检索到 10 行,那么数据库必须在内部表中执行 10 次查找。 如果从外部表中检索到 10,000,000 行,则数据库必须在内表中执行 10,000,000 次查找。
  2. 优化器将另一个行源指定为内部循环。
    在执行计划中,外循环出现在内循环之前,如下:
NESTED LOOPS outer_loopinner_loop
  1. 对于客户端的每一个 fetch 请求,基本流程如下:
    a. 从外部行源获取一行
    b. 探测内部行源以查找与谓词条件匹配的行
    c. 重复上述步骤,直到 fetch 请求获取到所有行

有时,数据库对 rowid 进行排序以获得更有效的缓冲区访问模式。

9.2.1.3 Nested Nested Loops

嵌套循环的外循环本身可以是由不同的嵌套循环生成的行源。

数据库可以嵌套两个或多个外部循环,以根据需要连接尽可能多的表。 每个循环都是一种数据访问方法。 以下模板显示了数据库如何迭代三个嵌套循环:

SELECT STATEMENTNESTED LOOPS 3NESTED LOOPS 2          - Row source becomes OUTER LOOP 3.1NESTED LOOPS 1        - Row source becomes OUTER LOOP 2.1OUTER LOOP 1.1INNER LOOP 1.2  INNER LOOP 2.2INNER LOOP 3.2

例如:

set pages 9999
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%';select * from table(dbms_xplan.display_cursor);-----------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |             |       |       |     5 (100)|          |
|   1 |  NESTED LOOPS                         |             |     3 |   102 |     5   (0)| 00:00:01 |
|   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")

原文中这个例子讲的很详细,此不赘述。

9.2.1.4 Current Implementation for Nested Loops Joins

Oracle 数据库 11g 引入了嵌套循环的新实现,可减少物理 I/O 的总体延迟。

当索引或表块不在缓冲区高速缓存中并且需要处理连接时,需要物理 I/O。 数据库可以批量处理多个物理 I/O 请求,并使用向量 I/O(数组)而不是一次处理一个。 数据库将一组 rowids 发送到执行读取的操作系统。

9.2.1.5 Original Implementation for Nested Loops Joins

虽然很详细,但没太看懂两者之间的区别。

9.2.1.6 Nested Loops Controls

您可以添加 USE_NL 提示以指示优化器将每个指定的表连接到具有嵌套循环连接的另一个行源,使用指定的表作为内表。

相关提示 USE_NL_WITH_INDEX(table index) 提示指示优化器使用指定表作为内表,通过嵌套循环连接将指定表连接到另一个行源。 该索引是可选的。 如果未指定索引,则嵌套循环连接使用具有至少一个连接谓词的索引作为索引键。

要强制使用部门作为内部表进行嵌套循环连接,请添加 USE_NL 提示,如以下查询中所示:

-- The ORDERED hint causes Oracle to join tables in the order in which they appear in the FROM clause.SELECT /*+ ORDERED USE_NL(d) */ e.last_name, d.department_name
FROM   employees e, departments d
WHERE  e.department_id=d.department_id;

9.2.2 Hash Joins

数据库使用散列连接来连接更大的数据集。

优化器使用两个数据集中较小的一个在内存中的连接键上构建哈希表,使用确定性哈希函数指定哈希表中存储每一行的位置。 然后数据库扫描更大的数据集,探测哈希表以找到满足连接条件的行。

9.2.2.1 When the Optimizer Considers Hash Joins

一般来说,当需要连接比较大量的数据(或者小表的很大一部分需要连接)时,优化器会考虑使用哈希连接,这种连接是等值连接。

当较小的数据集适合内存时,哈希连接最具成本效益。 在这种情况下,成本仅限于对两个数据集进行一次读取。

因为哈希表在 PGA 中,所以 Oracle 数据库可以访问行而不锁定它们。 该技术通过避免在数据库缓冲区高速缓存中重复锁定和读取块的必要性来减少逻辑 I/O。

如果数据集不适合内存,那么数据库会对行源进行分区,并且连接会逐个分区进行。 这可以使用大量的排序区内存和临时表空间的 I/O。 这种方法仍然是最具成本效益的方法,尤其是当数据库使用并行查询服务器时。

9.2.2.2 How Hash Joins Work

散列算法采用一组输入并应用确定性散列函数来生成随机散列值。

在散列连接中,输入值是连接键。 输出值是数组中的索引(槽),它是哈希表。

9.2.2.2.1 Hash Tables

为了说明哈希表,假设数据库在部门和员工的联接中对 hr.departments 进行哈希处理。 连接键列是 department_id。

部门表前5行如下:

SQL> select * from departments where rownum < 6;DEPARTMENT_ID DEPARTMENT_NAME                MANAGER_ID LOCATION_ID
------------- ------------------------------ ---------- -----------10 Administration                        200        170020 Marketing                             201        180030 Purchasing                            114        170040 Human Resources                       203        240050 Shipping                              121        1500

数据库将散列函数应用于表中的每个部门 ID,为每个部门生成一个散列值。 对于这个例子,哈希表有 5 个槽(它可能有更多或更少)。 因为 n 是 5,所以可能的散列值范围是 1 到 5。散列函数可能会为部门 ID 生成以下值:

f(10) = 4
f(20) = 1
f(30) = 4
f(40) = 2
f(50) = 5

请注意,散列函数恰好为部门 10 和 30 生成相同的散列值 4。这称为散列冲突。 在这种情况下,数据库使用链表将部门 10 和 30 的记录放在同一个槽中。 从概念上讲,哈希表如下所示:

1    20,Marketing,201,1800
2    40,Human Resources,203,2400
3
4    10,Administration,200,1700 -> 30,Purchasing,114,1700
5    50,Shipping,121,1500

从以上结果可知,即使哈希冲突也没有关系,因为还存放了额外的值。

9.2.2.2.2 Hash Join: Basic Steps

优化器使用较小的数据源在内存中的连接键上构建哈希表,然后扫描较大的表以查找连接的行。

基本步骤如下:

  1. 数据库对较小的数据集(称为构建表)执行完整扫描,然后将哈希函数应用于每行中的连接键,以在 PGA 中构建哈希表。
    在伪代码中,算法可能如下所示:
FOR small_table_row IN (SELECT * FROM small_table)
LOOPslot_number := HASH(small_table_row.join_key);INSERT_HASH_TABLE(slot_number,small_table_row);
END LOOP;
  1. 数据库使用成本最低的访问机制来探测第二个数据集,称为探测表。
    通常,数据库对较小和较大的数据集执行全表扫描。 伪代码中的算法可能如下所示:
FOR large_table_row IN (SELECT * FROM large_table)
LOOPslot_number := HASH(large_table_row.join_key);small_table_row = LOOKUP_HASH_TABLE(slot_number,large_table_row.join_key);IF small_table_row FOUNDTHENoutput small_table_row + large_table_row;END IF;
END LOOP;

如果哈希表槽中存在多行,则数据库遍历行的链表,检查每一行。 例如,如果部门 30 散列到插槽 4,则数据库检查每一行,直到找到 30。

例如:

SELECT o.customer_id, l.unit_price * l.quantity
FROM   orders o, order_items l
WHERE  l.order_id = o.order_id;--------------------------------------------------------------------------
| Id  | Operation            |  Name        | Rows  | Bytes | Cost (%CPU)|
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |              |   665 | 13300 |     8  (25)|
|*  1 |  HASH JOIN           |              |   665 | 13300 |     8  (25)|
|   2 |   TABLE ACCESS FULL  | ORDERS       |   105 |   840 |     4  (25)|
|   3 |   TABLE ACCESS FULL  | ORDER_ITEMS  |   665 |  7980 |     4  (25)|
--------------------------------------------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------1 - access("L"."ORDER_ID"="O"."ORDER_ID")

因为相对于大 6 倍的 order_items 表,orders 表很小,所以数据库对订单进行哈希处理。 在散列连接中,构建表的数据集始终出现在操作列表的首位(步骤 2)。 在步骤 3 中,数据库稍后对较大的 order_items 执行完整扫描,探测每一行的哈希表。

9.2.2.3 How Hash Joins Work When the Hash Table Does Not Fit in the PGA

当散列表不完全适合 PGA 时,数据库必须使用不同的技术。 在这种情况下,数据库使用一个临时空间来保存散列表的部分(称为分区),有时还包含探查散列表的较大表的部分。

基本流程如下:

  1. 数据库对较小的数据集执行完整扫描,然后在 PGA 和磁盘上构建散列桶数组。
    当 PGA 哈希区填满时,数据库会在哈希表中找到最大的分区并将其写入磁盘上的临时空间。 数据库将属于该磁盘分区的任何新行存储在磁盘上,并将所有其他行存储在 PGA 中。 因此,哈希表的一部分在内存中,一部分在磁盘上。
  2. 数据库开始第一轮读取其他数据集。
  3. 数据库一一读取每个磁盘上的临时分区
  4. 数据库将每个分区行连接到相应的磁盘临时分区中的行。

9.2.2.4 Hash Join Controls

USE_HASH 提示指示优化器在将两个表连接在一起时使用哈希连接。

9.2.3 Sort Merge Joins

排序合并连接是嵌套循环连接的一种变体。

如果连接中的两个数据集尚未排序,则数据库对它们进行排序。 这些是 SORT JOIN 操作。 对于第一个数据集中的每一行,数据库探测第二个数据集的匹配行并将它们连接起来,它的起始位置基于前一次迭代中的匹配。 这是 MERGE JOIN 操作。

9.2.3.1 When the Optimizer Considers Sort Merge Joins

哈希连接需要一个哈希表和一个对该表的探测,而排序合并连接需要两种排序。

当满足以下任一条件时,优化器可能会选择排序合并连接而不是哈希连接来连接大量数据:

  • 两个表之间的连接条件不是等值连接,即使用<、<=、>、>=等不等条件。
    与排序合并相比,散列连接需要相等条件。

  • 由于其他操作需要排序,优化器发现使用排序合并更便宜。
    如果存在索引,则数据库可以避免对第一个数据集进行排序。 但是,无论索引如何,数据库总是对第二个数据集进行排序(为什么?)

与嵌套循环连接相比,排序合并具有与哈希连接相同的优势:数据库访问 PGA 中的行而不是 SGA,通过避免重复锁定和读取数据库缓冲区缓存中的块的必要性来减少逻辑 I/O。 一般来说,哈希连接比排序合并连接执行得更好,因为排序很昂贵。 但是,排序合并连接比散列连接提供以下优点:

  • 在初始排序之后,优化合并阶段,从而更快地生成输出行。
  • 当哈希表不能完全置入内存时,排序合并可能比哈希连接更具成本效益。
    内存不足的哈希联接需要将哈希表和其他数据集都复制到磁盘。 在这种情况下,数据库可能必须多次从磁盘读取。 在排序合并中,如果内存不能保存两个数据集,则数据库将它们都写入磁盘,但读取每个数据集不超过一次。

9.2.3.2 How Sort Merge Joins Work

与嵌套循环连接一样,排序合并连接读取两个数据集,但在它们尚未排序时对它们进行排序。

对于第一个数据集中的每一行,数据库在第二个数据集中找到一个起始行,然后读取第二个数据集,直到找到一个不匹配的行。 在伪代码中,排序合并的高级算法可能如下所示:

READ data_set_1 SORT BY JOIN KEY TO temp_ds1
READ data_set_2 SORT BY JOIN KEY TO temp_ds2READ ds1_row FROM temp_ds1
READ ds2_row FROM temp_ds2WHILE NOT eof ON temp_ds1,temp_ds2
LOOPIF ( temp_ds1.key = temp_ds2.key ) OUTPUT JOIN ds1_row,ds2_rowELSIF ( temp_ds1.key <= temp_ds2.key ) READ ds1_row FROM temp_ds1ELSIF ( temp_ds1.key => temp_ds2.key ) READ ds2_row FROM temp_ds2
END LOOP

其实以上算法的前提,还是等值查询。

还是看个例子吧,本例中,外围表带索引,无需再排序:

set pages 9999
SELECT e.employee_id, e.last_name, e.first_name, e.department_id, d.department_name
FROM   employees e, departments d
WHERE  e.department_id = d.department_id
ORDER BY department_id;select * from table(dbms_xplan.display_cursor);--------------------------------------------------------------------------------------------
| Id  | Operation                    | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |             |       |       |     6 (100)|          |
|   1 |  MERGE JOIN                  |             |   106 |  4028 |     6  (17)| 00:00:01 |
|   2 |   TABLE ACCESS BY INDEX ROWID| DEPARTMENTS |    27 |   432 |     2   (0)| 00:00:01 |
|   3 |    INDEX FULL SCAN           | DEPT_ID_PK  |    27 |       |     1   (0)| 00:00:01 |
|*  4 |   SORT JOIN                  |             |   107 |  2354 |     4  (25)| 00:00:01 |
|   5 |    TABLE ACCESS FULL         | EMPLOYEES   |   107 |  2354 |     3   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------4 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")filter("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")

这两个数据集是部门表和员工表。 因为索引按部门 ID 对部门表进行排序,所以数据库可以读取该索引并避免排序(步骤 3)。 数据库只需要对employees表进行排序(第4步),这是CPU最密集的操作。

假设外围表无索引(通过hint禁用),同样的SQL语句,其执行计划如下:

SELECT /*+ USE_MERGE(d e) NO_INDEX(d) */ e.employee_id, e.last_name, e.first_name, e.department_id, d.department_name
FROM   employees e, departments d
WHERE  e.department_id = d.department_id
ORDER BY department_id;-----------------------------------------------------------------------------------
| Id  | Operation           | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |             |       |       |     8 (100)|          |
|   1 |  MERGE JOIN         |             |   106 |  4028 |     8  (25)| 00:00:01 |
|   2 |   SORT JOIN         |             |    27 |   432 |     4  (25)| 00:00:01 |
|   3 |    TABLE ACCESS FULL| DEPARTMENTS |    27 |   432 |     3   (0)| 00:00:01 |
|*  4 |   SORT JOIN         |             |   107 |  2354 |     4  (25)| 00:00:01 |
|   5 |    TABLE ACCESS FULL| EMPLOYEES   |   107 |  2354 |     3   (0)| 00:00:01 |
-----------------------------------------------------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------4 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")filter("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")Hint Report (identified by operation id / Query Block Name / Object Alias):
Total hints for statement: 1 (U - Unused (1))
---------------------------------------------------------------------------3 -  SEL$1 / "D"@"SEL$1"U -  USE_MERGE(d e)

因为部门.department_id 索引被忽略,优化器执行排序,这将步骤 2 和步骤 3 的综合成本增加了 33%(从 6 到 8)。

9.2.3.3 Sort Merge Join Controls

USE_MERGE 提示指示优化器使用排序合并连接。

在某些情况下,使用 USE_MERGE 提示覆盖优化器可能是有意义的。 例如,优化器可以选择对表进行全扫描并避免查询中的排序操作。 但是,成本会增加,因为通过索引和单块读取访问大表,而不是通过全表扫描更快地访问。

9.3 Join Types

连接类型由连接条件的类型决定。

9.3.1 Inner Joins

内连接(有时称为简单连接)是只返回满足连接条件的行的连接。 内连接是等值连接或非等值连接。

9.3.1.1 Equijoins

等值连接是一种内连接,其连接条件包含一个相等运算符。

以下示例是一个等值连接,因为连接条件仅包含一个相等运算符:

SELECT e.employee_id, e.last_name, d.department_name
FROM   employees e, departments d
WHERE  e.department_id=d.department_id;

在上述查询中,连接条件为 e.department_id=d.department_id。 如果employees表中某行的department ID与departments表中某行的值相匹配,则数据库返回join的结果; 否则,数据库不会返回结果。

9.3.1.2 Nonequijoins

nonequijoin 是一种内部连接,其连接条件包含一个不是相等运算符的运算符。

以下查询列出了雇用日期发生在员工 176(因为他在 2007 年换工作而在 job_history 中列出)在公司工作时的所有员工:

SELECT e.employee_id, e.first_name, e.last_name, e.hire_date
FROM   employees e, job_history h
WHERE  h.employee_id = 176
AND    e.hire_date BETWEEN h.start_date AND h.end_date;

在前面的示例中,连接employees 和job_history 的条件不包含相等运算符,因此它是nonequijoin。 Nonequijoins 是比较少见的。

请注意,散列连接至少需要部分等值连接。 以下 SQL 脚本包含一个相等连接条件 (e1.empno = e2.empno) 和一个非相等条件:

SET AUTOTRACE TRACEONLY EXPLAIN
SELECT *
FROM   scott.emp e1 JOIN scott.emp e2
ON     ( e1.empno = e2.empno
AND      e1.hiredate BETWEEN e2.hiredate-1 AND e2.hiredate+1 )

优化器为以上的查询选择一个哈希连接。

9.3.1.3 Band Joins

区段连接是一种特殊类型的非等连接,其中一个数据集中的键值必须落在第二个数据集的指定范围(“区段”)内。 同一张表可以同时作为第一和第二数据集。

从 Oracle Database 12c 第 2 版 (12.2) 开始,数据库更有效地评估区段连接。 优化避免了对超出定义范围的行进行不必要的扫描。

优化器使用成本估算来选择连接方法(散列、嵌套循环或排序合并)和并行数据分布方法。 在大多数情况下,优化的性能可与等值连接相媲美。

以下示例查询工资比每位员工的工资少 100 美元到多 100 美元的员工。 因此,区段的宽度为 200 美元。 这些示例假设允许将每个员工的薪水与其自身进行比较。 以下查询包括部分示例输出:

SELECT  e1.last_name || ' has salary between 100 less and 100 more than ' || e2.last_name AS "SALARY COMPARISON"
FROM    employees e1, employees e2
WHERE   e1.salary
BETWEEN e2.salary - 100
AND     e2.salary + 100;SALARY COMPARISON
-------------------------------------------------------------
King has salary between 100 less and 100 more than King
Kochhar has salary between 100 less and 100 more than Kochhar
Kochhar has salary between 100 less and 100 more than De Haan
De Haan has salary between 100 less and 100 more than Kochhar
De Haan has salary between 100 less and 100 more than De Haan
Russell has salary between 100 less and 100 more than Russell
Partners has salary between 100 less and 100 more than Partners
...

9.3.2 Outer Joins

外连接返回满足连接条件的所有行,以及一个表中没有其他表满足条件的行。 因此,外连接的结果集是内连接的超集。

在 ANSI 语法中,OUTER JOIN 子句指定外连接。 在 FROM 子句中,左表出现在 OUTER JOIN 关键字的左侧,右表出现在这些关键字的右侧。 左表也称为外表,右表也称为内表。 例如,在以下语句中,employees 表是左表或外部表:

SELECT employee_id, last_name, first_name
FROM   employees LEFT OUTER JOIN departments
ON     (employees.department_id=departments.departments_id);

外连接要求外连接表是驱动表。 在前面的示例中,employees 是驱动表,departments 是被驱动表。

9.3.2.1 Nested Loops Outer Joins

数据库使用此操作循环通过两个表之间的外连接。 外部联接返回外部(保留)表行,即使内部(可选)表中没有相应的行。

在标准的嵌套循环中,优化器根据成本选择表的顺序——哪个是驱动表,哪个是驱动表。 但是,在嵌套循环外连接中,连接条件决定了表的顺序。 数据库使用外部保留行的表来驱动到内部表。

优化器在以下情况下使用嵌套循环连接来处理外连接:

  • 可以从外部表驱动到内部表。
  • 数据量足够小以使嵌套循环方法有效。

9.3.2.2 Hash Join Outer Joins

当数据量足够大以使哈希连接有效,或者无法从外部表驱动到内部表时,优化器使用哈希连接来处理外部连接。

成本决定了表的顺序。 外部表(包括保留的行)可用于构建哈希表,也可用于探测哈希表。

此示例显示了一个典型的散列连接外连接查询及其执行计划。 在本例中,查询了所有信用额度大于 1000 的客户。 需要外部连接,以便查询捕获没有订单的客户。

  • 外表时customers。
  • 内表是orders。
  • 联接保留customers行,以及包括orders中没有相应行的那些行。

您可以使用 NOT EXISTS 子查询来返回行。 但是,因为您要查询表中的所有行,所以散列连接的性能更好(除非 NOT EXISTS 子查询没有嵌套)。

注意下例中的(+):

SELECT cust_last_name, SUM(NVL2(o.customer_id,0,1)) "Count"
FROM   customers c, orders o
WHERE  c.credit_limit > 1000
AND    c.customer_id = o.customer_id(+)
GROUP BY cust_last_name;---------------------------------------------------------------------------
| Id  | Operation          | Name      |Rows  |Bytes|Cost (%CPU)|Time     |
---------------------------------------------------------------------------
|  0 | SELECT STATEMENT    |           |      |       | 7 (100)|          |
|  1 |  HASH GROUP BY      |           |  168 |  3192 | 7  (29)| 00:00:01 |
|* 2 |   HASH JOIN OUTER   |           |  318 |  6042 | 6  (17)| 00:00:01 |
|* 3 |    TABLE ACCESS FULL| CUSTOMERS |  260 |  3900 | 3   (0)| 00:00:01 |
|* 4 |    TABLE ACCESS FULL| ORDERS    |  105 |   420 | 2   (0)| 00:00:01 |
---------------------------------------------------------------------------

该查询查找满足各种条件的客户。 当外部联接在内部表中找不到任何对应的行时,它会为内部表列以及外部(保留)表行返回 NULL。 此操作查找没有任何订单行的所有客户行。

9.3.2.3 Sort Merge Outer Joins

当外部联接不能从外部(保留)表驱动到内部(可选)表时,它不能使用散列联接或嵌套循环联接。

在这种情况下,它使用排序合并外连接。

优化器在以下情况下使用排序合并进行外连接:

  • 嵌套循环连接效率低下。 由于数据量大,嵌套循环连接可能效率低下。
  • 优化器发现使用排序合并比散列连接更便宜,因为其他操作需要排序。

9.3.2.4 Full Outer Joins

完全外连接是左外连接和右外连接的组合。

除了内部联接之外,两个表中尚未在内部联接结果中返回的行将被保留并使用空值进行扩展。 换句话说,完全外连接将表连接在一起,但在连接表中显示没有对应行的行。

以下查询检索所有部门和每个部门中的所有员工,但还包括:

  • 任何没有部门的员工
  • 任何没有员工的部门
SELECT d.department_id, e.employee_id
FROM   employees e FULL OUTER JOIN departments d
ON     e.department_id = d.department_id
ORDER BY d.department_id;

从 Oracle 数据库 11g 开始,Oracle 数据库自动使用基于散列连接的本机执行方法来尽可能执行完全外连接。 当数据库使用新方法执行全外连接时,查询的执行计划包含HASH JOIN FULL OUTER。 示例 9-11 中的查询使用以下执行计划:

---------------------------------------------------------------------------
| Id| Operation               | Name       |Rows|Bytes |Cost (%CPU)|Time  |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT        |            |122 | 4758 | 6  (34)|00:0 0:01|
| 1 |  SORT ORDER BY          |            |122 | 4758 | 6  (34)|00:0 0:01|
| 2 |   VIEW                  | VW_FOJ_0   |122 | 4758 | 5  (20)|00:0 0:01|
|*3 |    HASH JOIN FULL OUTER |            |122 | 1342 | 5  (20)|00:0 0:01|
| 4 |     INDEX FAST FULL SCAN| DEPT_ID_PK | 27 |  108 | 2   (0)|00:0 0:01|
| 5 |     TABLE ACCESS FULL   | EMPLOYEES  |107 |  749 | 2   (0)|00:0 0:01|
---------------------------------------------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------3 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")

HASH JOIN FULL OUTER 包含在前面的计划(第3步)中,说明查询使用了哈希全外连接的执行方式。 通常,当两个表之间的全外连接条件是等连接时,哈希全外连接执行方法是可能的,Oracle 数据库自动使用它。

要指示优化器考虑使用散列完全外连接执行方法,请应用 NATIVE_FULL_OUTER_JOIN 提示。 要指示优化器不要考虑使用散列完全外连接执行方法,请应用 NO_NATIVE_FULL_OUTER_JOIN 提示。 NO_NATIVE_FULL_OUTER_JOIN 提示指示优化器在加入每个指定表时排除本机执行方法。 相反,完全外连接作为左外连接和反连接的联合执行。

9.3.2.5 Multiple Tables on the Left of an Outer Join

在 Oracle Database 12c 中,一个外连接表的左侧可能存在多个表。

此增强功能使 Oracle 数据库能够合并包含多个表并显示在外连接左侧的视图。 在 Oracle Database 12c 之前的版本中,如下查询无效,并会触发 ORA-01417 错误消息:

SELECT t1.d, t3.c
FROM   t1, t2, t3
WHERE  t1.z = t2.z
AND    t1.x = t3.x (+)
AND    t2.y = t3.y (+);

从 Oracle Database 12c 开始,上述查询有效。

9.3.3 Semijoins

半连接是两个数据集之间的连接,当子查询数据集中存在匹配行时,它从第一个数据集中返回一行。

9.3.3.1 When the Optimizer Considers Semijoins

数据库在第一次匹配时停止处理第二个数据集。 因此,当第二个数据集中的多行满足子查询条件时,优化不会从第一个数据集中复制行。
注意:即使导致它们的 SQL 构造是子查询,半连接和反连接也被视为连接类型。 它们是优化器用来展平子查询构造的内部算法,以便可以以类似连接的方式解决它们。

当查询只需要确定是否存在匹配时,半连接避免了返回大量行。

对于大型数据集,这种优化可以显着节省嵌套循环连接的时间,嵌套循环连接必须遍历内部查询为外部查询中的每一行返回的每条记录。 优化器可以将半连接优化应用于嵌套循环连接、哈希连接和排序合并连接。

优化器可能会在以下情况下选择半连接:

  • 该语句使用 IN 或 EXISTS 子句。
  • 该语句在 IN 或 EXISTS 子句中包含一个子查询。
  • IN 或 EXISTS 子句不包含在 OR 分支内。

9.3.3.2 How Semijoins Work

根据使用的连接类型,半连接优化的实现方式不同。

以下伪代码显示了嵌套循环连接的半连接:

FOR ds1_row IN ds1 LOOPmatch := false;FOR ds2_row IN ds2_subquery LOOPIF (ds1_row matches ds2_row) THENmatch := true;EXIT -- stop processing second data set when a match is foundEND IFEND LOOPIF (match = true) THEN RETURN ds1_rowEND IF
END LOOP

上述伪代码中,ds1为第一个数据集,ds2_subquery为子查询数据集。 代码从第一个数据集中获取第一行,然后循环遍历子查询数据集寻找匹配项。 代码一找到匹配就退出内部循环,然后开始处理第一个数据集中的下一行。

以下查询使用 WHERE EXISTS 子句仅列出包含员工的部门:

SELECT department_id, department_name
FROM   departments
WHERE EXISTS (SELECT 1FROM   employees WHERE  employees.department_id = departments.department_id)

执行计划在步骤 1 中显示了 NESTED LOOPS SEMI 操作:

---------------------------------------------------------------------------
| Id| Operation          | Name              |Rows|Bytes|Cost (%CPU)|Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT   |                   |   |     | 2 (100)|         |
| 1 |  NESTED LOOPS SEMI |                   |11 | 209 | 2   (0)|00:00:01 |
| 2 |   TABLE ACCESS FULL| DEPARTMENTS       |27 | 432 | 2   (0)|00:00:01 |
|*3 |   INDEX RANGE SCAN | EMP_DEPARTMENT_IX |44 | 132 | 0   (0)|         |
---------------------------------------------------------------------------

以下查询使用 IN 子句仅列出包含员工的部门:

SELECT department_id, department_name
FROM   departments
WHERE  department_id IN (SELECT department_id FROM   employees);

该计划与上一个示例的计划相同。

9.3.4 Antijoins

反连接是两个数据集之间的连接,当子查询数据集中不存在匹配行时,它会从第一个数据集中返回一行。

与半连接一样,反连接在找到第一个匹配项时停止处理子查询数据集。 与半连接不同,反连接仅在未找到匹配项时返回一行。

9.3.4.1 When the Optimizer Considers Antijoins

当查询只需要在不存在匹配时返回一行时,反连接避免了不必要的处理。

对于大型数据集,这种优化可以显着节省嵌套循环连接的时间。 后一个连接必须遍历外部查询中每一行的内部查询返回的每条记录。 优化器可以将反连接优化应用于嵌套循环连接、哈希连接和排序合并连接。

优化器可能会在以下情况下选择反连接:

  • 该语句使用 NOT IN 或 NOT EXISTS 子句。
  • 该语句在 NOT IN 或 NOT EXISTS 子句中有一个子查询。
  • NOT IN 或 NOT EXISTS 子句不包含在 OR 分支内。
  • 该语句执行外连接并将 IS NULL 条件应用于连接列,如下例所示:
SET AUTOTRACE TRACEONLY EXPLAIN
SELECT emp.*
FROM   emp, dept
WHERE  emp.deptno = dept.deptno(+)
AND    dept.deptno IS NULLExecution Plan
----------------------------------------------------------
Plan hash value: 1543991079------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes |Cost (%CPU)|Time    |
------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |    14 |  1400 |  5  (20)| 00:00:01 |
|*  1 |  HASH JOIN ANTI    |      |    14 |  1400 |  5  (20)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| EMP  |    14 |  1218 |  2   (0)| 00:00:01 |
|   3 |   TABLE ACCESS FULL| DEPT |     4 |    52 |  2   (0)| 00:00:01 |
------------------------------------------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------1 - access("EMP"."DEPTNO"="DEPT"."DEPTNO")Note
------ dynamic statistics used: dynamic sampling (level=2)

9.3.4.2 How Antijoins Work

根据使用的连接类型,反连接优化的实现方式不同。

以下伪代码显示了嵌套循环连接的反连接:

FOR ds1_row IN ds1 LOOPmatch := true;FOR ds2_row IN ds2 LOOPIF (ds1_row matches ds2_row) THENmatch := false;EXIT -- stop processing second data set when a match is foundEND IFEND LOOPIF (match = true) THEN RETURN ds1_rowEND IF
END LOOP

在上述伪代码中,ds1 是第一个数据集,ds2 是第二个数据集。 代码从第一个数据集中获取第一行,然后遍历第二个数据集寻找匹配项。 代码一找到匹配就退出内部循环,并开始处理第一个数据集中的下一行。

例子Example 9-15 Semijoin Using WHERE EXISTS有问题,不是anti join而是semi join。

9.3.4.3 How Antijoins Handle Nulls

对于半连接,IN 和 EXISTS 在功能上是等效的。 但是,因为空值,NOT IN 和 NOT EXISTS 在功能上不等效。

如果将空值返回给 NOT IN 运算符,则该语句不返回任何记录。 要了解原因,请考虑以下 WHERE 子句:

WHERE department_id NOT IN (null, 10, 20)

数据库测试前面的表达式如下:

WHERE (department_id != null)
AND   (department_id != 10)
AND   (department_id != 20)

要使整个表达式为真,每个单独的条件都必须为真。 但是,一个空值不能与另一个值进行比较,因此 department_id !=null 条件不能为真,因此整个表达式始终为假。 以下技术可以避免以上情况:

  • 将 NVL 函数应用于子查询返回的列。
  • 向子查询添加一个 IS NOT NULL 谓词。
  • 实施 NOT NULL 约束。

与 NOT IN 相比,NOT EXISTS 子句仅考虑返回匹配存在的谓词,并忽略任何不匹配或由于空值而无法确定的行。 如果子查询中至少有一行与外部查询中的行匹配,则 NOT EXISTS 返回 false。 如果没有匹配的元组,则 NOT EXISTS 返回 true。 子查询中存在空值不会影响对匹配记录的搜索。

在 Oracle 数据库 11g 之前的版本中,当子查询可能返回空值时,优化器无法使用反连接优化。 但是,从 Oracle 数据库 11g 开始,以下部分中描述的 ANTI NA(和 ANTI SNA)优化使优化器即使在可能出现空值时也可以使用反连接。

例如:

SELECT department_id, department_name
FROM   departments
WHERE  department_id NOT IN (SELECT department_id FROM   employees);

即使多个部门不包含员工,上述查询也不返回任何行。 由于employees.department_id 列可以为空,因此出现此结果,这不是用户想要的。

执行计划如下:

-----------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |             |       |       |     6 (100)|          |
|   1 |  MERGE JOIN ANTI NA                   |             |    17 |   323 |     6  (17)| 00:00:01 |
|   2 |   SORT JOIN                           |             |    27 |   432 |     2   (0)| 00:00:01 |
|   3 |    TABLE ACCESS BY INDEX ROWID BATCHED| DEPARTMENTS |    27 |   432 |     2   (0)| 00:00:01 |
|   4 |     INDEX FULL SCAN                   | DEPT_ID_PK  |    27 |       |     1   (0)| 00:00:01 |
|*  5 |   SORT UNIQUE                         |             |   107 |   321 |     4  (25)| 00:00:01 |
|   6 |    TABLE ACCESS FULL                  | EMPLOYEES   |   107 |   321 |     3   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------5 - access("DEPARTMENT_ID"="DEPARTMENT_ID")filter("DEPARTMENT_ID"="DEPARTMENT_ID")

ANTI SNA 代表“single null-aware antijoin”。 ANTI NA 代表“null-aware antijoin”。 空值感知操作使优化器即使在可为空的列上也可以使用反连接优化。 在 Oracle 数据库 11g 之前的版本中,当可能为空时,数据库无法对 NOT IN 查询执行反连接。

假设用户通过对子查询应用 IS NOT NULL 条件来重写查询:

SELECT department_id, department_name
FROM   departments
WHERE  department_id NOT IN (SELECT department_id FROM   employeesWHERE  department_id IS NOT NULL);

执行计划如下:

----------------------------------------------------------------------------------------
| Id  | Operation          | Name              | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |                   |       |       |     3 (100)|          |
|   1 |  NESTED LOOPS ANTI |                   |    17 |   323 |     3   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| DEPARTMENTS       |    27 |   432 |     3   (0)| 00:00:01 |
|*  3 |   INDEX RANGE SCAN | EMP_DEPARTMENT_IX |    41 |   123 |     0   (0)|          |
----------------------------------------------------------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------3 - access("DEPARTMENT_ID"="DEPARTMENT_ID")filter("DEPARTMENT_ID" IS NOT NULL)

前面的查询返回 16 行,这是预期的结果。 计划中的第 1 步显示标准 NESTED LOOPS ANTI 连接,而不是 ANTI NA 或 ANTI SNA 连接,因为子查询不能返回空值。

假设用户发出以下带有 NOT EXISTS 子句的查询以列出不包含员工的部门:

SELECT department_id, department_name
FROM   departments d
WHERE  NOT EXISTS(SELECT nullFROM   employees eWHERE  e.department_id = d.department_id)

前面的查询避免了 NOT IN 子句的 null 问题。 因此,即使 employees.department_id 列可以为空,该语句也会返回所需的结果。

执行计划如下:

----------------------------------------------------------------------------------------
| Id  | Operation          | Name              | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |                   |       |       |     3 (100)|          |
|   1 |  NESTED LOOPS ANTI |                   |    17 |   323 |     3   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| DEPARTMENTS       |    27 |   432 |     3   (0)| 00:00:01 |
|*  3 |   INDEX RANGE SCAN | EMP_DEPARTMENT_IX |    41 |   123 |     0   (0)|          |
----------------------------------------------------------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------3 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")

执行计划的第 1 步揭示了一个 NESTED LOOPS ANTI 操作,而不是 ANTI NA 变体,当可能出现空值时,它是 NOT IN 所必需的。

9.3.5 Cartesian Joins

当一个或多个表对语句中的任何其他表没有任何连接条件时,数据库使用笛卡尔连接。

优化器将来自一个数据源的每一行与来自另一个数据源的每一行连接起来,创建两组的笛卡尔积。 因此,连接产生的总行数使用以下公式计算,其中 rs1 是第一个行集中的行数,rs2 是第二个行集中的行数:

rs1 X rs2 = total rows in result set

9.3.5.1 When the Optimizer Considers Cartesian Joins优化器仅在特定情况下对两个行源使用笛卡尔连接。

通常,情况是以下之一:

  • 不存在连接条件。
    在某些情况下,优化器可以在两个表之间选择一个公共过滤条件作为可能的连接条件。
    注意:如果查询计划中出现笛卡尔联接,则可能是由于无意中省略了联接条件造成的。 通常,如果一个查询连接 n 个表,则需要 n-1 个连接条件来避免笛卡尔连接。
  • 笛卡尔连接是一种有效的方法时。
    例如,优化器可能决定生成两个非常小的表的笛卡尔积,这两个表都连接到同一个大表。
  • ORDERED 提示在指定连接表之前指定一个表。

9.3.5.2 How Cartesian Joins Work

笛卡尔连接使用嵌套的 FOR 循环。

在高层次上,笛卡尔连接的算法如下所示,其中 ds1 通常是较小的数据集,而 ds2 是较大的数据集:

FOR ds1_row IN ds1 LOOPFOR ds2_row IN ds2 LOOPoutput ds1_row and ds2_rowEND LOOP
END LOOP

在此示例中,用户打算执行employees 表和departments 表的内部联接,但不小心遗漏了联接条件:

SELECT e.last_name, d.department_name
FROM   employees e, departments d

执行计划为:

--------------------------------------------------------------------------------------
| Id  | Operation              | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |             |       |       |    12 (100)|          |
|   1 |  MERGE JOIN CARTESIAN  |             |  2889 | 57780 |    12   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL    | DEPARTMENTS |    27 |   324 |     3   (0)| 00:00:01 |
|   3 |   BUFFER SORT          |             |   107 |   856 |     9   (0)| 00:00:01 |
|   4 |    INDEX FAST FULL SCAN| EMP_NAME_IX |   107 |   856 |     0   (0)|          |
--------------------------------------------------------------------------------------

在上述计划的步骤 1 中,CARTESIAN 关键字表示存在笛卡尔连接。 行数 (2889) 是 27 和 107 的乘积。

第三步,BUFFER SORT操作表示数据库正在将扫描emp_name_ix得到的数据块从SGA复制到PGA。 这种策略避免了对数据库缓冲区缓存中相同块的多次扫描,这会产生许多逻辑读取并允许资源争用。

9.3.5.3 Cartesian Join Controls

ORDERED 提示指示优化器按照它们在 FROM 子句中出现的顺序连接表。 通过在没有直接连接(在下面的例子中,你可以看到e和l没有直接的连接)的两个行源之间强制连接,优化器必须执行笛卡尔连接。

在以下示例中,ORDERED 提示指示优化器连接员工和位置,但没有连接条件连接这两个行源:

SELECT /*+ORDERED*/ e.last_name, d.department_name, l.country_id, l.state_province
FROM   employees e, locations l, departments d
WHERE  e.department_id = d.department_id
AND    d.location_id = l.location_id

以下执行计划显示位置(步骤 6)和员工(步骤 4)之间的笛卡尔积(步骤 3),然后将其连接到部门表(步骤 2):

----------------------------------------------------------------------------------------------
| Id  | Operation                | Name              | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |                   |       |       |   152 (100)|          |
|*  1 |  HASH JOIN               |                   |   106 |  4664 |   152   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL      | DEPARTMENTS       |    27 |   513 |     3   (0)| 00:00:01 |
|   3 |   MERGE JOIN CARTESIAN   |                   |  2461 | 61525 |   149   (0)| 00:00:01 |
|   4 |    VIEW                  | index$_join$_001  |   107 |  1177 |     2   (0)| 00:00:01 |
|*  5 |     HASH JOIN            |                   |       |       |            |          |
|   6 |      INDEX FAST FULL SCAN| EMP_DEPARTMENT_IX |   107 |  1177 |     1   (0)| 00:00:01 |
|   7 |      INDEX FAST FULL SCAN| EMP_NAME_IX       |   107 |  1177 |     1   (0)| 00:00:01 |
|   8 |    BUFFER SORT           |                   |    23 |   322 |   147   (0)| 00:00:01 |
|   9 |     TABLE ACCESS FULL    | LOCATIONS         |    23 |   322 |     1   (0)| 00:00:01 |
----------------------------------------------------------------------------------------------

上例中,去掉hint或者改写FROM子句为FROM employees e, departments d, locations l,就不会出现笛卡尔积。

9.4 Join Optimizations

连接优化使连接更有效。

9.4.1 Bloom Filters

Bloom filter 以其创建者 Burton Bloom 的名字命名,是一种低内存数据结构,用于测试成员是否在集合中。

布隆过滤器可以正确地判断元素不在集合中,但可能误判元素在集合中。 因此,假阴性是不可能的,但假阳性是可能的。

9.4.1.1 Purpose of Bloom Filters

布隆过滤器测试一组值以确定它们是否是另一组的成员。

例如,一组是 (10,20,30,40),第二组是 (10,30,60,70)。 布隆过滤器可以确定 60 和 70 肯定不在第一组中,并且 10 和 30 可能是第一组的成员。 当存储过滤器所需的内存量相对于数据集中的数据量较小时,以及大多数数据预计无法通过成员资格测试时,布隆过滤器特别有用。

Oracle 数据库使用布隆过滤器来实现各种特定目标,包括:

  • 减少并行查询中传输到从属进程的数据量,尤其是当数据库因为不满足连接条件而丢弃大多数行时
  • 在连接中构建分区访问列表时消除不需要的分区,称为分区修剪
  • 测试服务器结果缓存中是否存在数据,从而避免磁盘读取
  • 过滤 Exadata Cell中的成员,尤其是在星型模式中加入大型事实表和小型维度表时

布隆过滤器可以在并行和串行处理中发生。

9.4.1.2 How Bloom Filters Work

布隆过滤器使用一组位来指示包含在集合中。

例如,数组中的 8 个元素(此示例中使用的任意数字)最初设置为 0:

e1 e2 e3 e4 e5 e6 e7 e80  0  0  0  0  0  0  0

这个数组代表一个集合。 为了表示该数组中的输入值 i,将三个单独的哈希函数(三个是任意的)应用于 i,每个生成一个介于 1 和 8 之间的哈希值:

f1(i) = h1
f2(i) = h2
f3(i) = h3

例如,要将值 17 存储在此数组中,哈希函数将 i 设置为 17,然后返回以下哈希值:

f1(17) = 5
f2(17) = 3
f3(17) = 5

在前面的示例中,两个哈希函数碰巧返回相同的值 5,称为哈希冲突。 因为不同的哈希值是 5 和 3,所以数组中的第 5 个和第 3 个元素设置为 1:

e1 e2 e3 e4 e5 e6 e7 e80 0 1 0 1 0 0 0

测试集合中 17 的成员资格会反转该过程。 要测试集合是否不包含值 17,元素 3 或元素 5 必须包含 0。如果任一元素中存在 0,则集合不能包含 17。不可能出现假阴性。

要测试集合是否包含 17,元素 3 和元素 5 都必须包含 1 值。 但是,如果测试表明两个元素都为 1,那么集合仍然可能不包括 17。可能出现误报。 例如,以下数组可能表示值 22,其中元素 3 和元素 5 的值也都为 1:

e1 e2 e3 e4 e5 e6 e7 e81 0 1 0 1 0 0 0

9.4.1.3 Bloom Filter Controls

优化器会自动判断是否使用布隆过滤器。

要覆盖优化器决策,请使用提示 PX_JOIN_FILTER 和 NO_PX_JOIN_FILTER。

在Oracle中,布隆过滤器也称为Join Filter。

9.4.1.4 Bloom Filter Metadata

V$ 视图包含有关 Bloom 过滤器的元数据。

您可以查询以下视图:

  • V$SQL_JOIN_FILTER
    此视图显示由活动布隆过滤器过滤掉(FILTERED 列)和测试(PROBED 列)的行数。
  • V$PQ_TQSTAT
    此视图显示在执行树的每个阶段通过每个并行执行服务器处理的行数。 您可以使用它来监控布隆过滤器减少了并行进程之间的数据传输量。

在执行计划中,布隆过滤器由 Operation 列中的关键字 JOIN FILTER 和 Name 列中的前缀 :BF 指示,如以下计划片段的第 9 步所示:

---------------------------------------------------------------------------
| Id  | Operation                 | Name     |    TQ  |IN-OUT| PQ Distrib |
---------------------------------------------------------------------------
...
|   9 |      JOIN FILTER CREATE   | :BF0000  |  Q1,03 | PCWP |            |

在计划的 Predicate Information 部分中,包含以字符串 SYS_OP_BLOOM_FILTER 开头的函数的过滤器表示使用 Bloom 过滤器。

9.4.1.5 Bloom Filters: Scenario

SELECT /*+ parallel(s) */ p.prod_name, s.quantity_sold
FROM   sh.sales s, sh.products p, sh.times t
WHERE  s.prod_id = p.prod_id
AND    s.time_id = t.time_id
AND    t.fiscal_week_number = 18;SELECT * FROMTABLE(DBMS_XPLAN.DISPLAY_CURSOR(format => 'BASIC,+PARALLEL,+PREDICATE'));---------------------------------------------------------------------------
| Id | Operation                  | Name     |    TQ  |IN-OUT| PQ Distrib |
---------------------------------------------------------------------------
|  0 | SELECT STATEMENT           |          |        |      |            |
|  1 |  PX COORDINATOR            |          |        |      |            |
|  2 |   PX SEND QC (RANDOM)      | :TQ10003 |  Q1,03 | P->S | QC (RAND)  |
|* 3 |    HASH JOIN BUFFERED      |          |  Q1,03 | PCWP |            |
|  4 |     PX RECEIVE             |          |  Q1,03 | PCWP |            |
|  5 |      PX SEND BROADCAST     | :TQ10001 |  Q1,01 | S->P | BROADCAST  |
|  6 |       PX SELECTOR          |          |  Q1,01 | SCWC |            |
|  7 |        TABLE ACCESS FULL   | PRODUCTS |  Q1,01 | SCWP |            |
|* 8 |     HASH JOIN              |          |  Q1,03 | PCWP |            |
|  9 |      JOIN FILTER CREATE    | :BF0000  |  Q1,03 | PCWP |            |
| 10 |       BUFFER SORT          |          |  Q1,03 | PCWC |            |
| 11 |        PX RECEIVE          |          |  Q1,03 | PCWP |            |
| 12 |         PX SEND HYBRID HASH| :TQ10000 |        | S->P | HYBRID HASH|
|*13 |          TABLE ACCESS FULL | TIMES    |        |      |            |
| 14 |      PX RECEIVE            |          |  Q1,03 | PCWP |            |
| 15 |       PX SEND HYBRID HASH  | :TQ10002 |  Q1,02 | P->P | HYBRID HASH|
| 16 |        JOIN FILTER USE     | :BF0000  |  Q1,02 | PCWP |            |
| 17 |         PX BLOCK ITERATOR  |          |  Q1,02 | PCWC |            |
|*18 |          TABLE ACCESS FULL | SALES    |  Q1,02 | PCWP |            |
---------------------------------------------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------3 - access("S"."PROD_ID"="P"."PROD_ID")8 - access("S"."TIME_ID"="T"."TIME_ID")13 - filter("T"."FISCAL_WEEK_NUMBER"=18)18 - access(:Z>=:Z AND :Z<=:Z)filter(SYS_OP_BLOOM_FILTER(:BF0000,"S"."TIME_ID"))

示意图如下,总共建立了一个布隆过滤器,原文有详细的文字介绍:

9.4.2 Partition-Wise Joins

分区连接是一种优化,它将两个表的大连接(其中一个必须在连接键上分区)分成几个较小的连接。

分区连接是以下之一:

  • 完全分区连接
    两个表必须在它们的连接键上均等分区,或者使用引用分区(即,通过引用约束相关联)。 数据库将一个大连接分成两个连接表的两个分区之间的小连接。
  • 部分分区连接
    只有一张表在连接键上被分区。 另一个表可能已分区,也可能未分区。

9.4.2.1 Purpose of Partition-Wise Joins

当连接并行执行时,分区连接通过最小化并行执行服务器之间交换的数据量来减少查询响应时间。

该技术显著缩短了响应时间并提高了 CPU 和内存的使用率。 在 Oracle 真正应用集群 (Oracle RAC) 环境中,分区连接还可以避免或至少限制互连上的数据流量,这是为大规模连接操作实现良好可扩展性的关键。

9.4.2.2 How Partition-Wise Joins Work

当数据库串行连接两个分区表而不使用智能分区连接时,单个服务器进程执行连接。

在下图中,连接不是分区方式的,因为服务器进程将表 t1 的每个分区连接到表 t2 的每个分区。

感觉有点像笛卡尔积。

9.4.2.2.1 How a Full Partition-Wise Join Works

数据库以串行或并行方式执行完全智能分区连接。

下图显示了并行执行的完整分区连接。 在这种情况下,并行粒度是一个分区。 每个并行执行服务器成对加入分区。 例如,第一个并行执行服务器将 t1 的第一个分区连接到 t2 的第一个分区。 然后并行执行协调器组装结果。

完全分区连接也可以将分区连接到子分区,这在表使用不同的分区方法时很有用。 例如,客户按哈希分区,但销售额按范围分区。 如果您通过哈希对 sales 进行子分区,那么数据库可以在客户的哈希分区和 sales 的哈希子分区之间执行完整的分区连接。

在执行计划中,在连接之前存在分区操作表示存在完整的分区连接,如下面的代码片段所示:

|   8 |         PX PARTITION HASH ALL|
|*  9 |          HASH JOIN           |
9.4.2.2.2 How a Partial Partition-Wise Join Works

与完整的分区连接不同,部分分区连接必须并行执行。

下图显示了已分区的 t1 和未分区的 t2 之间的部分分区连接。

因为 t2 没有分区,一组并行执行服务器必须根据需要从 t2 生成分区。 然后,一组不同的并行执行服务器将 t1 分区连接到动态生成的分区。 并行执行协调器组装结果。

在执行计划中,操作 PX SEND PARTITION (KEY) 表示部分分区连接,如以下代码段所示:

|  11 |            PX SEND PARTITION (KEY)    |

9.4.3 In-Memory Join Groups

连接组是用户创建的对象,其中列出了可以有意义地连接的两个或多个列。

在某些查询中,连接组消除了解压缩和散列列值的性能开销。 加入组需要内存中列存储(IM 列存储)。

SQL调优指南笔记9:Joins相关推荐

  1. SQL调优指南笔记6:Explaining and Displaying Execution Plans

    本文为SQL Tuning Guide第6章"解释和显示执行计划"的笔记. 了解如何解释SQL语句并显示其计划对于 SQL 调优至关重要. 重要基本概念 row source tr ...

  2. SQL调优指南笔记19:Influencing the Optimizer

    本文为SQL Tuning Guide第19章"Influencing the Optimizer"的笔记. 重要基本概念 driving table The table to w ...

  3. SQL调优指南笔记23:Performing Application Tracing

    本文为SQL Tuning Guide 第23章"Performing Application Tracing"的笔记. 本章解释什么是端到端应用程序跟踪,以及如何生成和读取跟踪文 ...

  4. SQL调优指南笔记11:Histograms

    本文为SQL Tuning Guide第11章"Histograms"的笔记. 重要基本概念 endpoint value An endpoint value is the hig ...

  5. SQL调优指南笔记8:Optimizer Access Paths

    本文为SQL Tuning Guide第8章"优化访问路径"的笔记. 重要基本概念 access path The means by which the database retr ...

  6. SQL调优指南笔记16:Managing Historical Optimizer Statistics

    本文为SQL Tuning Guide第16章"Managing Historical Optimizer Statistics"的笔记. 本章讲述如何保留.报告和恢复非当前统计信 ...

  7. sql调优的几种方式_「数据库调优」屡试不爽的面试连环combo

    点赞再看,养成习惯,微信搜索[三太子敖丙]关注这个互联网苟且偷生的工具人. 本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试完整考点.资料以及我的 ...

  8. 面试中sql调优的几种方式_面试方式

    面试中sql调优的几种方式 The first question I ask someone in an interview for a cybersecurity position is, &quo ...

  9. oracle sql 执行计划分析_Oracle SQL调优系列之看懂执行计划explain

    1.文章写作前言简介 SQL调优系列博客链接:SQL调优专栏 之前曾经拜读过<收获,不止sql调优>一书,此书是国内DBA写的一本很不错的调优类型的书,是一些很不错的调优经验的分享.虽然读 ...

最新文章

  1. stateful openflow------整理openstate原理以及具体应用
  2. 重入锁、死锁、活锁、公平非公平锁……一下子都给你屡清楚了
  3. 步步理解 JAVA 泛型编程 – 共三篇
  4. JavaScript ES 5 语法 重构 new
  5. Teams Bot如何判断用户所在的时区
  6. AT2300-[ARC068C]Snuke Line【整除分块】
  7. BZOJ2503: 相框
  8. Javascript二进制运算符的一些运用场景
  9. perl DBD Informix install and test
  10. 某大型银行深化系统技术方案之六:系统架构之运作流程
  11. 有关i++问题,和一些另外的易错点
  12. centos之ctrl+z
  13. 喝酒必备神器微信小程序源码下载免服务器和域名带流量主收益
  14. 微观经济学案例分析(五)
  15. 制作游戏辅助/外挂违法吗?
  16. 小区宽带网络解决方案
  17. IOS:IOS集成开发和环境的介绍
  18. bootstrap:导航栏【基础、简单、实用】
  19. iOS中更新版权 Copyright
  20. ROS Stage学习

热门文章

  1. 匹兹堡大学申请条件计算机科学,美国匹兹堡大学计算机科学专业好不好?
  2. 1G~5G的关键技术和技术标准
  3. 吞噬星空是鸿蒙三部曲吗,星辰变吞噬星空盘龙三部有什么关系
  4. 新旧电脑数据如何迁移?电脑数据导入到另一台电脑
  5. 麦肯锡解决问题的7个步骤
  6. 广州Uber优步司机奖励政策(2月1日~2月7日)
  7. 星女郎PK谋女郎 颜值衣品谁更胜一筹?
  8. 计算机软件提供的审计抽样,2017年注会考试审计考点之审计抽样在控制测试中的应用...
  9. Halcon 简单入门3D点云计算高度
  10. python 爬虫 小姐姐