【问题标题】:Oracle SQL - JOIN performance in comparing null valuesOracle SQL - 比较空值时的 JOIN 性能
【发布时间】:2012-05-14 02:57:07
【问题描述】:

早上好,

在我昨天在两个体面大小的结果集(每个

SELECT a JOIN b ON a.class = b.class OR (a.class is null AND b.class is null)

但是,我注意到一个严重的性能问题,主要集中在 OR 语句的使用上。我使用以下方法解决了这个问题:

SELECT a JOIN b ON NVL(a.class, 'N/A') = NVL(b.class, 'N/A')

第一个查询的运行时间长得令人无法接受,而第二个查询要快几个数量级(>45 分钟 vs.

什么会导致执行时间如此显着增加? Oracle SQL 是否不像许多其他语言那样短路布尔比较?有没有办法挽救第一个查询而不是第二个查询(用于一般 SQL 而不仅仅是 Oracle)?

【问题讨论】:

  • 您是否使用 EXPLAIN PLAN 来发现每个查询的不同执行路径?
  • 请注意,对于具有 NULL 值的行,您将在此处生成叉积。这真的是你想要的吗?
  • 在 JOIN 中有一些附加条件,我认为没有必要在示例中包含这些条件。目标是及时解决 null=null 的情况,以及为什么第一个查询与第二个查询相比几乎是冰冷的。
  • 您是否在查询中运行了EXPLAIN PLAN?请看下面我的回答。您的连接语法迫使优化器选择两个不同的数据路径,因为它不知道无论何时 a.class 都应该以任何方式、形状或形式与 b.class 进行比较。 class 为 null,b.class 为 null。

标签: sql performance oracle join


【解决方案1】:

在第一种情况下,因为每个 null 都不相同,所以数据库不使用优化(对于 a 中的每一行,请检查表 b 中的每一行)。

在第二种情况下,数据库首先将所有空值更改为“N/A”,然后仅比较 a.classb.class,使用优化

在 Oracle 中比较空值非常耗时。 Null 是未定义的值 - 一个 null 与另一个 null 不同。 比较两个几乎相同的查询的结果:

select 1 from dual where null is null

select 1 from dual where null = null

只有带有特殊 is null 子句的第一个查询返回正确答案。因此,null 值无法被索引。

【讨论】:

    【解决方案2】:

    试试这个:

    SELECT a from Table1 a JOIN JTable1 b ON a.class = b.class
    where a.class is null
    union all
    SELECT a from Table1 a JOIN JTable1 b ON a.class = b.class
    where b.class is null
    

    应该更快

    【讨论】:

    • 这根本不返回任何数据。 NULL != NULL 因此在任何一种情况下都不会发生连接。
    • 我想你的意思是:SELECT a.class, columna, columnb from a JOIN b ON a.class = b.class UNION ALL SELECT a.class, columna, columnb from a CROSS JOIN b where a.class is null and b.class is null。这通常仍然比将空值转换为值慢,因为您必须执行 HASH JOIN 和 MERGE JOIN CARTESIAN,然后是 UNION ALL。
    【解决方案3】:

    你为什么不让它更容易一点。 喜欢

    选择 * 从 a,b 在哪里 a.class(+)=b.class(+)

    我认为它更具可读性。

    【讨论】:

    • 即使该语法有效(我不认为它有效),它也会返回不同的结果集。
    • 是的,那不是一回事
    • 请停止使用过时的 Oracle Join 语法。
    【解决方案4】:

    您将返回一个带有任何具有空类的记录的叉积。您的结果可以吗?

    我在 11gR2 中创建了两个示例查询:

    WITH a as 
    (select NULL as class, 5 as columna from dual
     UNION
     select NULL as class, 7 as columna from dual
     UNION
     select NULL as class, 9 as columna from dual
     UNION
     select 'X' as class, 3 as columna from dual
     UNION
     select 'Y' as class, 2 as columna from dual),
     b as 
     (select NULL as class, 2 as columnb from dual
     UNION
     select NULL as class, 15 as columnb from dual
     UNION
     select NULL as class, 5 as columnb from dual
     UNION
     select 'X' as class, 7 as columnb from dual
     UNION
     select 'Y' as class, 9 as columnb from dual)
        SELECT * from a JOIN b ON (a.class = b.class 
                                  OR (a.class is null AND b.class is null))
    

    当我在这个查询上运行 EXPLAIN PLAN 时,它表明表(在我的例子中是内联视图)是通过 NESTED LOOPS 连接的。 NESTED LOOPS 连接通过扫描一个表的第一行,然后扫描另一个表的每一行以查找匹配项,然后扫描第一个表的第二行,在第二个表上查找匹配项等操作。因为您不是直接比较JOIN 的 OR 部分中的任一表,优化器都必须使用 NESTED LOOPS。

    在幕后它可能看起来像这样:

    • 获取表 A,第 1 行。如果类为空,则将表 A 中的这一行包含在结果集中。
    • 仍在表 A 的第 1 行时,在表 B 中搜索类为空的所有行。
    • 对表 A 第 1 行和表 B 中找到的所有行执行叉积
    • 在结果集中包含这些行
    • 获取表 A,第 2 行。如果类为空,则将表 A 中的这一行包含在结果集中。
    • ....等

    当我将 SELECT 语句更改为 SELECT * FROM a JOIN b ON NVL(a.class, 'N/A') = NVL(b.class, 'N/A') 时,EXPLAIN 表示使用了 HASH JOIN。散列连接本质上生成小表的每个连接键的散列,然后扫描大表,在小表中找到匹配的每一行的散列。在这种情况下,由于它是一个简单的 Equijoin,优化器可以毫无问题地对驱动表的每一行进行哈希处理。

    在幕后它可能看起来像这样:

    • 遍历表 A,将 NULL 类值转换为“N/A”
    • 散列表 A 的每一行。
    • 哈希表 A 现在位于临时空间或内存中。
    • 扫描表 B,将 NULL 类值转换为“N/A”,然后计算值的哈希值。在哈希表中查找哈希(如果存在),将来自表 A 和 B 的连接行包含在结果集中。
    • 继续扫描 B.

    如果您对查询运行 EXPLAIN PLAN,您可能会发现类似的结果。

    即使最终结果相同,但由于您没有在第一个查询中使用“OR”连接表,优化器无法使用更好的连接方法。如果驱动表很大,或者如果您强制对大型辅助表进行全表扫描,则嵌套循环可能会非常慢。

    您可以使用 ANSI COALESCE 函数来模拟其他数据库系统中的 NVL oracle 函数。这里真正的问题是你试图加入一个 NULL 值,在那里你真的应该有一个“NO CLASS”或其他一些识别“null”类的方法,在 null = nothing 而不是 null = unknown 的意义上.

    在 cmets 中回答您的问题的附录:

    对于空查询,SQL 引擎将执行以下操作:

    1. 从表 A 中读取第 1 行,类为空,转换为“N/A”。
    2. 表 B 有 3 行,其类为空,将每个空转换为“N/A”。
    3. 由于第一行与所有 3 行都匹配,因此将 3 行添加到我们的结果集中,其中一个用于 A1B1、A1B2、A1B3。
    4. 从表 A 中读取第 2 行,类为空,转换为“N/A”/
    5. 表 B 有 3 行,其类为空,将每个空转换为“N/A”。
    6. 由于第二行匹配所有 3 行,因此将 3 行添加到我们的结果集中,A2B1、A2B2、A2B3 各行。
    7. 从表 A 中读取第 3 行,类为空,转换为“N/A”/
    8. 表 B 有 3 行,其类为空,将每个空转换为“N/A”。
    9. 由于第三行与所有 3 行匹配,因此将 3 行添加到我们的结果集中,A3B1、A3B2、A3B3 各行。 10.. 第 4 行和第 5 行不为空,因此不会在连接的这一部分中处理它们。

    对于“N/A”查询,SQL 引擎将执行以下操作:

    1. 从表 A 中读取第 1 行,类为空,转换为“N/A”,散列此值。
    2. 从表 A 中读取第 2 行,类为空,转换为“N/A”,散列此值。
    3. 从表 A 中读取第 3 行,类为空,转换为“N/A”,散列该值。
    4. 从表 A 中读取第 4 行,类不为空,散列此值。
    5. 从表 A 中读取第 5 行,类不为空,散列此值。
    6. 哈希表 C 现在在内存中。
    7. 从表 B 中读取第 1 行,类为空,转换为“N/A”,散列值。
    8. 将哈希值与内存中的哈希表进行比较,为每个匹配添加一行到结果集中。找到 3 行,A1、A2 和 A3。结果添加了 A1B1、A2B1、A3B1。
    9. 从表 B 中读取第 2 行,类为空,转换为“N/A”,散列值。
    10. 将哈希值与内存中的哈希表进行比较,为每个匹配添加一行到结果集中。找到 3 行,A1、A2 和 A3。结果添加了 A1B2、A2B2、A3B2。
    11. 从表 B 中读取第 3 行,类为空,转换为“N/A”,散列值。
    12. 将哈希值与内存中的哈希表进行比较,为每个匹配添加一行到结果集中。找到 3 行,A1、A2 和 A3。结果已添加 A1B3、A2B3、A3B3。

    【讨论】:

    • 我没有太多使用 EXPLAIN PLAN 的经验,但是我看到第一个查询使用嵌套循环,这确实是大部分 CPU 成本的来源。谢谢你的清晰解释。
    • 编辑:除了我在示例中提到的之外,JOIN 中还使用了其他条件,但是您如何看到形成的叉积?在第二种情况下,如果 class 为 null,则将其转换为“N/A”,即两个表中都不存在的值,然后与“N/A”进行比较。还有什么我忽略的吗?
    • 请参阅我的答案附录。想象一下,如果我将 NULL 值替换为“A.class= 'Class1' 和 B.class= 'Class1',并且表 A 或表 B 上有多个 'Class1' 值”。
    【解决方案5】:

    解释很简单: 第一个必须在连接操作中使用嵌套循环,它总是在您使用 OR 操作时发生。 第二个必须使用哈希连接操作,比上一个更快。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-10-19
      • 2018-03-10
      • 2013-05-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-03-17
      • 1970-01-01
      相关资源
      最近更新 更多