【问题标题】:Why Oracle similar queries generate explain plan very different为什么 Oracle 类似查询生成的解释计划大不相同
【发布时间】:2021-11-14 11:29:17
【问题描述】:

为什么以下非常相似的查询会产生截然不同的计划?

查询 1

SELECT *
FROM ACTORREPORTADO_RIESGO ars1
WHERE ars1.fechariesgo =
            ( SELECT MAX(fechariesgo) FROM actorreportado_riesgo ars2
                WHERE ars2.idactorreportado=ars1.idactorreportado
            )
        AND COALESCE(nivelriesgo,' ') NOT IN ('N',' ')  this is the line diff

查询 2

SELECT *
FROM ACTORREPORTADO_RIESGO ars1
WHERE ars1.fechariesgo =
            ( SELECT MAX(fechariesgo) FROM actorreportado_riesgo ars2
                WHERE ars2.idactorreportado=ars1.idactorreportado
            )
        AND nivelriesgo NOT IN ('N',' ');  -- this is the line diff

表格有 15 行

CREATE TABLE ACTORREPORTADO_RIESGO (
  IDACTORREPORTADO NUMBER(17) NOT NULL,
  FECHARIESGO DATE NOT NULL,
  NIVELRIESGO VARCHAR2(10),
  USUARIORIESGO VARCHAR2(50),
  OBSERVACIONES VARCHAR2(500),
  CONSTRAINT PK_ACTORREPORTADO_RIESGO PRIMARY KEY (IDACTORREPORTADO, FECHARIESGO)
)

【问题讨论】:

  • 查询在逻辑上是等价的。然而,虽然这对于合格的程序员来说可能(应该是!)显而易见,但对优化器来说并不明显。优化器必须分析coalesce 的第二个参数,注意它是一个文字,然后检查NOT IN 列表中的值,并注意到相同的文字出现在列表中,因此可以转换第一个查询进入第二个,计划更便宜。这对优化器要求太多了。最好我们自己编写更简单、更高效的查询。

标签: sql oracle explain


【解决方案1】:

原因是COALESCE(nivelriesgo,' ') 是一个必须为每一行计算的表达式,因此数据库不能在nivelriesgo 上使用索引(如果它们存在)或直接使用表中的nivelriesgo 值。

如果您将其更改为逻辑上的等效项,我希望您会得到相同的两个计划:

AND nivelriesgo IS NOT NULL
AND nivelriesgo NOT IN ('N',' ')

【讨论】:

    【解决方案2】:

    tldr;优化器不知道COALESCE(nivelriesgo,' ') 是什么所以不会做得很好。

    细节: 这里最重要的是基数以及 Oracle 如何估计它。

    COALESCE(nivelriesgo,' ') NOT IN ('N',' ')
    

    Oracle 不知道表达式COALESCE(nivelriesgo,' ') 的分布,因此它不得不依靠一些硬编码猜测。对于NOT IN 列表中的每个元素,Oracle 将使用应用 5% 的选择性。所以两个元素给你 5% 和 5%(因为表达式的值不能是 'N' 而不是''),这给你 0.25% 的选择性。应用于您的基数 15:Oracle 估计此过滤器(单独)将为您提供 1 行(从 0.0375 向上取整)。

    如果 Oracle 认为通过表扫描应用过滤器将返回一行,那么它将认为它只需要运行一次标量相关子查询。这对优化器来说似乎很便宜,因为这个相关子查询有一个支持索引。

    当您将外部过滤器更改为

    nivelriesgo NOT IN ('N',' ')
    

    Oracle 突然有一些信息可以作为这个估计的基础——它知道你的表中有多少 nivelriesgo 的不同值(感谢统计数据),它知道这些值的范围是什么,它甚至可能对分布有所了解(如果您有直方图)。 根据您的计划,Oracle 估计此过滤器将返回 14 行(共 15 行)。将标量相关子查询循环 14 次显然不如只循环一次那么吸引人,所以这显然不是一个好主意。

    它已确定它可以重写您的查询,使其有效

    select ars1.*
    from  ( select ars2.idactorreportado
                  ,max(fechariesgo) max_fechariesgo 
            from   actorreportado_riesgo ars2
            group by ars2.idactorreportado
          ) ars2
    join  actorreportado_riesgo ars1
      on  ars2.idactorreportado = ars1.idactorreportado
     and  ars2.max_fechariesgo  = ars1.fechariesgo
    where ars1.nivelriesgo NOT IN ('N',' ');
    

    因此,不必每行执行一次标量相关子查询,它用您的其他过滤器标识,它已将其取消嵌套,并且只需执行一次即可。

    现在,这就是超级有趣的东西的用武之地,Oracle 对这个新子查询将返回多少行并不是 100% 的信心。如果结果数量很少,它将使用索引查找(使用主键)并保留该行,如果它与您的 nivelriesgo 过滤器匹配(它认为大多数行都是如此)。如果子查询有大量结果,它将只进行全表扫描(仅返回与您的nivelriesgo 过滤器匹配的行)并将其与子查询结果进行哈希连接 - 这对于大型结果集通常是最佳的。

    【讨论】:

      猜你喜欢
      • 2012-11-12
      • 2011-05-21
      • 2011-02-16
      • 1970-01-01
      • 2021-08-23
      • 2011-08-16
      • 2020-08-02
      • 2013-04-01
      • 2020-11-29
      相关资源
      最近更新 更多