为什么这个 Oracle 查询这么慢?

Why is this Oracle query so slow?

解释我是如何陷入这种查询性能混乱的小故事:
我得到了一个旧的 Oracle 数据库来修复和维护。

数据模型设计的很烂,没有远见
数据库状况糟糕,执行速度极慢。
当然,这个数据库已经 6 年多没有调整或修改了...

我从这个数据库中的许多 "snapshot" table 之一开始。
(而不是正确地跟踪历史,原始开发人员只是安排记录的快照被复制并存储在额外的 table 中。所以你必须查询这些快照 table 来获取历史分析.)

这个 table 有大约 100 列,但我们不关心其中的大部分。
然而,当我几天前开始研究这个时,这个 table 有 只有一个索引 ID 列的非唯一索引,没有根本没有主键约束。

这是一个快照 table,这意味着它包含来自不同时间点的行的历史副本。
例如,9/20/19 上的第 #12345 行,同样是同一行 #12345,但与 9/30/19 上的一样
所以,唉,ID 列必须允许重复值。

所以我想,第一步:一起在 IDsnapshot_date 上创建一个复合主键约束,以创建一个适当的唯一标识符。

我正在尝试构建的分析查询是此应用程序中已使用的许多不同的预先存在的查询的科学怪人。
它看起来像垃圾,因为那是我必须处理的...

select efh.snapshot_date,
       max(efhp.snapshot_date) as previous_snapshot_date,
       substr(efh.edge_vp,1,instr(efh.edge_vp,'@oracle.com')-1) as edge_vp,
       substr(efh.edge_rm,1,instr(efh.edge_rm,'@oracle.com')-1) as edge_rm,
       sum(case when efh.oppty_status = 'Open' then NVL(efh.ARR_FORECAST, 0) else 0 end) as forecast,
       sum(case when efh.oppty_status = 'Open' then NVL(efh.ARR_BEST,0) else 0 end) as best,
       sum(case when efh.oppty_status = 'Won' then NVL(efh.ARR,0) else 0 end) as closed,
       sum(case when efh.oppty_status = 'Open' then nvl(efh.ARR_PIPELINE,0) else 0 end) as pipeline,
       sum(case when efh.oppty_status = 'Open' then NVL(efh.ARR_BEST,0) else 0 end) +
       sum(case when efh.oppty_status = 'Open' then nvl(efh.ARR_PIPELINE,0) else 0 end) as pipe_best,
       sum(case when efh.oppty_status = 'Won' then efh.ARR else 0 end) +
       sum(case when efh.oppty_status = 'Open' then NVL(efh.ARR_FORECAST,0) else 0 end) as closed_forecast

  from edge_forecast_hist efh
  left join edge_forecast_hist efhp on efhp.edge_vp = efh.edge_vp and efhp.edge_rm = efh.edge_rm and efhp.snapshot_date < efh.snapshot_date
 where efh.snapshot_date >= TRUNC(sysdate) - INTERVAL '70' DAY
   and efh.edge_asm != 'REDACTED'
   and efh.oppty_status in ('Open', 'Won')

group by efh.snapshot_date,
         substr(efh.edge_vp,1,instr(efh.edge_vp,'@oracle.com')-1),
         substr(efh.edge_rm,1,instr(efh.edge_rm,'@oracle.com')-1)
order by 1, 2, 3, 4

一开始,这个查询执行大约需要 5 分钟。

点击图片放大

在我创建了一个主键以及在该查询中引用的每一列上的索引之后,执行时间下降到大约 4 分钟,这绝对是一个改进,但不如我预期的那么好。
(此查询仅 returns 几百行。)

当我尝试 explain 这个查询时,我注意到实际上只有少数索引被使用。
(请参阅上面的屏幕截图中指示正在使用哪些索引的三个复选标记。)

并且执行计划中有一些令人不安的语言,
比如NESTED LOOPSTABLE ACCESS(table扫描??)

点击图片放大

这是一个糟糕的数据库,我是 Oracle 的新手,我并不完全理解执行计划中每件事的细微差别。
什么似乎是这里的瓶颈,我该如何缓解它?

想到的一些想法:

如果真的有帮助,我可以做几个小时的工作,尝试将那些 case 语句拆分出来。
(即在多个子查询中计算结果,将 case 条件移动到 where 子句中。)
但我还假设 Oracle 足够聪明,可以找到最有效的执行计划,而无需填鸭式喂食。
换句话说,我不想做所有的工作,只是为了得到完全相同的执行计划。
我需要知道实际问题是什么,这样我才能制定一个有针对性的解决方案来正确解决根本原因。

我很确定您可以将此查询替换为使用分析函数的查询,这样会快得多。

过滤使这有点棘手。通常,这意味着 运行 window 在子查询中起作用,然后在外部查询中进行过滤。但是,可能与 JOIN 条件相互作用。

综上所述,您想要的查询是这样的:

select efh.snapshot_date, efh.previous_snapshot_date,
       efh.edge_vp, efh.edge_rm,
       efh.forecast,
       . . .
from (select eft.*,
             lag(efh.snapshot_date) over (partition by efh.edge_vp, efh.edge_rm order by efh.snapshot_date) as previous_snapshot_date,
             substr(efh.edge_vp, 1, instr(efh.edge_vp,'@oracle.com')-1) as edge_vp,
             substr(efh.edge_rm, 1, instr(efh.edge_rm,'@oracle.com')-1) as edge_rm,
             sum(case when efh.oppty_status = 'Open' then efh.ARR_FORECAST else 0 end) over (partition by efh.edge_vp, efh.edge_rm order by efh.snapshot_date) as forecast,
             . . . 
      from edge_forecast_hist efh
     ) efh
where efh.snapshot_date >= TRUNC(sysdate) - INTERVAL '70' DAY and
      efh.edge_asm <> 'REDACTED' and
      efh.oppty_status in ('Open', 'Won');

附加列应遵循与 forecast.

相同的结构

这与@Gordon Linoff 有一些相似之处,但使用 Rows Unbounded Preceding 的附加分析函数来创建一个累积总和,这正是您的查询所执行的操作:

SELECT snapshot_date, previous_snapshot_date, edge_vp, edge_rm
, forecast, best, closed, pipeline
, best + pipeline AS pipe_best
, closed + forecast AS closed_forecast

FROM
(
  SELECT efh.snapshot_date
  , LAG(efh.snapshot_date) OVER (PARTITION BY efh.edge_vp, efh.edge_rm ORDER BY snapshot_date) as previous_snapshot_date
  , substr(efh.edge_vp,1,instr(efh.edge_vp,'@oracle.com')-1) as edge_vp
  , substr(efh.edge_rm,1,instr(efh.edge_rm,'@oracle.com')-1) as edge_rm
  , SUM(CASE WHEN efh.oppty_status = 'Open' THEN efh.ARR_FORECAST END) OVER (PARTITION BY efh.edge_vp, efh.edge_rm ORDER BY snapshot_date ROWS UNBOUNDED PRECEDING) as forecast
  , SUM(CASE WHEN efh.oppty_status = 'Open' THEN efh.ARR_BEST END) OVER (PARTITION BY efh.edge_vp, efh.edge_rm ORDER BY snapshot_date ROWS UNBOUNDED PRECEDING) as best
  , SUM(CASE WHEN efh.oppty_status = 'Won' THEN efh.ARR END) OVER (PARTITION BY efh.edge_vp, efh.edge_rm ORDER BY snapshot_date ROWS UNBOUNDED PRECEDING) as closed
  , SUM(CASE WHEN efh.oppty_status = 'Open' THEN efh.ARR_PIPELINE END) OVER (PARTITION BY efh.edge_vp, efh.edge_rm ORDER BY snapshot_date ROWS UNBOUNDED PRECEDING) as pipeline

  FROM edge_forecast_hist efh

  WHERE efh.snapshot_date >= TRUNC(sysdate) - INTERVAL '70' DAY
   AND efh.edge_asm != 'REDACTED'
   AND efh.oppty_status in ('Open', 'Won')
)

只有一个版本 - 没有 window 函数(虽然它们 一个好主意)但是 JOIN 有两个派生表(一个带有 efh.oppty_status = 'Open' 和另一个 efh.oppty_status = 'Won' 为了摆脱 SUM 中的所有条件。同时利用 edge_vp 分组与 substr(efh.edge_vp,1,instr(efh.edge_vp,'@oracle.com')-1) 分组相同的事实因为 @oracle.com.

之后的字符串中没有任何内容
select o.snapshot_date, 
       greatest(o.previous_snapshot_date, w.previous_snapshot_date) as previous_snapshot_date,
       substr(o.edge_vp,1,instr(o.edge_vp,'@oracle.com')-1) as edge_vp,
       substr(o.edge_rm,1,instr(o.edge_rm,'@oracle.com')-1) as edge_rm,
       o.forecast,
       o.best,
       w.closed,
       o.pipeline,
       o.best + o.pipeline AS pipe_best,
       w.closed + o.forecast AS closed_forecast
from (select efh.snapshot_date,
             max(efhp.snapshot_date) as previous_snapshot_date,
             efh.edge_vp,
             efh.edge_rm,
             sum(NVL(efh.ARR_FORECAST, 0)) as forecast,
             sum(NVL(efh.ARR_BEST,0)) as best,
             sum(nvl(efh.ARR_PIPELINE,0)) as pipeline
      from edge_forecast_hist efh
      left join edge_forecast_hist efhp on efhp.edge_vp = efh.edge_vp and efhp.edge_rm = efh.edge_rm and efhp.snapshot_date < efh.snapshot_date
      where efh.snapshot_date >= TRUNC(sysdate) - INTERVAL '70' DAY
        and efh.edge_asm != 'REDACTED'
        and efh.oppty_status = 'Open'
      group by efh.snapshot_date,
               efh.edge_vp,
               efh.edge_rm
      ) o
join (select efh.snapshot_date,
             max(efhp.snapshot_date) as previous_snapshot_date,
             efh.edge_vp,
             efh.edge_rm,
             sum(NVL(efh.ARR,0)) as closed
      from edge_forecast_hist efh
      left join edge_forecast_hist efhp on efhp.edge_vp = efh.edge_vp and efhp.edge_rm = efh.edge_rm and efhp.snapshot_date < efh.snapshot_date
      where efh.snapshot_date >= TRUNC(sysdate) - INTERVAL '70' DAY
        and efh.edge_asm != 'REDACTED'
        and efh.oppty_status = 'Won'
      group by efh.snapshot_date,
               efh.edge_vp,
               efh.edge_rm
      ) w ON w.snapshot_date = o.snapshot_date AND w.edge_vp = o.edge_vp AND w.edge_rm = o.edge_rm
order by 1, 2, 3, 4